目标网站:aHR0cHM6Ly9ndDQuZ2VldGVzdC5jb20v
本文纯粹取巧!!!对小萌新很不友好
本文仅供学习交流,因使用本文内容而产生的任何风险及后果,作者不承担任何责任,一起学习吧
如有错误,请大佬们指出
代码均脱敏,如有侵权,请及时联系作者删除
目标确认
记得先将网站的配置设置为无感(一键通过)
目标接口:login
响应确认(success and fail )
请求成功返回数据如下:
{
"result": "success",
"reason": "",
"captcha_args": {
省略
},
"status": "success"
}
请求失败返回数据如下:
{
"result": "fail",
"reason": "pass_token used",
"captcha_args": {},
"status": "success"
}
负载分析
[ol]
[/ol]
以上参数均来自接口verify(直接搜就知道),那么逆向目标改变为这个接口。还是一样负载分析(搜索大法):
如下图:

1.png (157.8 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
[ol]
[/ol]
分析接口load,负载如下图:

2.png (15.45 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
[ol]
[/ol]
那么目标就先放在load上
load逆向
关键点就一个challenge,和之前的方法看堆栈后搜索。你要先确认它走了哪些js文件,在通过搜索快速定位。

3.png (183.26 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
是吧,很明确就知道是gt4.js这个文件,而不是上面那个。这就是看栈的好处。
直接进去打上断点。

4.png (80.7 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
这里调试比较基础,我直接放结果:
// challenge就是一个uuid,其实你也可以直接看出来,经验丰富的话
var uuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0;
var v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
那我们整理一下。代码如下(),试试发包。
import random
import re
import requests
def generate_uuid():
template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
def replace_char(match):
c = match.group()
r = random.randint(0, 15)
v = r if c == 'x' else (r & 0x3 | 0x8)
return hex(v)[2:] # hex函数返回的结果是'0x...',取[2:]去掉前缀
return re.sub(r'[xy]', replace_char, template)
# 测试生成 UUID
print(generate_uuid())
cookies = {
欸嘿
}
headers = {
欸嘿
}
params = {
欸嘿
}
response = requests.get('欸嘿', params=params, cookies=cookies, headers=headers)
print(response.text)

5.png (37.97 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
ok! 这里的逆向基本都解决了。那我们的重点又回到接口verify。基本上重点就是这个w了。那么还是老套路,看栈,然后可以搜索一些这些值看看能不能快速定位:w:,\u0077。如果发现都不行,那么就老老实实的一步步跟。w:可以搜到入口,打赏断点看看。

6.png (57.47 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
gcaptch4.js代码复制下来,通过v神(v_jstool)开源的工具,简单处理一下(打开配置页面仅变量压缩)。将代码覆盖。方便后续调试。 -->
w逆向
定位到如下:

7.png (58.33 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
我把关键代码摘出来分析,如下:
var _ᖚᖆᖀᖃ = (0,
_ᖆᕿᖄᖀ[_ᖀᖄᕴᖄ(9)])(_ᕹᖀᖃᖙ[_ᖄᕿᖚᕺ(9)][_ᖀᖄᕴᖄ(587)](_ᕶᖉᖃᕾ), _ᖉᕹᕺᖀ)
, u = {
callback: _ᖀᖄᕴᖄ(52),
captcha_id: _ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(542)],
challenge: _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(603)],
client_type: _ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(629)],
lot_number: _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(461)],
risk_type: _ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(666)],
payload: _ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(692)],
process_token: _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(697)],
payload_protocol: _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(644)],
pt: _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(641)],
w: _ᖚᖆᖀᖃ
};
// 这里很明显w --> _ᖚᖆᖀᖃ --> (0,_ᖆᕿᖄᖀ[_ᖀᖄᕴᖄ(9)])(_ᕹᖀᖃᖙ[_ᖄᕿᖚᕺ(9)][_ᖀᖄᕴᖄ(587)](_ᕶᖉᖃᕾ), _ᖉᕹᕺᖀ)
那么关键就是解出这部分的逻辑(这里你可以解混淆,然后通过花瓶替换,我这边讲解扎实的基本功就不替换了)。
核心代码讲解如下:
(0,_ᖆᕿᖄᖀ[_ᖀᖄᕴᖄ(9)])(_ᕹᖀᖃᖙ[_ᖄᕿᖚᕺ(9)][_ᖀᖄᕴᖄ(587)](_ᕶᖉᖃᕾ), _ᖉᕹᕺᖀ)
// 控制台看看
(0,_ᖆᕿᖄᖀ['default'])(_ᕹᖀᖃᖙ['default']['stringify'](_ᕶᖉᖃᕾ), _ᖉᕹᕺᖀ)
//相当于(0,_ᖆᕿᖄᖀ['default'])这个函数传入,两个值,一个_ᕹᖀᖃᖙ['default']['stringify'](_ᕶᖉᖃᕾ),一个_ᖉᕹᕺᖀ
// 第一个值是'{"device_id":"","lot_number":"欸嘿","pow_msg":"欸嘿","pow_sign":"欸嘿","geetest":"captcha","lang":"zh","ep":"123","biht":"1426265548","gee_guard":{"roe":{"aup":"3","sep":"3","egp":"3","auh":"3","rew":"3","snh":"3","res":"3","cdc":"3"}},"So89":"1AnD","2e424091":{"2a4b":"b2e4"},"em":{"ph":0,"cp":0,"ek":"11","wd":1,"nt":0,"si":0,"sc":0}}'
// 第二个是一个对象
我们现象目光放在入参这个字符串(json转的),分析构造。这里我看到stringify和后面这个_ᕶᖉᖃᕾ的输出,我就猜想了一下。如下:

