原创投稿,作者:white-beer,无限debugger bypass。
无限debugger bypass已经是老生常谈的问题了,原理其实就是hook setInterval&eval&setTimeout&constructor 去解决常见无限debugger的几种形式(setInterval、setTimeout、原型链)。这里就不再赘述了
参考文章:https://mp.weixin.qq.com/s/hpJ3qSaYI4yFJU0LWsm23A
个人推荐上文中大佬的万能脚本。
(function() { // 立即执行函数,创建独立作用域
// 启用严格模式,防止一些不规范的代码写法 ;
// 调试配置对象
//按0开启日志,默认关闭
const DEBUG = {
enable: 1, // 控制是否输出日志信息的开关
deb: 1 // 控制是否在关键位置设置断点的开关
};
//作者:Dexter
//公众号:我不是蜘蛛
// 定义日志输出函数,根据 DEBUG.enable 决定是否输出日志
const log = function(...args) {
if (DEBUG.enable==0) {
console.log(...args);
}
};
// 保存原始的 setInterval 函数
var originalSetInterval = window.setInterval;
// 用新的函数替换原始的 setInterval
window.setInterval = function(callback, delay) {
// 获取除了 callback 和 delay 的其他额外参数
var args = Array.prototype.slice.call(arguments, 2);
// 如果 callback 是字符串,则删除其中的 debugger 语句
if (typeof callback === 'string') {
callback = callback.replace(/debugger;/g, '');
} else if (typeof callback === 'function') {
// 如果 callback 是函数,替换包含 debugger 的回调
var originalCallback = callback;
callback = function() {
// 获取原始回调函数的源码
var callbackSource = originalCallback.toString();
// 如果源码包含 debugger,将其删除并创建新的函数
if (callbackSource.indexOf('debugger') !== -1) {
callbackSource = callbackSource.replace(/debugger;/g, '');
originalCallback = new Function('return ' + callbackSource)();
}
// 调用修改后的回调函数,并传入参数
originalCallback.apply(this, arguments);
};
}
// 调用原始的 setInterval 函数,并传入修改后的 callback、delay 和其他参数
return originalSetInterval.apply(window, [callback, delay].concat(args));
}
//============ toString 相关防护 ============
var temp_eval = eval; // 保存原始的 eval 函数
var temp_toString = Function.prototype.toString; // 保存原始的 toString 方法
// 改进toString方法的处理
Function.prototype.toString = function () {
if (this === eval) {
return 'function eval() { [native code] }';
} else if (this === Function) {
return 'function Function() { [native code] }';
} else if (this === Function.prototype.toString) {
return 'function toString() { [native code] }';
} else if(this===window.setInterval){
return 'function setInterval() { [native code] }';
}
return temp_toString.apply(this, arguments);
}
//============ eval 相关hook ============
window.eval = function () { // 重写全局 eval 函数
const stackTrace = new Error().stack; // 获取调用栈
const callLocation = stackTrace; // 保存调用位置
log(callLocation); // 输出调用信息
if (DEBUG.deb==0) { // 根据配置决定是否断点
debugger;
}
log("=============== eval end ===============");
// 处理传入的字符串参数,移除 debugger 语句
if (typeof arguments[0] == "string") {
var temp_length = arguments[0].match(/debugger/g);
if (temp_length != null) {
temp_length = temp_length.length;
var reg = /debugger/;
while (temp_length) {
arguments[0] = arguments[0].replace(reg, "");
temp_length--;
}
}
}
return temp_eval(...arguments); // 使用原始 eval 执行处理后的代码
}
//============ Function 相关hook ============
var _debugger = Function; // 保存原始 Function 构造函数
Function = function () { // 重写 Function 构造函数
const stackTrace = new Error().stack; // 获取调用栈
const callLocation = stackTrace; // 保存调用位置
log(callLocation); // 输出调用信息
if (DEBUG.deb==0) { // 根据配置决定是否断点
debugger;
}
log("=============== Function end ===============");
// 处理所有参数中的 debugger 语句
var reg = /debugger/;
for (var i = 0; i < arguments.length; i++) {
if (typeof arguments[i] == "string") {
var temp_length = arguments[i].match(/debugger/g);
if (temp_length != null) {
temp_length = temp_length.length;
while (temp_length) {
arguments[i] = arguments[i].replace(reg, "");
temp_length--;
}
}
}
}
return _debugger(...arguments); // 使用原始 Function 构造函数创建函数
}
// 保持原型链的完整性
Function.prototype = _debugger.prototype;
// 重写 Function 构造函数的 constructor
Function.prototype.constructor = function () {
const stackTrace = new Error().stack; // 获取调用栈
const callLocation = stackTrace; // 保存调用位置
log(callLocation); // 输出调用信息
if (DEBUG.deb==0) { // 根据配置决定是否断点
debugger;
}
log("=============== Function constructor end ===============");
// 处理所有参数中的 debugger 语句
var reg = /debugger/;
for (var i = 0; i < arguments.length; i++) {
if (typeof arguments[i] == "string") {
var temp_length = arguments[i].match(/debugger/g);
if (temp_length != null) {
temp_length = temp_length.length;
while (temp_length) {
arguments[i] = arguments[i].replace(reg, "");
temp_length--;
}
}
}
}
return _debugger(...arguments); // 使用原始 Function 构造函数
}
// 确保构造函数的原型链正确
Function.prototype.constructor.prototype = Function.prototype;
})();
首先需要找到无限debugger 触发的入口,以瑞数waf举例(其他的没啥意思)
一般来说会有个这样的文件夹,这个文件就是无限debugger 的触发入口。
或者也可以通过在堆栈中定位到入口位置。
找到位置后在其开头打上断点,刷新界面,此时就会在无限debugger 逻辑进入内存之前断住了。紧接着在控制台输入上面的万能js脚本
然后放开断点,此时无限debugger就已成功绕过。
定位加密方式
首先想到的就是全局搜encrypt/encode,但是这个站存在js混淆因此难以直接定位到,此时可以通过一些特定参数进行定位。一些混淆的不狠的,用火狐浏览器能返混淆出来一部分。
通过抓包定位到加密内容放在cipherH5参数中。
因为一般混淆不会混淆参数,只会混淆函数名称之类的。全局搜这个参数,此时就能定位到加密函数了。
DEBUG跟入即可看到key和iv值(CryptoJS的WordArray格式)。
反混淆
将其反混淆后能看到源码为:因此确定其为aes-cbc-PKCS7加密方式。
this['Encrypt'] = function(input) {
var helper = _0x251c08; // 辅助函数
var parsedInput = CryptoJS['enc']['Utf8']['parse'](input); // 将输入字符串解析为 CryptoJS 的 WordArray
var encrypted = CryptoJS['AES']['encrypt'](parsedInput, this['key'], { // 使用 AES 加密
'iv': this['aesIv'], // 初始化向量
'mode': CryptoJS['mode']['CBC'], // 加密模式为 CBC
'padding': CryptoJS['pad']['Pkcs7'] // 填充模式为 PKCS7
});
return encrypted['toString'](); // 返回加密后的字符串
};
接下来将CryptoJS的WordArray格式的key和iv还原即可,iv为例。
// 假设 aesIv 是从控制台获取的数组
var aesIv = [
943208504,
926365495,
909522486,
892679220
];
// 将数组转换为 CryptoJS 的 WordArray
var ivWordArray = CryptoJS.lib.WordArray.create(aesIv);
// 将 WordArray 转换为 16 字节的十六进制字符串
var ivHex = ivWordArray.toString(CryptoJS.enc.Hex);
console.log('IV (Hex):', ivHex);
// 将 WordArray 转换为 Base64 字符串
var ivBase64 = ivWordArray.toString(CryptoJS.enc.Base64);
console.log('IV (Base64):', ivBase64);
解密成功:
瑞数waf全混淆定位加解密函数
hook Json Parse & hook Json stringify
其实无论是什么样的js,在其发送加密数据之前肯定是需要进行json序列化的。生命周期:
明文数据 -> json.stringify 序列化 -> 调用加密函数
同理在其解密数据之后肯定需要json反序列化
调用解密函数 -> json.Parse 反序列化 -> 明文数据
那么其实就意味着我们可以通过hook stringify&Parse 这两个函数,在其前后堆栈中找到加解密函数。这样无论他是混淆还是隐式调用都能找到加密函数了。这里以某金融网站为例:
无限debugger的过程就不赘述了上文中已经提及。
以加密为例,首先在控制台输入hook JSON.stringify的
let _JSONStringify = JSON.stringify;
JSON.stringify = function (value, replacer, space) {
console.log('JSON.stringify 被调用了!');
console.log('待序列化内容:', value);
debugger;
// 调用原始方法执行序列化
const result = _JSONStringify.call(this, value, replacer, space);
console.log('序列化结果:', result);
// 如果需要,修改最终结果(这里可以自定义逻辑)
// return result + 'n/* 自定义注释 */';
// 返回序列化之后的结果
return result;
};
然后点击功能点,此时hook到了函数。一步步,步过。
此时即可看到动态调用的加密函数
在该处打上断点,步入后将其提升为全局函数即可。(可能需要步入很多层)
将函数提升为全局
前言:由于金融、证券等客户的Web站大概率存在全流量加密并且加密方式不是简单的AES-CBC/ECB,大多数情况都是国密SM4或自定义的加密算法。因此下文用来解决该问题。
工具安装&运行(docker)
docker pull registry.cn-beijing.aliyuncs.com/iinti/common:sekiro-all-in-one-latest;
docker run -d -p 5612:5612 -v ~/sekiro-mysql-data:/var/lib/mysql --name sekiro-all-in-one registry.cn-beijing.aliyuncs.com/iinti/common:sekiro-all-in-one-latest
定位加密方法
首先先定位目标站前端界面中的加密方法,这个方法有很多:抓个包定位包中的特殊参数、全局搜encrypt/encode .......
工具配置
在debug到对应加密函数的情况下,将该函数扩展到全局范围。在控制台输入:
window.loginDecrypt=this.encrypt_ecb
并进行验证,如图即使成功。
启动docker工具,并在控制台输入:其中wss协议对应https,ws对应http(根据站是啥协议定),group可以是任意的,registerAction注册的action名称也是任意的,后续的根据具体加密逻辑实现。
请注意,如果目标网站是https,且demo无法正确连接,请下载证书并安装到你的系统中点击这里 pem格式 或者 crt格式下载证书,并跟随系统引导安装根证书
function SekiroClient(e){if(this.wsURL=e,this.handlers={},this.socket={},!e)throw new Error("wsURL can not be empty!!");this.webSocketFactory=this.resolveWebSocketFactory(),this.connect()}SekiroClient.prototype.resolveWebSocketFactory=function(){if("object"==typeof window){var e=window.WebSocket?window.WebSocket:window.MozWebSocket;return function(o){function t(o){this.mSocket=new e(o)}return t.prototype.close=function(){this.mSocket.close()},t.prototype.onmessage=function(e){this.mSocket.onmessage=e},t.prototype.onopen=function(e){this.mSocket.onopen=e},t.prototype.onclose=function(e){this.mSocket.onclose=e},t.prototype.send=function(e){this.mSocket.send(e)},new t(o)}}if("object"==typeof weex)try{console.log("test webSocket for weex");var o=weex.requireModule("webSocket");return console.log("find webSocket for weex:"+o),function(e){try{o.close()}catch(e){}return o.WebSocket(e,""),o}}catch(e){console.log(e)}if("object"==typeof WebSocket)return function(o){return new e(o)};throw new Error("the js environment do not support websocket")},SekiroClient.prototype.connect=function(){console.log("sekiro: begin of connect to wsURL: "+this.wsURL);var e=this;try{this.socket=this.webSocketFactory(this.wsURL)}catch(o){return console.log("sekiro: create connection failed,reconnect after 2s:"+o),void setTimeout(function(){e.connect()},2e3)}this.socket.onmessage(function(o){e.handleSekiroRequest(o.data)}),this.socket.onopen(function(e){console.log("sekiro: open a sekiro client connection")}),this.socket.onclose(function(o){console.log("sekiro: disconnected ,reconnection after 2s"),setTimeout(function(){e.connect()},2e3)})},SekiroClient.prototype.handleSekiroRequest=function(e){console.log("receive sekiro request: "+e);var o=JSON.parse(e),t=o.sekiro_seq;if(o.action){var n=o.action;if(this.handlers[n]){var s=this.handlers[n],i=this;try{s(o,function(e){try{i.sendSuccess(t,e)}catch(e){i.sendFailed(t,"e:"+e)}},function(e){i.sendFailed(t,e)})}catch(e){console.log("error: "+e),i.sendFailed(t,":"+e)}}else this.sendFailed(t,"no action handler: "+n+" defined")}else this.sendFailed(t,"need request param {action}")},SekiroClient.prototype.sendSuccess=function(e,o){var t;if("string"==typeof o)try{t=JSON.parse(o)}catch(e){(t={}).data=o}else"object"==typeof o?t=o:(t={}).data=o;(Array.isArray(t)||"string"==typeof t)&&(t={data:t,code:0}),t.code?t.code=0:(t.status,t.status=0),t.sekiro_seq=e;var n=JSON.stringify(t);console.log("response :"+n),this.socket.send(n)},SekiroClient.prototype.sendFailed=function(e,o){"string"!=typeof o&&(o=JSON.stringify(o));var t={};t.message=o,t.status=-1,t.sekiro_seq=e;var n=JSON.stringify(t);console.log("sekiro: response :"+n),this.socket.send(n)},SekiroClient.prototype.registerAction=function(e,o){if("string"!=typeof e)throw new Error("an action must be string");if("function"!=typeof o)throw new Error("a handler must be function");return console.log("sekiro: register action: "+e),this.handlers[e]=o,this};
var client = new SekiroClient("wss://127.0.0.1:5612/business/register?group=test_web&clientId=" + Math.random());
client.registerAction("River6Enc", function (request, resolve, reject) {
var data1 = request['data1'];
var data2 = request['data2'];
var res = window.River6Enc(data1,data2);
resolve(res);
});
编写python脚本进行测试
import requests
data = {
"group": "test_web",#之前设置的group
"action": "loginDecrypt",#之前注册的action
"data1":"ff98a4726147460dadmin",#加密函数参数
"data2":"ZWQxNGIyZmQxNGZh"#加密函数参数
}
res = requests.get("http://127.0.0.1:5612/business/invoke",params=data )
print(res.text)
Yakit热加载
根据编写热加载代码即可:
loginDecryot = func(lfz) {
data1='ff98a4726147460d'+lfz
data2='ZWQxNGIyZmQxNGZh'
rsp = http.Get(f"http://127.0.0.1:5612/business/invoke?group=test_web&action=loginDecrypt&data1=${data1}&data2=${data2}")~
printf("data: ")
return json.Find(rsp.Data(), "$.data")
}
此时用户名就实现了自动加密。
原文始发于微信公众号(李白你好):流量解密-Web&小程序 | 无限debugger bypass、常规js逆向、非常规js逆向
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论