声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
最近星球有个小伙伴,公司是做 gov 相关业务的,碰到一个采购网站,感觉有些棘手,故咨询 K 哥。该业务网站,一打开,转起来了,当然,和国外产品,完全不是一个量级。也挺别致,本期就对这个网站,进行逆向分析:
逆向目标
-
目标:某采购网,360 磐云盾、文字点选验证码逆向分析
-
网站:
aHR0cDovL3d3dy55bmdwLmNvbS9wYWdlL3Byb2N1cmVtZW50L3Byb2N1cmVtZW50TGlzdC5odG1s
360 网站云防护系统 --- 磐云
产品介绍:aHR0cHM6Ly9iLjM2MC5uZXQvcHJvZHVjdC1jZW50ZXIvMzYwLW5ldC1zYWZlL3Bhbi15dW4=
网站接入 360 网站云防护系统后,所有的访问请求先经过防御平台过滤,实现源站隐身,使其不会暴露在公网的攻击威胁之中。依托 360 全面攻击样本库,可实时对恶意访问流量进行甄别和拦截,并保证正常访问请求返回源站。
逆向过程
抓包分析
打开网站链接,页面显示 进入前检查浏览器...
,然后开始转圈,一段时间后,进入页面,内容渲染了出来。这个过程,看起来像是在校验浏览器环境。打开开发者人员工具,清空缓存,刷新页面,抓包后发现,procurementList.html
接口走了三遍:
① 响应返回的 html 的 title 为 请稍候... | 磐云
,也就是正在检测中,检测的是什么目前还不知道,响应 cookie 为 PYCCV
:
② 携带参数 answer
、PYCCV
(cookie)再次请求该接口,响应状态码为 302,重定向了,响应 cookie 为 PYCCS
:
③ 最后,携带 PYCCS
(cookie)请求接口,即可获取到正确的 html 页面,响应 cookie 为 xincaigou
:
除此之外,在翻到第二页的时候,还会触发一个文字点选验证码(依次点击),后文会逐一分析:
逆向分析
answer
综上所述,整套流程中,请求 cookie 都是响应返回的,只有 answer
参数是动态变化的。如果按照常规思路,全局搜索或者跟栈,尝试定位该参数,但是操作后会发现,这个参数并不是 js 文件中,通过算法生成的,那到底是怎么来的呢?
我们将目光放回到第一次请求 procurementList.html
接口时,返回的 html 中去,拉到最下面,会发现通过 eval 函数,输出了一段自执行的 js 代码:
平时业务做的多的小伙伴,应该对这种类型较为熟悉,不少网站也是通过这种方式对参数或者 cookie 进行加密,接下来,我们就分析下,这段代码的逻辑,看其是如何执行的:
eval(function (p, a, c, k, e, d) {
e = function (c) {
return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
};
if (!''.replace(/^/, String)) {
while (c--) d[e(c)] = k[c] || e(c);
k = [function (e) {
return d[e]
}];
e = function () {
return '\\w+'
};
c = 1;
}
;
while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
return p;
}('i n$=[\'\\q\\f\\a\\4\\k\\k\\3\\o\\p\\3\',\'\\q\\m\\4\\c\\B\\4\\o\\g\\m\\3\\6\',\'\\q\\w\\a\\4\\k\\k\\3\\o\\p\\3\\G\\7\\6\\b\'];$(n$[0])["\\g\\a\\7\\m"]();$(y(){F(y(){i 9={};i 8;i 5=A;i j=I["\\v\\g\\3\\6\\E\\p\\3\\o\\e"]["\\e\\7\\H\\7\\m\\3\\6\\w\\4\\g\\3"]();5=5*C;(8=j["\\b\\4\\e\\f\\a"](/D ([\\d.]+)/))?9["\\h\\3"]=8[l]:(8=j["\\b\\4\\e\\f\\a"](/J\\/([\\d.]+)/))?9["\\c\\h\\6\\3\\c\\7\\t"]=8[l]:(8=j["\\b\\4\\e\\f\\a"](/T\\/([\\d.]+)/))?9["\\f\\a\\6\\7\\b\\3"]=8[l]:(8=j["\\b\\4\\e\\f\\a"](/S.([\\d.]+)/))?9["\\7\\x\\3\\6\\4"]=8[l]:(8=j["\\b\\4\\e\\f\\a"](/R\\/([\\d.]+).*U/))?9["\\g\\4\\c\\4\\6\\h"]=8[l]:V;5=5+Q;r(9["\\h\\3"]||9["\\c\\h\\6\\3\\c\\7\\t"]||9["\\f\\a\\6\\7\\b\\3"]||9["\\7\\x\\3\\6\\4"]||9["\\g\\4\\c\\4\\6\\h"]){5=(5*L+K);r(5<s)5=5+z;i u=$(n$[1]);r(5>z)5=P["\\c\\k\\7\\7\\6"](5/s);u["\\O\\4\\k"](5);$(n$[2])["\\g\\v\\N\\b\\h\\e"]()}},M)});', 58, 58, '|||x65|x61|x08c924|x72|x6f|x0fcad9|x06dd1a|x68|x6d|x66||x74|x63|x73|x69|var|x01c264|x6c|0x1|x77|_|x6e|x67|x23|if|0x7b|x78|x0b515d|x75|x43|x70|function|0x929|0x14|x5f|0x0f|msie|x41|setTimeout|x46|x4c|navigator|firefox|0x7|0x3|0x3e8|x62|x76|Math|0x20|version|opera|chrome|safari|0x0'.split('|'), 0, {}))
自执行函数中,总共传入了六个参数 p, a, c, k, e, d,各自的含义如下:
-
p:一段被编码的 JavaScript 代码,包含转义字符(如
\\x61
表示a
); -
a:基数(58),用于解码字符串;
-
c:关键字数量(58);
-
k:关键字数组(字符串分割后的数组);
-
e:解码函数(用于将数字转为字符);
-
d:存储解码后的键值对。
再将上面的代码拆分一下,逐步分析,首先就是 e 函数,功能就是解码:
e = function (c) {
return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
};
首先递归,将 c
除以基数 a
,生成高位字符,取余后,若余数 c % a > 35
,转为 ASCII 字符(如 36 -> 36+29=65 -> 'A'),否则转为 base36
字符串(如 10 -> 'a'
),简而言之,就是将数字 c
转换为字符串。
接着做字符串替换:
if (!''.replace(/^/, String)) {
// d[e(c)] = k[c] -> 将 k 数组中的值按解码函数 e 映射到 d
while (c--) d[e(c)] = k[c] || e(c);
k = [function (e) {
return d[e]
}];
e = function () {
return '\\w+'
};
c = 1;
}
构建一个字典 d
,用于将混淆的字符(如 \\x61
)还原为原始关键字。
最后将混淆的代码字符串 p
中的占位符(如 \\b0\\b
)替换为实际值:
while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]);
return p;
一套操作下来,就吐出了真正的生成 answer
参数的算法:
var _$ = ['\x23\x63\x68\x61\x6c\x6c\x65\x6e\x67\x65', '\x23\x77\x61\x66\x5f\x61\x6e\x73\x77\x65\x72', '\x23\x43\x68\x61\x6c\x6c\x65\x6e\x67\x65\x46\x6f\x72\x6d'];
$(_$[0])["\x73\x68\x6f\x77"]();
$(function () {
setTimeout(function () {
var x06dd1a = {};
var x0fcad9;
var x08c924 = 0x14;
var x01c264 = navigator["\x75\x73\x65\x72\x41\x67\x65\x6e\x74"]["\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65"]();
x08c924 = x08c924 * 0x0f;
(x0fcad9 = x01c264["\x6d\x61\x74\x63\x68"](/msie ([\d.]+)/)) ? x06dd1a["\x69\x65"] = x0fcad9[0x1] : (x0fcad9 = x01c264["\x6d\x61\x74\x63\x68"](/firefox\/([\d.]+)/)) ? x06dd1a["\x66\x69\x72\x65\x66\x6f\x78"] = x0fcad9[0x1] : (x0fcad9 = x01c264["\x6d\x61\x74\x63\x68"](/chrome\/([\d.]+)/)) ? x06dd1a["\x63\x68\x72\x6f\x6d\x65"] = x0fcad9[0x1] : (x0fcad9 = x01c264["\x6d\x61\x74\x63\x68"](/opera.([\d.]+)/)) ? x06dd1a["\x6f\x70\x65\x72\x61"] = x0fcad9[0x1] : (x0fcad9 = x01c264["\x6d\x61\x74\x63\x68"](/version\/([\d.]+).*safari/)) ? x06dd1a["\x73\x61\x66\x61\x72\x69"] = x0fcad9[0x1] : 0x0;
x08c924 = x08c924 + 0x20;
if (x06dd1a["\x69\x65"] || x06dd1a["\x66\x69\x72\x65\x66\x6f\x78"] || x06dd1a["\x63\x68\x72\x6f\x6d\x65"] || x06dd1a["\x6f\x70\x65\x72\x61"] || x06dd1a["\x73\x61\x66\x61\x72\x69"]) {
x08c924 = (x08c924 * 0x3 + 0x7);
if (x08c924 < 0x7b) x08c924 = x08c924 + 0x929;
var x0b515d = $(_$[1]);
if (x08c924 > 0x929) x08c924 = Math["\x66\x6c\x6f\x6f\x72"](x08c924 / 0x7b);
x0b515d["\x76\x61\x6c"](x08c924);
$(_$[2])["\x73\x75\x62\x6d\x69\x74"]()
}
}, 0x3e8)
});
吐出来的算法经过了混淆,很多经过十六进制转义后的字符串,我们可以通过 DeepSeek 之类的 AI 工具,对代码进行美化还原,调校后效果如下:
navigator = {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
};
(function() {
// 浏览器信息检测逻辑
const browserInfo = {};
let matchResult;
let answer = 20;
const userAgent = navigator.userAgent.toLowerCase();
// 检测浏览器类型
if (matchResult = userAgent.match(/msie ([\d.]+)/)) {
browserInfo.ie = matchResult[1];
} else if (matchResult = userAgent.match(/firefox\/([\d.]+)/)) {
browserInfo.firefox = matchResult[1];
} else if (matchResult = userAgent.match(/chrome\/([\d.]+)/)) {
browserInfo.chrome = matchResult[1];
} else if (matchResult = userAgent.match(/opera.([\d.]+)/)) {
browserInfo.opera = matchResult[1];
} else if (matchResult = userAgent.match(/version\/([\d.]+).*safari/)) {
browserInfo.safari = matchResult[1];
}
// 计算延迟时间
answer = answer * 15; // 20*15=300
answer += 32; // 300+32=332
if (browserInfo.ie || browserInfo.firefox || browserInfo.chrome || browserInfo.opera || browserInfo.safari) {
answer = (answer * 3) + 7; // 332*3+7=1003
if (answer < 123) {
answer += 2345; // 下限保护
}
if (answer > 2345) {
answer = Math.floor(answer / 123); // 上限保护
}
// 输出结果 (替代表单提交)
console.log('计算后的 answer:', answer);
}
})();
// 计算后的 answer: 1003
与网站结果一致:
可以通过 subprocess 模块结合正则表达式,执行出结果,当然,这只是其中一种思路,完整的算法同步在知识星球中,可供参考学习。
文字点选验证码
通过以上步骤,就能成功采集到第一页的数据了,但是当我们接着采集第二页的时候,会发现,响应内容为 验证码校验不通过, 当前请求禁止访问
,响应状态码 406。到网页中操作后发现,翻页就会触发文字点选验证码,同样的,我们来抓包分析下。
/captcha.get.svc
接口,响应返回了 originalImageBase64(验证码图片链接)、secretKey、token 以及 wordList(依次点击的文字),请求参数中,ts 为时间戳,clientUid 看起来像是 uuid,后文分析:
/captcha.check.svc
为验证接口,请求参数如下,除了前文提到的,还有两个参数,其中 token 参数是 /captcha.get.svc
接口返回的,pointJson 参数后文会进行逆向分析:
验证通过,响应内容如下,此时的 cookies 就白了:
验证失败,响应内容如下:
先来看看 clientUid 参数,从 /captcha.get.svc
接口处,跟栈到 verify.js
文件中去,ctrl + f 局部搜索一下该参数,有四个结果,全部打上断点,清除缓存,刷新网页,翻页再次触发验证码之前,会断住。此时 clientUid 参数的值已经生成了,从浏览器的 localStorage 中读取到的已存储的 point 数据:
既然能取到数据,那肯定有地方先存储了,直接局部搜索 localStorage.setItem
试试,成功定位到了生成的位置,就是 uuid,直接扣下来,或者用 python 复现都可以:
接下来分下验证参数 pointJson,该案例较为清晰,所有的参数算法,都在 verify.js 文件中。同上文,局部搜索 pointJson,在搜索到的位置,都打上断点,依次点击文字后,即会断住:
这里就是个标准的 AES 加密,加密模式为 ECB,key 是由前面接口返回的,加密内容为点选的坐标,可以去 K 哥工具站(https://www.kgtools.cn/secret/aes)校验下:
验证通过后,接着请求 /procurementList.html
接口,抓包发现,该接口的 captchaCheckFlag
参数,从 "0" 变成了一段加密字符串:
直接到 verify.js 文件中搜索该参数,发现没有任何结果,那么就得跟栈分析了,向上跟栈到下图处,即可定位到参数生成的位置:
生成算法如下:
var captchaVerification = aesEncrypt(_this.backToken + '---' + JSON.stringify(_this.checkPosArr), _this.secretKey)
至此,整个流程就分析完了,相关算法源码会分享到知识星球中,仅供学习交流!
结果验证
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)