前言
现如今越来越多的站点使用前端数据加密,从之前传统的base64、md5变向AES、RSA、或SM2/SM4(国产替代RSA)加密,有时候或干脆进行签名校验,这就需要我们逆向提取加密算法以及密钥,过程相对繁琐复杂,并且越来越多行业的内部系统使用了加密技术对HTTP的请求体加密, 时间戳, RequestId等防止进行数据包的抓包改包,这样一来增大了安全测试人员的测试难度,也使得很多不懂加密测试的人员望而却步
正文
从测试人员的角度出发,当我们拿到一个站点,在抓包时我们都希望碰到的是明文传输,因为简单且清晰明了,方便我们做测试,如下图
但是现如今这样的站点是少之又少,大多数都进行了加密处理,这就要求我们对JS逆向和前端调试有一定的了解,说起JS逆向,那都是老生常谈了,想找到JS中的加解密的方法,需要我们首先快速定位到加解密函数和明文点的位置,以下面这个站点为例,一个很常规很普通很常见的登录
在开始之前给大家推荐一款Google浏览器插件:v_jstools,能够一键监测JS中指定函数的调用,下载好之后,本地导入插件即可,在配置v_jstools的时候一定要注意不要全部勾选,根据实际情况选择需要挂钩的函数即可,否则信息流太大干扰我们测试
一切都配置好后,打开挂钩功能,并F12打开浏览器控制台,此时刷新页面,如果出现“inject start!”则表示插件生效了
当我们输入账号密码发送数据时,针对请求包,v_jstools帮我们找到了请求的明文点
既然定位到了请求的明文点,那就跟进对应的JS文件,并在该代码处(JSON.stringify)打上断点跟踪
当我们重新发送数据包,点击步过一次,就可以很容易的发现n变量就是我们提交的账号密码明文内容
继续点击步过,经过t.data=l(n)后,data内容为密文,并且通过对作用域中的请求包比较,发现t.data即为加密后的内容,那么l()函数即为加密函数
说干就干!我们跟进l()函数中看看,发现l函数即为加密函数,并且在附近也发现了解密函数,其中t参数为原始的内容,f参数为密钥,h为密码
再返回看,我们也可以发现requestID算法,时间戳算法和签名算法
假设现在我们知道了正确的账号密码:test/123,我们如何修改数据包,这里有三种解决方案,第一种方案就是直接在明文点修改,进入到调试中,走到加密前的一步,直接在作用域中修改
不过这种方式可能会引文加密方式和一些签名的限制,可能修改无效,有很大的局限性,赌一赌还是可以的。第二种方式需要用到JS-forward,大致的原理就是在明文点处插入一段JS代码,这段代码会通过AJAX请求发送给bp,bp做拦截处理并修改内容后,会将数值返回到原始变量中,首先确定明文变量名,通过调试,可以判定明文变量就是t.data
随后启动JS-forward,输入变量名、数据类型、请求标识(REQUEST),输入$end 结束后,会开启2个端口进行监听,分别是38080和28080,并且会生成一段js代码,这段代码就是我们即将插入到明文点的代码
在插入修改JS代码之前,要注意的是,我们需要新创一个文件夹,然后F12,找到对应的源代码进行替换,否则是无法直接进行修改JS中的代码,成功的话,右下角会有一个紫色圆的标识
然后将刚才生成的JS代码复制到明文点函数的第一行,最后保存
关闭F12,刷新页面再次发包时bp即可接收到明文信息
第三种方式是通过JSrpc注入,主动主动发包加解密,需要借助工具JS-RPC,这款工具的工作原理就是在浏览器控制台中执行一段代码,通过websocket与本地的python服务端进行连接,当我们想要执行代码,只需要通过RPC调用控制台中的函数即可,打开客户端,然后在控制台中输入JSrpc的注入代码,代码如下
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| function Hlclient(wsURL) { this.wsURL = wsURL; this.handlers = { _execjs: function (resolve, param) { var res = eval(param) if (!res) { resolve("没有返回值") } else { resolve(res) }
} }; this.socket = undefined; if (!wsURL) { throw new Error('wsURL can not be empty!!') } this.connect() }
Hlclient.prototype.connect = function () { console.log('begin of connect to wsURL: ' + this.wsURL); var _this = this; try { this.socket = new WebSocket(this.wsURL); this.socket.onmessage = function (e) { _this.handlerRequest(e.data) } } catch (e) { console.log("connection failed,reconnect after 10s"); setTimeout(function () { _this.connect() }, 10000) } this.socket.onclose = function () { console.log('rpc已关闭'); setTimeout(function () { _this.connect() }, 10000) } this.socket.addEventListener('open', (event) => { console.log("rpc连接成功"); }); this.socket.addEventListener('error', (event) => { console.error('rpc连接出错,请检查是否打开服务端:', event.error); });
}; Hlclient.prototype.send = function (msg) { this.socket.send(msg) }
Hlclient.prototype.regAction = function (func_name, func) { if (typeof func_name !== 'string') { throw new Error("an func_name must be string"); } if (typeof func !== 'function') { throw new Error("must be function"); } console.log("register func_name: " + func_name); this.handlers[func_name] = func; return true
}
Hlclient.prototype.handlerRequest = function (requestJson) { var _this = this; try { var result = JSON.parse(requestJson) } catch (error) { console.log("catch error", requestJson); result = transjson(requestJson) } if (!result['action']) { this.sendResult('', 'need request param {action}'); return } var action = result["action"] var theHandler = this.handlers[action]; if (!theHandler) { this.sendResult(action, 'action not found'); return } try { if (!result["param"]) { theHandler(function (response) { _this.sendResult(action, response); }) return } var param = result["param"] try { param = JSON.parse(param) } catch (e) {} theHandler(function (response) { _this.sendResult(action, response); }, param)
} catch (e) { console.log("error: " + e); _this.sendResult(action, e); } }
Hlclient.prototype.sendResult = function (action, e) { if (typeof e === 'object' && e !== null) { try { e = JSON.stringify(e) } catch (v) { console.log(v) } } this.send(action + atob("aGxeX14") + e); }
function transjson(formdata) { var regex = /"action":(?<actionName>.*?),/g var actionName = regex.exec(formdata).groups.actionName stringfystring = formdata.match(/{..data..:.*..\w+..:\s...*?..}/g).pop() stringfystring = stringfystring.replace(/\\"/g, '"') paramstring = JSON.parse(stringfystring) tens = `{"action":` + actionName + `,"param":{}}` tjson = JSON.parse(tens) tjson.param = paramstring return tjson }
|
然后进行连接
1
| var liuty = new Hlclient("ws://127.0.0.1:12080/ws?group=liuty111");
|
首先还是调试到加密那里,前面我们也已经知道了加密函数为l(),然后在控制台中输入window.enc = l, 控制台会显示当前函数信息,并注册保存非形参的参数,那我们可以主动调用enc()函数, 随便做个测试,就123吧,查看是否有效
可以看到是没问题的,那我们就向JSrpc中注册这些函数
1 2 3 4
| demo.regAction("enc", function (resolve, param) { var res = enc(String(param)); resolve(res); })
|
显示“true”则代表成功,那我们就可以通过mitmproxy将原始请求发送到JS-RPC中进行加密后修改原始数据包内容, 再进行发包(mitmproxy是一款代理工具, 想bp一样可以进行拦截,改包等操作)
再分析一遍JS,加上前面的分析和理解,不难做出判断:r很明显就是时间戳,n是将数据请求经过v()函数处理后再进行JSON编码,i是使用p函数生成requestId,s是使用MD5()函数并通过n+i+r字符串拼接的方式生成HASH,最后对变量n使用l()函数进行加密,知道这些之后,我们开始打上断点进行调试,首先记录这些函数
1 2 3 4 5 6 7 8 9 10
| 时间戳: window.time = Date.parse requestId: window.id = p v函数: window.v1 = v 签名: window.m = a.a.MD5 加密: window.enc = l
|
随后进行注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| demo.regAction("req", function (resolve,param) { let timestamp = time(new Date()); let requestid = id(); let v_data = JSON.stringify(v1(param)); let sign = m(v_data + requestid + timestamp).toString(); let encstr = enc(v_data);
let res = { "timestamp":timestamp, "requestid":requestid, "encstr":encstr, "sign":sign }; resolve(res); })
|
这样一来,我们就可以一次性获取所有请求的需求了,对于mitmproxy我就不多说了,现在我们构建mitmproxy的脚本,通过前面的分析我们的代码逻辑为: 提取原始请求体后, 向请求头中添加requestId, timestamp, sign字段并且替换原始请求体为加密后的内容
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
| import json import time import hashlib import uuid from mitmproxy import http import requests import requests
def request(flow: http.HTTPFlow) -> None: if flow.request.pretty_url.startswith("http://测试IP"): original_body = flow.request.content.decode('utf-8') data = {"group": "zzz", "action": "req", "param": original_body} res = requests.post("http://127.0.0.1:12080/go",data=data) res_json = json.loads(res.text)["data"] data_json = json.loads(res_json) print(data_json) encrypted_body = data_json["encstr"]
flow.request.text = encrypted_body
request_id = data_json["requestid"] timestamp = data_json["timestamp"] sign = data_json["sign"]
flow.request.headers["requestId"] = request_id flow.request.headers["timestamp"] = str(timestamp) flow.request.headers["sign"] = sign
|
然后运行mitmproxy时加载这个脚本并开启一个监听端口即可,但别忘了bp的upstream也要设为mitmproxy一样的的监听端口
下面做个测试,浏览器做好代理后,当bp发送明文数据包时, 在经过mitm处理后,会自动加密,okkkkk,success! 解密同理
好了,就分享到这了
大考就要开始了,该收拾行李去了