208 lines
9.6 KiB
Python
Executable File
208 lines
9.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Generate Burp autoDecoder integration documentation from analysis_result.json.
|
||
|
||
Usage:
|
||
python3 scripts/emit_burp_doc.py --analysis analysis_result.json --output generated/burp-autodecoder.md
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from common import load_json
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Generate Burp autoDecoder integration guide.")
|
||
parser.add_argument("--analysis", required=True, help="Path to analysis_result.json.")
|
||
parser.add_argument("--output", required=True, help="Output markdown file path.")
|
||
parser.add_argument("--status", default="", help="Optional Flask status JSON; uses the actual fallback port when present.")
|
||
args = parser.parse_args()
|
||
|
||
analysis = load_json(args.analysis)
|
||
flask = analysis.get("flask", {})
|
||
flask_server = analysis.get("flask_server", {})
|
||
jsrpc = analysis.get("jsrpc", {})
|
||
port = flask.get("port", flask_server.get("port", 5000))
|
||
status = load_json(args.status, {}) if args.status else {}
|
||
if status.get("status") == "started" and status.get("port"):
|
||
port = status["port"]
|
||
transforms = analysis.get("transforms") or []
|
||
parameters = list(analysis.get("parameters", {}).keys())
|
||
proxy_url = f"http://127.0.0.1:{port}{flask.get('route', '/autodecoder')}"
|
||
encode_url = status.get("encode_url", f"http://127.0.0.1:{port}/encode")
|
||
decode_url = status.get("decode_url", f"http://127.0.0.1:{port}/decode")
|
||
jsrpc_port = analysis.get("jsrpc_server", {}).get("port", 12080)
|
||
group = jsrpc.get("group", "jsra")
|
||
action = jsrpc.get("action_name", "encode_password")
|
||
sample_data_body = "&".join(f"{p}=<plaintext>" for p in parameters)
|
||
|
||
# Build transform table if transforms exist
|
||
if transforms:
|
||
rows = "\n".join(
|
||
f"| {t.get('id')} | {t.get('direction')} | {t.get('location')} | "
|
||
f"{t.get('content_type', 'auto')} | `{t.get('path', '')}` | `{t.get('action')}` |"
|
||
for t in transforms
|
||
)
|
||
transform_table = f"""
|
||
## 转换项
|
||
|
||
| ID | 方向 | 位置 | 内容类型 | 路径 | JSRPC Action |
|
||
|---|---|---|---|---|---|
|
||
{rows}
|
||
"""
|
||
else:
|
||
transform_table = ""
|
||
|
||
content = f'''# Burp autoDecoder 配置(JSRA)
|
||
|
||
> 重要:此代理不实现加密/解密算法。它只通过 JSRPC 远程调用当前浏览器页面已验证的真实函数。若真实入口、运行时证据或 JSRPC 不可用,代理会返回 `JSRA_ERROR`,不得继续发送请求。
|
||
|
||
## 本地服务
|
||
|
||
- Health:`http://127.0.0.1:{port}/health`
|
||
- Healthz:`http://127.0.0.1:{port}/healthz`
|
||
- 转换接口:`http://127.0.0.1:{port}/autodecoder?direction=request`
|
||
- 响应接口:`http://127.0.0.1:{port}/autodecoder?direction=response`
|
||
- 当前服务端口:`{port}`。如果服务启动时发生端口回退,文档优先使用 `artifacts/flask_status.json` 中的实际地址。
|
||
{transform_table}
|
||
## 配置原则
|
||
|
||
1. 仅匹配授权目标 URL。
|
||
2. 请求方向使用 `direction=request`,响应解密使用 `direction=response`。
|
||
3. 在 autoDecoder 页面直接粘贴完整 HTTP 请求/响应时,插件可能以 `application/octet-stream` 调用接口;代理会从内层报文自动识别真实 `Content-Type`,并返回完整改写后的报文。
|
||
4. 使用 `dataBody/dataHeaders` wrapper 时,`dataBody` 传 body,`dataHeaders` 传可选头部;JSON 请求也可用 `X-JSRA-Content-Type` 覆盖。
|
||
5. 首次联调前访问 `/health`。
|
||
6. 发生 `JSRA_ERROR` 时不要继续发送被破坏的请求。
|
||
7. 不要把页面抓到的公钥、模数或某次样本结果改写成 Python 加密实现。
|
||
|
||
## Burp autoDecoder 配置
|
||
|
||
### 截图所示的“接口加解密设置”页面
|
||
|
||
| 页面字段 | 配置值 |
|
||
|---|---|
|
||
| 解密接口 | `{decode_url}`(可与加密接口同时配置) |
|
||
| 加密接口 | `{encode_url}` |
|
||
| 请求方向 | 当前测试请求时选择“请求数据包” |
|
||
| 响应方向 | 当前测试响应时选择“响应数据包”;与“请求数据包”互斥 |
|
||
| 处理请求头 | 默认不选;只有签名依赖请求头时才启用 |
|
||
| 请求 base64 编码 | 只有原始请求体本身是 Base64 时才选 |
|
||
| 请求自动 base64 解码 | 只有服务端要求先解码 Base64 时才选 |
|
||
| Proxy、Repeater 等模块真实调试 | 联调时可选,用于观察返回的完整数据包 |
|
||
|
||
加密接口和解密接口可以同时配置。请求方向调试时,在“原始数据包”区域粘贴完整 HTTP 请求并选择“请求数据包”;响应方向调试时切换为“响应数据包”,再粘贴完整 HTTP 响应,两个方向不能同时选。对于截图中的整包模式,接口收到的是完整 HTTP 报文,外层常见 `Content-Type: application/octet-stream`;代理会解析内层 `Content-Type`、替换 body,并同步 `Content-Length`。对于 wrapper 模式,autoDecoder 通过 `dataBody` 传请求体,勾选“处理请求头”时额外传 `dataHeaders`,并传 `requestorresponse=request` 或 `response`。未处理请求头时整包模式返回完整改写报文,wrapper 模式返回改写后的 body;处理请求头时必须返回 `headers + "\\r\\n\\r\\n\\r\\n\\r\\n" + body`,这是 autoDecoder 的固定返回格式。
|
||
|
||
响应解密时使用已配置的 `{decode_url}`,切换为“响应数据包”后进行测试;如果当前只验证请求加密,也可以保留解密接口配置不使用。
|
||
|
||
### 响应体配置
|
||
|
||
| 页面字段 | 配置值 |
|
||
|---|---|
|
||
| 响应数据方向 | 选择“响应数据包” |
|
||
| 解密接口 | `{decode_url}` |
|
||
| 处理响应头 | 只有响应解密依赖响应头时才启用 |
|
||
| 响应 base64 编码 | 只有响应体传入接口前需要 Base64 编码时才选 |
|
||
| 响应自动 base64 解码 | 只有接口返回 Base64、需要由 autoDecoder 还原二进制响应时才选 |
|
||
| 响应请求标识 | `requestorresponse=response` |
|
||
|
||
响应方向启用请求头处理时,接口接收 `dataBody`、`dataHeaders` 和 `requestorresponse=response`,并返回 `响应头 + \\r\\n\\r\\n\\r\\n\\r\\n + 解密后的响应体`。未启用响应头处理时只返回解密后的响应体。
|
||
|
||
响应体验证:
|
||
|
||
```bash
|
||
curl --noproxy '*' -sS -X POST {decode_url} \\
|
||
-H 'Content-Type: application/x-www-form-urlencoded' \\
|
||
--data-urlencode 'dataBody=<原始响应体>' \\
|
||
--data-urlencode 'dataHeaders=<可选的原始响应头>' \\
|
||
--data-urlencode 'requestorresponse=response'
|
||
```
|
||
|
||
预期结果为解密后的响应体;启用响应头处理时,返回值必须包含四组 CRLF 分隔的响应头和响应体。
|
||
|
||
### 页面配置验证(标准 autoDecoder 协议)
|
||
|
||
```bash
|
||
curl --noproxy '*' -sS -X POST {encode_url} \\
|
||
-H 'Content-Type: application/x-www-form-urlencoded' \\
|
||
--data-urlencode 'dataBody=<原始请求体>' \\
|
||
--data-urlencode 'requestorresponse=request'
|
||
```
|
||
|
||
勾选“处理请求头”时增加:
|
||
|
||
```bash
|
||
--data-urlencode 'dataHeaders=<原始请求头>'
|
||
```
|
||
|
||
此时预期返回格式为:`原始请求头 + \\r\\n\\r\\n\\r\\n\\r\\n + 改写后的请求体`。
|
||
|
||
### 完整 HTTP 数据包调试
|
||
|
||
生成的 `/encode` 和 `/decode` 也支持直接提交完整 HTTP 数据包,便于脱离 Burp 页面调试;该模式会拆分请求头和请求体,并自动更新 `Content-Length`。整包验证示例:
|
||
|
||
```bash
|
||
curl --noproxy '*' -sS -X POST {encode_url} \\
|
||
-H 'Content-Type: application/octet-stream' \\
|
||
--data-binary $'POST /newlogin/login.do?appName=arena&fromSite=77 HTTP/1.1\\r\\nHost: 127.0.0.1:8123\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\n\\r\\nloginId=admin&password2=123456'
|
||
```
|
||
|
||
预期返回完整 HTTP 请求,且只替换目标字段;若返回 `JSRA_ERROR`,先检查 JSRPC action 是否仍注册在当前浏览器页面。
|
||
|
||
### 兼容方式:dataBody/dataHeaders
|
||
|
||
Burp autoDecoder 插件使用 `dataBody` 和 `dataHeaders` 表单字段:
|
||
|
||
```
|
||
POST {proxy_url} HTTP/1.1
|
||
Content-Type: application/x-www-form-urlencoded
|
||
|
||
dataBody=<原始请求体>&dataHeaders=<原始请求头>
|
||
```
|
||
|
||
- `dataBody`:原始请求体(JSON 或 form-urlencoded)
|
||
- `dataHeaders`:原始请求头(可选,用于传递 Cookie、Authorization 等)
|
||
|
||
### 方式二:直接 POST
|
||
|
||
```
|
||
POST {proxy_url}?direction=request HTTP/1.1
|
||
Content-Type: <原始 Content-Type>
|
||
|
||
<原始请求体>
|
||
```
|
||
|
||
## 验证步骤
|
||
|
||
1. 在目标页面加载 `JsEnv_Dev.js` 和生成的 `jsrpc_inject.js`。
|
||
2. 确认浏览器 Console 出现 JSRPC connected,以及已注册的 action。
|
||
3. 用一个受控样本调用 JSRPC:
|
||
```bash
|
||
curl 'http://127.0.0.1:{jsrpc_port}/go?group={group}&action={action}¶m=test123'
|
||
```
|
||
4. 验证 Flask 代理:
|
||
```bash
|
||
curl -X POST {proxy_url} \\
|
||
-H "Content-Type: application/x-www-form-urlencoded" \\
|
||
--data-urlencode "dataBody={sample_data_body}"
|
||
```
|
||
5. 确认返回值与 JSRPC 调用结果一致。
|
||
|
||
## 故障排查
|
||
|
||
- `JSRA_ERROR: ...:EvidenceMissing:...`:没有真实运行时证据或候选入口不可用;重新注入探针、导出证据并运行差分验证。
|
||
- `JSRA_ERROR: ...:RuntimeError:...`:页面真实函数执行失败;检查页面状态、依赖、DOM 和存储前置条件。
|
||
- `JSRA_ERROR` / HTTP 502:JSRPC 连接、响应格式或页面入口失败;不要发送该请求,先恢复浏览器和 JSRPC。
|
||
'''
|
||
|
||
path = Path(args.output)
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(content, encoding="utf-8")
|
||
print(json.dumps({"status": "ok", "output": str(path)}, ensure_ascii=False))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|