前言
最新的测试中,经常遇到HTTP报文加密/加签传输的情况,这导致想要查看和修改明文报文很不方便。
之前应对这种情况我们有几种常见的办法解决,比如使用burpy插件、在Burp上下游使用mitmproxy进行代理等,但这些使用起来不太方便,并且偶尔遇到python和java间加密算法上的一些小差异,需要调试很久。
因此在想是否可以做一款Burp插件来解决这些问题,而且使用上要简单高效。在看了Burp新版接口 Montoya API
的介绍后,发现可以满足我们的需求,并且还有一些意想不到的发现,比如它可以满足 动态密钥
的场景。
原理
正常情况下,我们通过Burp代理进行渗透测试时,流量的流转情况如图所示。
而通过Burp的 Montoya API
我们可以分别做到 Request/Response 在 Client/Burp/Server 流转时实现自己对其的处理逻辑,像图中这样。
Burp Proxy
:Burp中的Proxy模块,即UI中看到的Proxy那样,负责代理以及流量记录等。Burp Http
:Burp中的Http模块,负责请求最终从Burp出去、响应刚到达Burp时的处理等。
而要实现我们的需求,很显然只需要在图中①,②,③,④进行不同的处理逻辑即可:
①:HTTP请求从客户端到达Burp时被触发。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文。
②:HTTP请求从Burp将要发送到Server时被触发。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。
③:HTTP请求从Server到达Burp时被触发。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文。
④:HTTP请求从Burp将要发送到Client时被触发。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client。
设计思路
逻辑成立,实现开始。
请求过滤
据过去遇到的情况,同一个系统可能出现部分流量加密的情况、甚至还有根据某个请求头的不同,不加密或者使用不同的加密方式。
想要支持这些情况,采用host白名单,url白名单的方案有其弊端,因此我引入了表达式的方式,你可以通过实现一个表达式,来过滤哪些请求需要/不需要处理,比如我们默认的表达式为:
!request.isStaticExtension() && request.host=='192.168.1.4'
表达式采用JS的语法,执行完成会返回一个
boolean
类型,用于判断是否过滤。
这条的意思是指请求不能是静态后缀,且host必须是192.168.1.4,在使用过程中用户可以根据自己需求修改,以应对各种特殊的场景。
核心实现
还是根据经验来,我想要的实现需要满足以下条件:
-
可以根据某个请求头的不同,使用不同的加密方式。
-
可以满足在加密加签同时存在、加密时算法组合的情况。
-
最好可以使用java中的加解密库,避免不同语言算法实现上的小差异。
调研了现在一些常见的实现思路,有的通过配置,但配置总会有不足;有的可以让用户自定义代码后在Repeater模块中右键使用,很灵活但无法做到对代理请求/响应自动解密。
综合考虑了上述方式的缺点,借鉴了其优点,提炼出自己的方案。
我将该功能命名为 HttpHook
,四个阶段分别为 hookRequestToBurp
,hookRequestToServer
, hookResponseToBurp
, hookResponseToClient
。他们分别接收请求或响应对象,由用户对其进行自定义处理。四个阶段分别有多种实现方式:
Python
:通过python代码实现四个函数,原理为将python编译并在jvm中运行。JS
:通过js代码实现四个函数,原理为将js编译并在jvm中运行。Java
:通过java代码实现四个函数,原理为动态加载。Grpc
:通过实现Grpc服务实现四个rpc接口,算是兜底方案。
这四种实现方式,可以分为两类:
Grpc
:你需要用其他语言实现Grpc Server,并自行通过三方库实现对应 Hook 接口
应有的功能。Code
:你需要用支持的方式编写对应语言的脚本,在脚本中组合、调用项目中的DataObjects和Util,实现对应 Hook 函数
应有的功能。
它们各有优缺点:
Grpc
:优点是跨语言能力强,运行兼容性强;缺点是学习成本稍高、依赖IO -> 可能存在性能问题、不同语言算法间可能存在兼容性问题,在动态密钥的情况下很难实现需求。Code
:优点是可以与JVM交互调用Java中的加解密库 -> 对Java来说没有算法兼容性的问题;缺点是需要熟悉项目自带的DataObjects和Utils,并且可能存在运行兼容性的问题。
上手难度
看了上边的实现,你发现居然还要我写代码,好麻烦。确实,不可否认,我们为了覆盖复杂的场景,采用了写代码这种适应性最强的方式。
因此我们采用尽量多而全的自带示例,来减少了上手难度。项目中涵盖了大量的示例,如AES-CBC
,AES-ECB
, AES-GCM
, RSA
, SM2
等常见算法(后续也会持续扩充),对于这些算法,可以做到开箱即用,至于更复杂的场景,你需要熟悉这些示例中的代码,灵活运用,从而解决问题。
效果演示
github地址:https://github.com/outlaws-bai/Galaxy
常规情况
这块看文章顶部的 gif
即可,更加直观,也比较简单,我们详细演示动态密钥情况下的使用。
动态密钥
示例编写
首先,我们写一个简单用于测试的客户端(index.html)和服务端(server.py),加密算法使用AES-CBC
。
index.html
:在用户打开页面时,请求 /api/getSecret
,获取加密使用的 key
和 iv
,之后用户登录(/api/login
)的请求和响应都会加密。
前言
最新的测试中,经常遇到HTTP报文加密/加签传输的情况,这导致想要查看和修改明文报文很不方便。
之前应对这种情况我们有几种常见的办法解决,比如使用burpy插件、在Burp上下游使用mitmproxy进行代理等,但这些使用起来不太方便,并且偶尔遇到python和java间加密算法上的一些小差异,需要调试很久。
因此在想是否可以做一款Burp插件来解决这些问题,而且使用上要简单高效。在看了Burp新版接口 Montoya API
的介绍后,发现可以满足我们的需求,并且还有一些意想不到的发现,比如它可以满足 动态密钥
的场景。
原理
正常情况下,我们通过Burp代理进行渗透测试时,流量的流转情况如图所示。
而通过Burp的 Montoya API
我们可以分别做到 Request/Response 在 Client/Burp/Server 流转时实现自己对其的处理逻辑,像图中这样。
Burp Proxy
:Burp中的Proxy模块,即UI中看到的Proxy那样,负责代理以及流量记录等。Burp Http
:Burp中的Http模块,负责请求最终从Burp出去、响应刚到达Burp时的处理等。
而要实现我们的需求,很显然只需要在图中①,②,③,④进行不同的处理逻辑即可:
①:HTTP请求从客户端到达Burp时被触发。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文。
②:HTTP请求从Burp将要发送到Server时被触发。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。
③:HTTP请求从Server到达Burp时被触发。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文。
④:HTTP请求从Burp将要发送到Client时被触发。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client。
设计思路
逻辑成立,实现开始。
请求过滤
据过去遇到的情况,同一个系统可能出现部分流量加密的情况、甚至还有根据某个请求头的不同,不加密或者使用不同的加密方式。
想要支持这些情况,采用host白名单,url白名单的方案有其弊端,因此我引入了表达式的方式,你可以通过实现一个表达式,来过滤哪些请求需要/不需要处理,比如我们默认的表达式为:
!request.isStaticExtension() && request.host=='192.168.1.4'
表达式采用JS的语法,执行完成会返回一个
boolean
类型,用于判断是否过滤。
这条的意思是指请求不能是静态后缀,且host必须是192.168.1.4,在使用过程中用户可以根据自己需求修改,以应对各种特殊的场景。
核心实现
还是根据经验来,我想要的实现需要满足以下条件:
-
可以根据某个请求头的不同,使用不同的加密方式。
-
可以满足在加密加签同时存在、加密时算法组合的情况。
-
最好可以使用java中的加解密库,避免不同语言算法实现上的小差异。
调研了现在一些常见的实现思路,有的通过配置,但配置总会有不足;有的可以让用户自定义代码后在Repeater模块中右键使用,很灵活但无法做到对代理请求/响应自动解密。
综合考虑了上述方式的缺点,借鉴了其优点,提炼出自己的方案。
我将该功能命名为 HttpHook
,四个阶段分别为 hookRequestToBurp
,hookRequestToServer
, hookResponseToBurp
, hookResponseToClient
。他们分别接收请求或响应对象,由用户对其进行自定义处理。四个阶段分别有多种实现方式:
Python
:通过python代码实现四个函数,原理为将python编译并在jvm中运行。JS
:通过js代码实现四个函数,原理为将js编译并在jvm中运行。Java
:通过java代码实现四个函数,原理为动态加载。Grpc
:通过实现Grpc服务实现四个rpc接口,算是兜底方案。
这四种实现方式,可以分为两类:
Grpc
:你需要用其他语言实现Grpc Server,并自行通过三方库实现对应 Hook 接口
应有的功能。Code
:你需要用支持的方式编写对应语言的脚本,在脚本中组合、调用项目中的DataObjects和Util,实现对应 Hook 函数
应有的功能。
它们各有优缺点:
Grpc
:优点是跨语言能力强,运行兼容性强;缺点是学习成本稍高、依赖IO -> 可能存在性能问题、不同语言算法间可能存在兼容性问题,在动态密钥的情况下很难实现需求。Code
:优点是可以与JVM交互调用Java中的加解密库 -> 对Java来说没有算法兼容性的问题;缺点是需要熟悉项目自带的DataObjects和Utils,并且可能存在运行兼容性的问题。
上手难度
看了上边的实现,你发现居然还要我写代码,好麻烦。确实,不可否认,我们为了覆盖复杂的场景,采用了写代码这种适应性最强的方式。
因此我们采用尽量多而全的自带示例,来减少了上手难度。项目中涵盖了大量的示例,如AES-CBC
,AES-ECB
, AES-GCM
, RSA
, SM2
等常见算法(后续也会持续扩充),对于这些算法,可以做到开箱即用,至于更复杂的场景,你需要熟悉这些示例中的代码,灵活运用,从而解决问题。
效果演示
github地址:https://github.com/outlaws-bai/Galaxy
常规情况
这块看文章顶部的 gif
即可,更加直观,也比较简单,我们详细演示动态密钥情况下的使用。
动态密钥
示例编写
首先,我们写一个简单用于测试的客户端(index.html)和服务端(server.py),加密算法使用AES-CBC
。
index.html
:在用户打开页面时,请求 /api/getSecret
,获取加密使用的 key
和 iv
,之后用户登录(/api/login
)的请求和响应都会加密。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<script src="https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js"></script>
</head>
<body>
<h2>Login</h2>
<form id="loginForm">
<label for="username">Username:</label>
<input type="text" id="username" name="username"><br><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br><br>
<button type="button" onclick="login()">Login</button>
</form>
<div id="userInfo" style="margin-top: 20px;">
</div>
<script>
let secretKey = '';
let iv = '';
async function getSecret() {
try {
const response = await fetch('/api/getSecret');
const data = await response.json();
secretKey = data.key;
iv = data.iv;
} catch (error) {
console.error('Error fetching the secret:', error);
}
}
function encrypt(data, key, iv) {
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const ivUtf8 = CryptoJS.enc.Utf8.parse(iv);
const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), keyUtf8, { iv: ivUtf8, mode: CryptoJS.mode.CBC });
return encrypted.toString();
}
function decrypt(data, key, iv) {
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const ivUtf8 = CryptoJS.enc.Utf8.parse(iv);
const decrypted = CryptoJS.AES.decrypt(data, keyUtf8, { iv: ivUtf8, mode: CryptoJS.mode.CBC });
return JSON.parse(CryptoJS.enc.Utf8.stringify(decrypted));
}
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const data = {
username: username,
password: password
};
const encryptedData = encrypt(data, secretKey, iv);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: encryptedData })
});
const result = await response.json();
const decryptedResult = decrypt(result.data, secretKey, iv);
if (decryptedResult.success) {
displayUserInfo(decryptedResult.user);
} else {
alert('Login failed: ' + decryptedResult.message);
}
} catch (error) {
console.error('Error during login:', error);
}
}
function displayUserInfo(user) {
const userInfoDiv = document.getElementById('userInfo');
userInfoDiv.innerHTML = `
<h3>User Information</h3>
<p><strong>Username:</strong> ${user.username}</p>
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>Full Name:</strong> ${user.fullName}</p>
`;
}
window.onload = getSecret;
</script>
</body>
</html>
server.py
:共有三个接口,/
返回index.html
给到游览器渲染页面;/api/getSecret
获取AES-CBC
加解密过程中需要的key
和iv
;/api/login
获取客户端输入数据后加密、发送请求、最后将结果解密展示到游览器。
# pip install fastapi pycryptodome
import json
import base64
import string
import random
import uvicorn
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.responses import HTMLResponse, JSONResponse
app = FastAPI()
users_db = {
"testuser": {
"username": "testuser",
"password": "testpassword",
"email": "testuser@example.com",
"fullName": "Test User",
}
}
class LoginRequest(BaseModel):
data: str
def encrypt(data: dict, key: bytes, iv: bytes) -> str:
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_data = pad(json.dumps(data).encode("utf-8"), AES.block_size)
encrypted = cipher.encrypt(padded_data)
return base64.b64encode(encrypted).decode("utf-8")
def decrypt(data: str, key: bytes, iv: bytes) -> dict:
encrypted_data = base64.b64decode(data)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)
return json.loads(decrypted_data.decode("utf-8"))
def generate_string(length: int) -> str:
return "".join(
random.choice(string.digits + string.ascii_letters) for _ in range(length)
)
@app.get("/api/getSecret")
async def get_secret():
key = generate_string(32)
iv = generate_string(16)
app.state.secret_key = key
app.state.secret_iv = iv
return {"key": app.state.secret_key, "iv": app.state.secret_iv}
@app.post("/api/login")
async def login(request: LoginRequest):
key = app.state.secret_key.encode()
iv = app.state.secret_iv.encode()
login_data = decrypt(request.data, key, iv)
username = login_data.get("username")
password = login_data.get("password")
if username in users_db and users_db[username]["password"] == password: # type: ignore
user_info = users_db[username] # type: ignore
encrypted_response = encrypt({"success": True, "user": user_info}, key, iv)
return JSONResponse(content={"data": encrypted_response})
else:
encrypted_response = encrypt(
{"success": False, "message": "Invalid username or password"}, key, iv
)
return JSONResponse(content={"data": encrypted_response})
@app.get("/", response_class=HTMLResponse)
async def index():
with open("index.html", "r", encoding="utf-8") as file:
html_content = file.read()
return HTMLResponse(content=html_content)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
然后我们启动服务:python server.py
正常情况下,客户端首先从服务端加载key
和iv
在页面上输入数据登录时,请求和响应被加密了。
尝试解密
Burp安装Galaxy后,配置下方的java脚本并启动服务,来完成需求
import org.m2sec.core.utils.*;
import org.m2sec.core.models.*;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
public class Test {
private Logger log;
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private byte[] secret;
private byte[] iv;
private static final Map<String, Object> paramMap = new HashMap<>();
private static final String jsonKey = "data";
private final ThreadLocal<Boolean> flag = ThreadLocal.withInitial(() -> false);
public Test(Logger log) {
this.log = log;
}
public Request hookRequestToBurp(Request request) {
flag.set(false);
if (request.getPath().endsWith("/api/getSecret")) {
log.info("match update key & iv api.");
flag.set(true);
} else if (request.getMethod().equalsIgnoreCase("POST")) {
byte[] encryptedData = getData(request.getContent());
byte[] data = decrypt(encryptedData);
request.setContent(data);
} else {
log.info("request666: {}", request);
}
return request;
}
public Request hookRequestToServer(Request request) {
if (!flag.get()) {
byte[] data = request.getContent();
byte[] encryptedData = encrypt(data);
byte[] body = toData(encryptedData);
request.setContent(body);
}
return request;
}
public Response hookResponseToBurp(Response response) {
if (!flag.get()) {
byte[] encryptedData = getData(response.getContent());
byte[] data = decrypt(encryptedData);
response.setContent(data);
}
return response;
}
public Response hookResponseToClient(Response response) {
if (flag.get()) {
Map<?, ?> bodyMap = JsonUtil.jsonStrToMap(new String(response.getContent()));
String secret1 = ((String) bodyMap.get("key"));
String iv1 = ((String) bodyMap.get("iv"));
secret = secret1.getBytes();
iv = iv1.getBytes();
paramMap.put("iv", iv);
log.info("update key & iv: {}, {}", secret1, iv1);
} else {
byte[] data = response.getContent();
byte[] encryptedData = encrypt(data);
byte[] body = toData(encryptedData);
response.setContent(body);
}
return response;
}
public byte[] decrypt(byte[] content) {
return CryptoUtil.aesDecrypt(ALGORITHM, content, secret, paramMap);
}
public byte[] encrypt(byte[] content) {
return CryptoUtil.aesEncrypt(ALGORITHM, content, secret, paramMap);
}
public byte[] getData(byte[] content) {
return CodeUtil.b64decode((String) JsonUtil.jsonStrToMap(new String(content)).get(jsonKey));
}
public byte[] toData(byte[] content) {
HashMap<String, Object> jsonBody = new HashMap<>();
jsonBody.put(jsonKey, CodeUtil.b64encodeToString(content));
return JsonUtil.toJsonStr(jsonBody).getBytes();
}
}
再次刷新页面,触发动态密钥变换,然后输入账号和密码点击登录,就可以看到请求和响应都被自动解密了,之后对明文进行测试即可。
总结
总结下插件的特点吧。Galaxy
-
简单高效:用户不需要启动多余的本地服务,配置成功后可以自动对报文进行加解密。
-
上手容易:通用算法已有示例,能做到开箱即用。
-
灵活:可以使用Python、JS、Java、Grpc多种方式实现。
-
支持面广:如加密算法组合、自定义算法、动态密钥等均可以支持,
更多网络安全优质免费学习资料与干货教程加v
送渗透工具、技术文档、书籍,面试题、视频(基础到进阶。环境搭建,HTML,PHP,MySQL基础学习,信息收集,SQL注入,XSS,CSRF,暴力破解等等)、应急响应笔记、学习路线。
申明:本账号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法