8.png (78.68 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
那么我们接着看_ᕶᖉᖃᕾ是咋来的,简单分析一下:
[ol]
[/ol]
直接打上断点,结果如下:

9.png (55.64 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
我把核心代码提取出来讲解一下:
{
pow_msg: _ᖄᖆᖗᕺ + h,
pow_sign: p
}
// 很明显搞清楚,让我们一步步还原。
_ᖄᖆᖗᕺ = _ᕶᖆᕷᕵ + _ᕶᖃᖁᕹ(111) + _ᖀᖄᕴᖄ + _ᕸᖙᕹᕷ(111) + _ᖄᕿᖚᕺ + _ᕶᖃᖁᕹ(111) + _ᕹᕿᖆᖀ + _ᕶᖃᖁᕹ(111) + _ᕶᕴᕹᕶ + _ᕶᖃᖁᕹ(111) + _ᕶᖉᖃᕾ + _ᕸᖙᕹᕷ(111) + _ᕿᕵᖆᕾ + _ᕸᖙᕹᕷ(111);
// 简单还原一下:
_ᖄᖆᖗᕺ = _ᕶᖆᕷᕵ + '|' + _ᖀᖄᕴᖄ + '|' + _ᖄᕿᖚᕺ + '|' + _ᕹᕿᖆᖀ + '|' + _ᕶᕴᕹᕶ + '|' + _ᕶᖉᖃᕾ + '|' + _ᕿᕵᖆᕾ + '|';
// 那么这个参数就是几个值同 '|' 拼接而成的
// _ᕶᖆᕷᕵ --> 1 定死
// _ᖀᖄᕴᖄ --> 0 定死
// _ᖄᕿᖚᕺ --> 'md5' 看情况,他有三种模式,有md5,sha1,sha256 这个是决定 p 的生成的。
// _ᕹᕿᖆᖀ --> '2025-04-16T14:29:07.516729+08:00' 一个时间戳还原代码后面写。
// _ᕶᕴᕹᕶ --> captcha_id
// _ᕶᖉᖃᕾ --> lot_number
// _ᕿᕵᖆᕾ --> ''
h = _ᕺᖗᕾᖗ['guid']() // 跟栈你会发现他就是 e() + e() + e() + e()
function e() {
var _ᖚᖆᖀᖃ = _ᖀᕴᖘᕺ.$_Dr()[0][7];
for (; _ᖚᖆᖀᖃ !== _ᖀᕴᖘᕺ.$_Dr()[3][6]; ) {
switch (_ᖚᖆᖀᖃ) {
case _ᖀᕴᖘᕺ.$_Dr()[3][7]:
return (65536 * (1 + Math['random']()) | 0)['toString'](16)['substring'](1);
break
}
}
}
//这个函数看着那么吓人,其实就执行了一步就是(65536 * (1 + Math['random']()) | 0)['toString'](16)['substring'](1);
//所以h函数就可以写为:
function e(){
return (65536 * (1 + Math['random']()) | 0)['toString'](16)['substring'](1);
}
h = e() + e() + e() + e()
// 接着看p,这个你看到分支你就笑吧。搞清楚入参,带入算算,如果是标准加密就爽了,下面这个都不用扣算法了。
// 入参就是pow_msg
switch (_ᖄᕿᖚᕺ) {
case 'md5':
p = (new (_ᖉᕹᕺᖀ[_ᕸᖙᕹᕷ(9)][_ᕸᖙᕹᕷ(771)]))[_ᕶᖃᖁᕹ(793)](l);
break;
case 'sha1':
p = (new (_ᖉᕹᕺᖀ[_ᕸᖙᕹᕷ(9)][_ᕸᖙᕹᕷ(789)]))[_ᕶᖃᖁᕹ(793)](l);
break;
case 'sha256':
p = (new (_ᖉᕹᕺᖀ[_ᕶᖃᖁᕹ(9)][_ᕶᖃᖁᕹ(778)]))[_ᕸᖙᕹᕷ(793)](l)
}
// 我们走入分支直接测试即可
// (new (_ᖉᕹᕺᖀ[_ᕸᖙᕹᕷ(9)][_ᕸᖙᕹᕷ(771)]))[_ᕶᖃᖁᕹ(793)]('12345') --> '827ccb0eea8a706c4c34a16891f84e7b' 标准小写md5-32位
// 那么这部分加密我们就完成了
继续分析参数:
[ol]
[/ol]
继续分析,本人还是继续从代码角度分析:
var i = (0,_ᕸᖙᕹᕷ[_ᖄᕿᖚᕺ(68)])(_ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(693)], _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(461)]),
r = (0,_ᕸᖙᕹᕷ[_ᖄᕿᖚᕺ(68)])(_ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(652)], _ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(461)])
规律如下图:

