jy无感取巧思路
目标网站:aHR0cHM6Ly9ndDQuZ2VldGVzdC5jb20v
本文纯粹取巧!!!对小萌新很不友好
本文仅供学习交流,因使用本文内容而产生的任何风险及后果,作者不承担任何责任,一起学习吧
如有错误,请大佬们指出
代码均脱敏,如有侵权,请及时联系作者删除
目标确认
记得先将网站的配置设置为无感(一键通过)
目标接口:login
响应确认(success and fail )
请求成功返回数据如下:
{
"result": "success",
"reason": "",
"captcha_args": {
省略
},
"status": "success"
}
请求失败返回数据如下:
{
"result": "fail",
"reason": "pass_token used",
"captcha_args": {},
"status": "success"
}
负载分析
- captcha_id
- lot_number
- pass_token
- gen_time
- captcha_output
以上参数均来自接口verify(直接搜就知道),那么逆向目标改变为这个接口。还是一样负载分析(搜索大法):
如下图:
- callback: 这个定死无所谓,本质就是一个
geetest_ + 时间戳 parseInt(Math.random() * 10000) + (new Date()).valueOf()
- captcha_id: 这个一个js文件里面,根据不同的验证类型,他的id也不同。这个也可以定死。
- client_type: 使用平台类型web啥的
- lot_number: 来自接口
load(分析在下面)
- risk_type: 风险评测ai,这个和验证类型有关
- payload: 来自接口
load(分析在下面)
- process_token: 来自接口
load(分析在下面)
- payload_protocol: 非重要参数定死即可
- pt: 非重要参数定死即可
- w: 一大串不用看,肯定是逆向点
分析接口load,负载如下图:
- callback: 这个定死无所谓,本质就是一个
geetest_ + 时间戳
- captcha_id: 这个一个js文件里面,根据不同的验证类型,他的id也不同。这个也可以定死。
- challenge: 搜不到,可能是加密参数
- client_type: 使用平台类型web啥的
- risk_type: ai
- lang: 非重要参数定死即可,使用语言
那么目标就先放在load上
load逆向
关键点就一个challenge,和之前的方法看堆栈后搜索。你要先确认它走了哪些js文件,在通过搜索快速定位。
是吧,很明确就知道是gt4.js这个文件,而不是上面那个。这就是看栈的好处。
直接进去打上断点。
这里调试比较基础,我直接放结果:
// 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)
ok! 这里的逆向基本都解决了。那我们的重点又回到接口verify。基本上重点就是这个w了。那么还是老套路,看栈,然后可以搜索一些这些值看看能不能快速定位:w:,\u0077。如果发现都不行,那么就老老实实的一步步跟。w:可以搜到入口,打赏断点看看。
<!-- 将gcaptch4.js代码复制下来,通过v神(v_jstool)开源的工具,简单处理一下(打开配置页面仅变量压缩)。将代码覆盖。方便后续调试。 -->
w逆向
定位到如下:
我把关键代码摘出来分析,如下:
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和后面这个_ᕶᖉᖃᕾ的输出,我就猜想了一下。如下:
那么我们接着看_ᕶᖉᖃᕾ是咋来的,简单分析一下:
- device_id :为空
- lot_number :load包里面有
- pow_msg: 能搜到,不多,全部打上断点
- pow_sign:能搜到,不多,全部打上断点
- geetest: 非重要参数
- lang:非重要参数
- ep:非重要参数
- biht:暂时不清楚
- gee_guard,So89,2e424091,em:暂时不清楚
直接打上断点,结果如下:
我把核心代码提取出来讲解一下:
{
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位
// 那么这部分加密我们就完成了
继续分析参数:
- So89 定值,我刷新了几次没变
- 2e424091 --> 变值,自己本身就是,这个参数就在上面可以直接看到
继续分析,本人还是继续从代码角度分析:
var i = (0,_ᕸᖙᕹᕷ[_ᖄᕿᖚᕺ(68)])(_ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(693)], _ᕶᖆᕷᕵ[_ᖄᕿᖚᕺ(461)]),
r = (0,_ᕸᖙᕹᕷ[_ᖄᕿᖚᕺ(68)])(_ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(652)], _ᕶᖆᕷᕵ[_ᖀᖄᕴᖄ(461)])
规律如下图:
打上断点,让我们看看他的逻辑
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 < _ᕹᕿᖆᖀ[_ᕶᖆᕷᕵ(84)] ? _ᕹᕿᖆᖀ[1] + 1 : _ᕹᕿᖆᖀ[0] + 1;
return _ᕶᕴᕹᕶ[_ᕶᖆᕷᕵ(74)](_ᖉᕹᕺᖀ, _ᕺᖗᕾᖗ)
})[_ᕶᖆᕷᕵ(93)](_ᕶᖆᕷᕵ(52))
})[_ᖄᕿᖚᕺ(93)](_ᖄᕿᖚᕺ(92))
}
// 这里这个代码其实不用跟的这么麻烦,我们简单跟一下找到如下:
// return new _ᖉᕹᕺᖀ(_ᖀᖄᕴᖄ[_ᕶᖉᖃᕾ(173)](_ᖚᖆᖀᖃ));
放个图,你可以看看我是如何猜想和思考的。
我的猜想是它按照固定数组在字符串中取值。让我们着手试试。结果如下:
那么r也是,结果如下:
那么入参已经全部弄清楚了。接下来就是加密。还是一样层层分析。
逆向算法分析
_ᖆᕿᖄᖀ['default'](_ᕹᖀᖃᖙ['default']['stringify'](_ᕶᖉᖃᕾ), _ᖉᕹᕺᖀ)
这是全文最难的地方。
你看看我找了什么,key,iv???
这不会是一个标准的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) + _ // 两个加密结果相加
这里结合之前的分析,我们猜测是流程是:
- _ 是 随机key通过rsa加密
- u 是 之前的入参和随机key 进行aes加密
- 最后将两个结果合并返回。
第一步我们只需要把自己生成的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 << 28) + n_segments[i]
# 公钥指数
e = 65537
# 待加密明文
plaintext = "2989a61219603dc6"
# ---- UTF-8 倒序编码 ----
def reverse_utf8_bytes(text):
result = []
for c in reversed(text):
code = ord(c)
if code < 0x80:
result.insert(0, code)
elif code < 0x800:
result.insert(0, 0xC0 | (code >> 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 < 8:
raise ValueError("Message too long")
# 随机非零 padding
padding = []
while len(padding) < ps_length:
b = random.randint(1, 255)
if b != 0:
padding.append(b)
padded = bytearray()
padded.append(0x00)
padded.append(0x02)
padded.extend(padding)
padded.append(0x00)
padded.extend(data)
return bytes(padded)
# ---- 构造明文块并加密 ----
padded_bytes = rsa_pkcs1_v15_pad(plaintext, 128)
m = bytes_to_long(padded_bytes) # 明文块 -> 整数 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)
结果如下:
成功了,不用扣代码了。爽!不过这个扣的难度不高。认真一点没啥问题。
最后的结果就是--> 两个结果合并返回。
最后测试一下,嗯哼,没毛病。无感相较于滑块简单点。滑块还有轨迹收集。