长亭百川云 - 文章详情

论报文加密加签场景下如何高效的进行渗透测试

越关山丶

58

2024-08-01

点击上方[蓝字],关注我们

**建议大家把公众号“Z2O安全攻防”设为星标,否则可能就看不到啦!**因为公众号现在只对常读和星标的公众号才能展示大图推送。操作方法:点击右上角的【...】,然后点击【设为星标】即可。

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者及本公众号团队不为此承担任何责任。

文章正文

前言

最新的测试中,经常遇到HTTP报文加密/加签传输的情况,这导致想要查看和修改明文报文很不方便。

之前应对这种情况我们有几种常见的办法解决,比如使用burpy插件、在Burp上下游使用mitmproxy进行代理等,但这些使用起来不太方便,并且偶尔遇到python和java间加密算法上的一些小差异,需要调试很久。

因此在想是否可以做一款Burp插件来解决这些问题,而且使用上要简单高效。在看了Burp新版接口 Montoya API
 的介绍后,发现可以满足我们的需求,并且还有一些意想不到的发现,比如它可以满足 动态密钥
 的场景。

详细介绍前,先放一张实现后的gif:

原理

正常情况下,我们通过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,在使用过程中用户可以根据自己需求修改,以应对各种特殊的场景。

核心实现

还是根据经验来,我想要的实现需要满足以下条件:

  1. 1. 可以根据某个请求头的不同,使用不同的加密方式。

  2. 2. 可以满足在加密加签同时存在、加密时算法组合的情况。

  3. 3. 最好可以使用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>  
<htmllang="en">  
<head>  
<metacharset="UTF-8">  
<metaname="viewport"content="width=device-width, initial-scale=1.0">  
<title>Login</title>  
<scriptsrc="https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js"></script>  
</head>  
<body>  
<h2>Login</h2>  
<formid="loginForm">  
<labelfor="username">Username:</label>  
<inputtype="text"id="username"name="username"><br><br>  
<labelfor="password">Password:</label>  
<inputtype="password"id="password"name="password"><br><br>  
<buttontype="button"onclick="login()">Login</button>  
</form>  
<divid="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>UserInformation</h3>  
<p><strong>Username:</strong> ${user.username}</p>  
<p><strong>Email:</strong> ${user.email}</p>  
<p><strong>FullName:</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  
importstring  
import random  
import uvicorn  
fromCrypto.Cipherimport AES  
fromCrypto.Util.Paddingimport pad, unpad  
from fastapi importFastAPI  
from pydantic importBaseModel  
from fastapi.responses importHTMLResponse,JSONResponse  
  
app =FastAPI()  
  
users_db ={  
"testuser":{  
"username":"testuser",  
"password":"testpassword",  
"email":"testuser@example.com",  
"fullName":"Test User",  
}  
}  
  
  
classLoginRequest(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)  
returnJSONResponse(content={"data": encrypted_response})  
else:  
        encrypted_response = encrypt(  
{"success":False,"message":"Invalid username or password"}, key, iv  
)  
returnJSONResponse(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()  
returnHTMLResponse(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;  
  
publicclassTest{  
  
privateLogger log;  
privatestaticfinalString ALGORITHM ="AES/CBC/PKCS5Padding";  
privatebyte[] secret;  
privatebyte[] iv;  
privatestaticfinalMap<String,Object> paramMap =newHashMap<>();  
privatestaticfinalString jsonKey ="data";  
  
privatefinalThreadLocal<Boolean> flag =ThreadLocal.withInitial(()->false);  
  
publicTest(Logger log){  
this.log = log;  
}  
  
publicRequest hookRequestToBurp(Request request){  
        flag.set(false);  
if(request.getPath().endsWith("/api/getSecret")){  
            log.info("match update key & iv api.");  
            flag.set(true);  
}elseif(request.getMethod().equalsIgnoreCase("POST")){  
byte[] encryptedData = getData(request.getContent());  
byte[] data = decrypt(encryptedData);  
            request.setContent(data);  
}else{  
            log.info("request666: {}", request);  
}  
return request;  
}  
  
publicRequest hookRequestToServer(Request request){  
if(!flag.get()){  
byte[] data = request.getContent();  
byte[] encryptedData = encrypt(data);  
byte[] body = toData(encryptedData);  
            request.setContent(body);  
}  
return request;  
}  
  
publicResponse hookResponseToBurp(Response response){  
if(!flag.get()){  
byte[] encryptedData = getData(response.getContent());  
byte[] data = decrypt(encryptedData);  
            response.setContent(data);  
}  
return response;  
}  
  
publicResponse hookResponseToClient(Response response){  
if(flag.get()){  
Map<?,?> bodyMap =JsonUtil.jsonStrToMap(newString(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;  
}  
  
publicbyte[] decrypt(byte[] content){  
returnCryptoUtil.aesDecrypt(ALGORITHM, content, secret, paramMap);  
}  
  
publicbyte[] encrypt(byte[] content){  
returnCryptoUtil.aesEncrypt(ALGORITHM, content, secret, paramMap);  
}  
  
publicbyte[] getData(byte[] content){  
returnCodeUtil.b64decode((String)JsonUtil.jsonStrToMap(newString(content)).get(jsonKey));  
}  
  
publicbyte[] toData(byte[] content){  
HashMap<String,Object> jsonBody =newHashMap<>();  
        jsonBody.put(jsonKey,CodeUtil.b64encodeToString(content));  
returnJsonUtil.toJsonStr(jsonBody).getBytes();  
}  
}

再次刷新页面,触发动态密钥变换,然后输入账号和密码点击登录,就可以看到请求和响应都被自动解密了,之后对明文进行测试即可。

总结

总结下插件的特点吧。Galaxy

  1. 1. 简单高效:用户不需要启动多余的本地服务,配置成功后可以自动对报文进行加解密。

  2. 2. 上手容易:通用算法已有示例,能做到开箱即用。

  3. 3. 灵活:可以使用Python、JS、Java、Grpc多种方式实现。

  4. 4. 支持面广:如加密算法组合、自定义算法、动态密钥等均可以支持。

技术交流

知识星球

欢迎加入知识星球**,星球**致力于红蓝对抗,实战攻防,星球不定时更新内外网攻防渗透技巧,以及最新学习研究成果等。常态化更新最新安全动态。针对网络安全成员的普遍水平,为星友提供了教程、工具、POC&EXP以及各种学习笔记等等。****

交流群

关注公众号回复“加群”,添加Z2OBot好友,自动拉你加入**Z2O安全攻防交流群(微信群)**分享更多好东西。

关注我们

关注福利:

回复“app" 获取  app渗透和app抓包教程

回复“渗透字典" 获取 针对一些字典重新划分处理,收集了几个密码管理字典生成器用来扩展更多字典的仓库。

回复“漏洞库" 获取 最新漏洞POC库(1.2W+)****

回复“资料" 获取 网络安全、渗透测试相关资料文档****

点个【 在看 】,你最好看

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2