SCF实现本地代理扫描

SCF

SCF全称(Serverless Cloud Function)即云函数,是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。 您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。

如何开发基于云函数的代理扫描?

首先需要明白通过云函数扫描的原理即我们本地的扫描器通过云函数代理转发HTTP请求(事实上云函数上也是可以直接run一些轻型扫描器)。将我们本地扫描器的请求通过MITMPROXY转发给云端函数由它发起真正的请求即可实现 本地 -> 云 -> 服务器 这一过程。

安装 & 使用 Mitmproxy

1
pip3 install mitmproxy

通过pip可以直接安装mitmproxy并通过如下命令启动:

1
mitmproxy -p 8080

开启mitmproxy后你可以设置监听的端口为HTTP代理并安装证书(否则你将无法转发HTTPS流量)

image-20210428140750684

接下来我们需要编写一个代理客户端脚本,不熟悉mitmproxy脚本开发的师傅可移步官方文档及Github示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
from base64 import b64encode, b64decode
from urllib.parse import urlparse
from mitmproxy import ctx
from random import choice
import mitmproxy.http
import json


TOKEN = 'zz123000' # SCF认证令牌, 可自定义
SERVER_URL = [] # 云函数API网关触发器URL

class Proxy:
def __init__(self):
pass

# 代理后的请求会经过该方法调用
def request(self, flow : mitmproxy.http.HTTPFlow):
# 原始请求对象
request = flow.request
# 封装请求数据
datas = {
'method': request.method,
'url': request.pretty_url,
'headers': dict(request.headers),
'params': dict(request.query),
'cookies': dict(request.cookies),
# 将原始数据内容(即POST请求体)进行base64编码
'data': b64encode(request.raw_content).decode()
}
# ctx.log.info(json.dumps(datas))
# 每次随机挑选一个SCF转发请求
PROXY_URL = choice(SERVER_URL)
# 封装新的请求对象
flow.request = flow.request.make(
method = 'POST',
url = PROXY_URL,
# 字典转为json发送
content = json.dumps(datas),
headers = {
'Host': urlparse(PROXY_URL).netloc,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36',
# 请求头中添加令牌认证
'Token': TOKEN
}
)

# SCF返回的响应内容会经过该方法调用
def response(self, flow : mitmproxy.http.HTTPFlow):
try:
# SCF返回200状态码表示请求成功
if flow.response.status_code == 200:
# 取出SCF请求返回的响应内容
datas = flow.response.content
datas = json.loads(b64decode(datas).decode())
# 封装响应对象
res = flow.response.make(
status_code = datas['status_code'],
content = datas['content'],
headers = datas['headers']
)
flow.response = res
else:
ctx.log.info(flow.response.status_code)
except Exception as e:
print(e)

addons = [
Proxy()
]

如上就是使用SCF代理所需的客户端脚本, 将请求数据包的内容重新封装后发往SCF即可,接着我们需要编写SCF服务端脚本, 来解析数据内容并重新封装请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# -*- coding: utf8 -*-
from base64 import b64decode, b64encode
import requests
import json


SCF_TOKEN = "zz123000"

# 认证失败返回提示信息
def authorization():
return {
"isBase64Encoded": False,
"statusCode": 401,
"headers": {},
"body": "Please provide correct Token",
}

def main_handler(event : dict, context : dict):
try:
# 请求头中获取令牌进行认证
token = event['headers']['Token']
if token != SCF_TOKEN:
return authorization()
except KeyError:
# 认证失败返回401
return authorization()
# 获取封装的请求数据
data = event["body"]
data = json.loads(data)
data['data'] = b64decode(data['data']).decode()
data.pop(list(data.keys())[-1])
try:
# 发送请求
res = requests.request(**data, verify=False)
text = {
'status_code': res.status_code,
'headers': dict(res.headers),
'content': res.text
}
return {
'isBase64Encoded': False,
'statusCode': 200,
'headers': {},
# 返回响应内容
'body': b64encode(json.dumps(text).encode('utf-8')).decode()
}
except (requests.exceptions.ConnectionError, Exception) as err:
error = str(err)
return {
'isBase64Encoded': False,
'statusCode': 408,
'headers': {},
# 请求失败返回异常信息
'body': b64encode(json.dumps(error).encode('utf-8')).decode()
}

在编写SCF脚本时需要注意, 首先需要从请求体中取出封装好的数据包, 将请求体的数据进行base64解码并转为字符串, 然后使用requests模块的request方法将 data 变量作为关键字参数传入。

踩坑

在将本地封装的请求数据转发至SCF时发现 mitmproxy自动在请求体中添加了一个随机参数, 暂时不知道是什么原因, 期待解答, 这里简单粗暴的做法就是在SCF中将最后一个参数移除。

image-20210428143858558

实现效果

image-20210428150923350

参考

SCFProxy