10.png (61.77 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
打上断点,让我们看看他的逻辑
var i = _ᕸᖙᕹᕷ['getStringByIndexes'](_ᕶᖆᕷᕵ['lot'],_ᕶᖆᕷᕵ['lotNumber']),
r = _ᕸᖙᕹᕷ['getStringByIndexes'](_ᕶᖆᕷᕵ['lotRes'],_ᕶᖆᕷᕵ['lotNumber'])
// lot_number 为入参
function _ᖚᖆᖀᖃ(_ᕶᖉᖃᕾ, _ᕶᕴᕹᕶ) {
// var _ᖄᕿᖚᕺ = _ᖀᕴᖘᕺ.$_Cl
// , _ᕶᖆᕷᕵ = ["$_DBF_"].concat(_ᖄᕿᖚᕺ)
// , _ᖀᖄᕴᖄ = _ᕶᖆᕷᕵ[1];
// _ᕶᖆᕷᕵ.shift(); 这部分你都不用看,没啥用,ob混淆解混用的
var _ᕹᕿᖆᖀ = _ᕶᖆᕷᕵ[0];
return _ᕶᖉᖃᕾ[_ᖄᕿᖚᕺ(48)](function(_ᖚᖆᖀᖃ) {
var _ᕶᖉᖃᕾ = _ᖀᕴᖘᕺ.$_Cl
, _ᖄᕿᖚᕺ = ["$_DCAl"].concat(_ᕶᖉᖃᕾ)
, _ᕶᖆᕷᕵ = _ᖄᕿᖚᕺ[1];
_ᖄᕿᖚᕺ.shift();
var _ᖀᖄᕴᖄ = _ᖄᕿᖚᕺ[0];
return _ᖚᖆᖀᖃ[_ᕶᖉᖃᕾ(48)](function(_ᖚᖆᖀᖃ) {
var _ᕶᖉᖃᕾ = _ᖀᕴᖘᕺ.$_Cl
, _ᖄᕿᖚᕺ = ["$_DCFk"].concat(_ᕶᖉᖃᕾ)
, _ᕶᖆᕷᕵ = _ᖄᕿᖚᕺ[1];
_ᖄᕿᖚᕺ.shift();
var _ᖀᖄᕴᖄ = _ᖄᕿᖚᕺ[0];
var _ᕹᕿᖆᖀ = _ᖚᖆᖀᖃ[_ᕶᖉᖃᕾ(64)]
, _ᖉᕹᕺᖀ = _ᕹᕿᖆᖀ[0]
, _ᕺᖗᕾᖗ = 1
放个图,你可以看看我是如何猜想和思考的。

11.png (99.63 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
我的猜想是它按照固定数组在字符串中取值。让我们着手试试。结果如下:

12.png (70.42 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
那么r也是,结果如下:

13.png (26.75 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
那么入参已经全部弄清楚了。接下来就是加密。还是一样层层分析。
逆向算法分析
_ᖆᕿᖄᖀ['default'](_ᕹᖀᖃᖙ['default']['stringify'](_ᕶᖉᖃᕾ), _ᖉᕹᕺᖀ)
这是全文最难的地方。

14.png (158.62 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
你看看我找了什么,key,iv???

15.png (80.89 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
这不会是一个标准的ase吧???不急,先猜想,后验证,大不了不对在扣呗。有的是机会。
把相关信息保存一下,我们猜测是:
猜想加密算法2 --> ase
ase : 模式 cbc
key e() + e() + e() + e()随机
iv 0000000000000000
我怀着好奇把1里面的new (_ᕶᖃᖁᕹ[_ᖄᕿᖚᕺ(9)])输出看了看,欸,这不是rsa的标志吗(每次结果都不一样我就在怀疑了,当然也可能是时间戳或者其他算法)?但是我看到了 65537 这是一个常用的 RSA 公钥指数。而且,它的数组跟了大数里面跟了一个小数37,正好是前面37的个数,这里就相当于表示这个大整数 n 拆分为 37 个段。
猜归猜,我们还是要搞清楚整体流程:
// 初始化前面的加密算法之后,我们一步步看看干了什么,我把核心流程抽取出来了
var o = _ᖀᖄᕴᖄ(878) === _ᕺᖗᕾᖗ[_ᖀᖄᕴᖄ(641)] // true
, a = _ᕺᖗᕾᖗ[_ᖄᕿᖚᕺ(641)] // '1'
, _ = _ᖆᖚᖉᕾ[a]['asymmetric']['encrypt'](_ᖂᖈᖗᕾ); // 这里把ase的key,放到加密算法1里面加密
while (o && (!_ || 256 !== _[_ᖀᖄᕴᖄ(84)])) // false 不走
_ᖂᖈᖗᕾ = (0,
_ᕴᕺᖙᕷ[_ᖄᕿᖚᕺ(58)])(),
_ = (new (_ᕶᖃᖁᕹ[_ᖄᕿᖚᕺ(9)]))[_ᖄᕿᖚᕺ(955)](_ᖂᖈᖗᕾ);
var u = _ᖆᖚᖉᕾ[a]['symmetrical']['encrypt'](_ᕶᖉᖃᕾ, _ᖂᖈᖗᕾ); // ase的key 和最开始的入参计算加密,生成字节数组
return (0,
_ᕴᕺᖙᕷ['arrayToHex'])(u) + _ // 两个加密结果相加
这里结合之前的分析,我们猜测是流程是:
[ol]
[/ol]
第一步我们只需要把自己生成的rsa给他替换了,看看能不能过就知道是否是正确的(这个方法很常用哦)。我测过了,是可以过的。代码如下:
import random
from Crypto.Util.number import bytes_to_long, long_to_bytes
n_segments = {
欸嘿
}
# 还原 n(拼接成一个大整数)
n = 0
for i in range(n_segments["t"] - 1, -1, -1):
n = (n > 6))
result.insert(1, 0x80 | (code & 0x3F))
else:
result.insert(0, 0xE0 | (code >> 12))
result.insert(1, 0x80 | ((code >> 6) & 0x3F))
result.insert(2, 0x80 | (code & 0x3F))
return result
# ---- 构造带 PKCS#1 padding 的明文块 ----
def rsa_pkcs1_v15_pad(plaintext: str, total_length: int = 128) -> bytes:
data = reverse_utf8_bytes(plaintext)
ps_length = total_length - len(data) - 3 # 3 = 0x00 0x02 0x00
if ps_length 整数 M
c = pow(m, e, n) # 执行 C = M^e mod n
cipher_hex = hex(c)[2:] # 转十六进制去掉 '0x' 前缀
# 可选补0,补齐256位 hex 字符
if len(cipher_hex) % 2 != 0:
cipher_hex = '0' + cipher_hex
print("加密前(明文):", plaintext)
print("Padding 后字节(十六进制):", padded_bytes.hex())
print("加密后(十六进制):", cipher_hex)
第二步,看看这个ase是否猜测正确。我们先简单编写一些aes代码,如下:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii
# 参数还原
plaintext = '欸嘿'
key_str = 'b903bbf7a0871a11'
iv_str = '0000000000000000'
key = key_str.encode('utf-8')
iv = iv_str.encode('utf-8')
# 补足 16 字节
key = key.ljust(16, b'\x00')
iv = iv.ljust(16, b'\x00')
# AES CBC PKCS7 加密
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size))
# 输出 hex 串用于比对
hex_output = binascii.hexlify(ciphertext).decode()
print(hex_output)
结果如下:

16.png (221.33 KB, 下载次数: 0)
下载附件
2025-4-16 18:56 上传
成功了,不用扣代码了。爽!不过这个扣的难度不高。认真一点没啥问题。
最后的结果就是--> 两个结果合并返回。
最后测试一下,嗯哼,没毛病。无感相较于滑块简单点。滑块还有轨迹收集。