写作背景
在一次实战渗透测试中,使用burp抓包遇到了js加密,通过js逆向找到了加密算法,最终利用mitmdump联动burpsuite实现了自动化加解密
遇到的问题
在得到授权的前提下,心情愉悦地打开burp抓包,进行日常地抓包改包测试,但是测试时发现请求体和响应体均被加密了,且密文都是d1,d2,d3格式,这就难受了≧ ﹏ ≦
js逆向(大佬可直接越过此部分看Mitmdump)
遇到上面地问题,首先当然是得定位加密函数了,F12打开浏览器控制台,直接搜索:"d1:"
可以看到此处可以获得d1,d2,d3,其中d1是由a的值获取的,d2是由s的值获取的,d3是由u的值获取的,那么我们直接下断点在a的生成处,重新刷新网页,在a处断下,F9单步步入
先是来到v["f"]的生成处,再单步两次回到之前的界面,再单步一次即可进入Object(v["f"])函数内
可以看出来是使用了cbc模式,猜测使用了AES加密,此时我们在右侧作用域查看一下变量值:
可以推测e为key值,i为iv值(e和i的值为前面n和o的值,后面会讲n和o怎么生成的),t为加密前的请求体值,但是此处有一个问题:iv值通常应该是16位才对啊,看来此处不能直接调用aes解密了(没学过密码学只能偷偷哭泣了/(ㄒoㄒ)/),看来只能硬抠代码了
这里d1的值已经弄明白了,我们再去看看d2和d3的值是怎么生成的,此处还是刷新网页,重新断下:
可以看到d2也就是s的值是由Object(v["c"])函数生成的,传入两个参数,生成d2时传入n和r,生成d3时传入o和r,而r的值在上面可以看出来是rsa值,此时看一下这个值是啥:
已经明确了rsa的值,此处推测是服务端的公钥,那么再来看n和o的值,可以看出来是调用y(32)函数生成的,跟进查看:
简简单单的根据"0123456789ABCDEFabcdef"生成的32位随机值,那么到这里,加密过程就已经弄清楚了:
d1是由n和o作为key和iv值,对原始报文使用aes加密生成的,d2由服务端的公钥和随机值n生成的,d3由服务端的公钥和随机值o生成的
那么我们前面说到了响应体也是有d1,d2,d3,那么再去看解密流程,同样的思路分析下来可以知道:
响应体中的d2可以使用客户端私钥解密出n,d3可以使用客户端私钥解密出o,d1可以使用n和o解密出原始报文(而私钥的获取方式同上公钥获取一样,下断点即可查看,解密函数就在加密函数下方)
咳咳,这个加解密,属实厉害,接下来就是抠js代码了
抠js代码
这个过程属实痛苦,js挨个抠的话,还好,但是我们仔细观察一下这个网站,发现是webpack的,vue开发的网站,硬抠肯定得掉不少头发,此处讲一个快捷的webpack通用抠代码的方法:
首先抠webpack入口,通常会在html主页面内嵌入,此处讲一个快速定位入口函数的方法,在索引页面内搜索call,挨个查看一下,会有如下类似代码:
将函数抠下来,放在js文件里,定义成如下格式:
var test_module;
(function(e) {
var u={};
function f(c) {
if (u[c])
return u[c].exports;
var n = u[c] = {
i: c,
l: !1,
exports: {}
};
// console.log(c)
return e[c].call(n.exports, n, n.exports, f),
n.l = !0,
n.exports
}
test_module=f;
})({
//此处留下贴之后抠的代码
})
在开头写个test_module是为了后面好调用,接下来就是抠加密和解密函数,先抠aes的加密函数,往上找找到aes加密函数的最前面
往上找到如图所示位置:
往下找到如图所示位置
"4b2a": function(t, e, i) {
"use strict";
i.d(e, "a", (function() {
return n
}
)),
i.d(e, "c", (function() {
return o
}
)),
i.d(e, "b", (function() {
return a
}
)),
i.d(e, "d", (function() {
return u
}
)),
i.d(e, "f", (function() {
return c
}
)),
i.d(e, "e", (function() {
return f
}
));
i("d3b7");
var r = i("7c74").sm2;
function n() {
var t = r.generateKeyPairHex();
return new Promise((function(e) {
t && e(t)
}
))
}
function o(t, e) {
var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 1
, n = "04" + r.doEncrypt(t, e, i);
return n
}
function a(t, e) {
var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 1
, n = r.doDecrypt(t.substring(2), e, i);
return n
}
var s = i("7c74").sm3;
function u(t) {
var e = s(t);
return e
}
var h = i("7c74").sm4;
function c(t, e, i) {
var r = h.encrypt(t, e, {
mode: "cbc",
iv: i
});
return r
}
function f(t, e, i) {
var r = h.decrypt(t, e, {
mode: "cbc",
iv: i
});
return r
}
},
抠下来代码如上,放置在之前抠的入口函数下的后面的中括号里,接下来就是挨个抠,js逆向俗称:缺啥补啥。为了方便,查看缺啥补啥,我们在入口处修改如下图所示:
这样做的好处是会提示我们缺啥函数,再去js文件里抠出来,抠的过程中会遇到:i("d3b7");如果是像上面这样调用函数且没有声明变量去获取返回值,可以把(i("d3b7"))注释掉,不然还得挨个抠下来,会很麻烦,而且也用不上,遇到后面这个情况得保留(例:h=i("d3b7")),接下来的过程便不多说了,漫长的抠代码过程
这部分算是讲完了,那么最后贴一个调用方法(var test=test_module("7a74");然后就可以用test去调用其方法了):
function test_enc(t, e, i) {
var test=test_module("7a74");
var r = test.encrypt(t, e, {
mode: "cbc",
iv: i
});
return r
}
mitmdump中间人代理
抠完代码后,我们直接使用mitmdump对burp做上下游代理,这样我们可以在mitmdump代理层去修改数据包,然后在burp层看到的就是我们日常渗透未加密的数据,是真的很方便
该方法适用于如下三个场景(其中app和小程序需能看到加解密或防篡改的源代码):
web网页端做了防篡改以及加解密
app端做了防篡改以及加解密
小程序端做了防篡改以及加解密
这里往下叙述共分为三个模块:
第一个模块为编写使用mitmdump的脚本
第二个模块为响应体解密,此处使用二级代理即可(即burp上游代理)
第三个模块为请求体以及响应体加解密,此处需使用三级代理(即burp上下游代理)
示例脚本
编写好js的加解密脚本后,使用如下模板(修改加解密函数以及request和response函数为适应自己的加解密代码,并命名为test.py):
import execjs
from mitmproxy import flowfilter
from mitmproxy.http import HTTPFlow
#解决js代码中中文编码错误
import subprocess
import json
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding='utf-8')
with open("test.js","r",encoding="utf-8") as f:
js_code=f.read()
f.close()
ctx=execjs.compile(js_code)
#请先修改公钥
publickey=""
#请先修改私钥
privatekey=""
def enc(data):
[d1,d2,d3]=ctx.call("req_enc",data,publickey)
return [d1,d2,d3]
def dec(d1,d2,d3):
result=ctx.call("res_dec",d1,d2,d3,privatekey)
return result
class FilterFlow:
def request(self, flow):
if flow.request.url.startswith("https://x.x.x.x/"):
req=flow.request.get_text()
if '"d1":' in req:
return
print("加密数据前:".format(req))
data=enc(req)
d1=data[0]
d2=data[1]
d3=data[2]
json_data={"d1":d1,"d2":d2,"d3":d3}
result=json.dumps(json_data)
print("加密数据后:".format(result))
flow.request.set_text(result)
def response(self, flow:HTTPFlow):
# print(flow.response.get_text())
if flow.request.url.startswith("https://x.x.x.x/"):
resp=flow.response.get_text()
data=json.loads(resp)
d1=data["d1"]
d2=data["d2"]
d3=data["d3"]
result=dec(d1,d2,d3)
flow.response.set_text(result)
addons = [
FilterFlow(),
]
响应体解密
此处仅需设置burp上游代理,方法如下:
mitmdump -p 9090 -s .\test.py --ssl-insecure
burp端设置:User Option => Upstream Proxy Servers => Add
请求体响应体加解密
此处需设置burp上下游代理,burp上游代理开启同上,此处只讲开启burp下游代理,即开启mitmdump的上游代理(需新开一个终端):
mitmdump -p 7070 -s .\test.py --mode upstream:https://127.0.0.1:8080 --ssl-insecure
这样就算配置完成了,在burp中已经可以正常测试了,如果需要跑sqlmap等其它工具,请将流量代理至burp即可,如:
python sqlmap.py -u "https://x.x.x.x/?id=1" --batch --proxy=http://127.0.0.1:8080
最后贴图纪念一下成果,已经可以正常加解密进行渗透测试了: