关于接龙管家的校验的逆向研究 Created 2026-04-04 | Updated 2026-04-04
| Post Views:
进行这个逆向的原因是学校要求的每晚的打卡,我之前也尝试对该小程序的发出的数据进行抓包并尝试请求,当然正常打卡肯定是没问题,但是 Authorization 会在三天之后过期,所以这种方法无持久性,一旦过期就要重新抓包,很显然这并不优雅,因此我进行了这次的逆向研究,并且我也没在网上找到相关的资料,所以决定写完并分享出来。
整体流程梳理 一开始尝试分析小程序包,但没有发现有价值的信息,于是转向网页端进行分析,从二维码登录流程入手。
整理后可以得到整个流程如下:
1 2 3 4 5 6 7 8 9 10 11 获取 uuid ↓ 扫码二维码 ↓ 轮询获取 wx_code ↓ 换取 Token ↓ 构造加密 Payload ↓ 调用业务接口
登入流程分析 获取 uuid 首先我们可以发现一个很可疑的请求
1 https://open.weixin.qq.com/connect/qrcode/081qn7WK3XnK000R
通过刷新网页,发现后面的 qrcode 后面的路径一直在发生改变,那么我们可以猜测这个是决定二维码生成的 id,之类的内容,那么我们需要继续分析 081qn7WK3XnK000R 是从那里获取的,发现另一个可疑的请求
1 https://open.weixin.qq.com/connect/qrconnect?appid=wx4a23ae4b8f291087&scope=snsapi_login&redirect_uri=https%3A%2F%2Fi.jielong.com%2Flogin-callback&state=&login_type=jssdk&self_redirect=true
我们先把这个请求的参数进行拆分
1 2 3 4 5 appid =wx4a23ae4b8f291087scope =snsapi_loginredirect_uri =https://i.jielong.com/login-callbacklogin_type =jssdkself_redirect =true
参数
说明
appid
应用唯一标识,用于微信的登录来源验证
scope
应用授权作用域,网页应用目前仅填写snsapi_login
response_type
微信验证完成之后返回的类型,官方开发者文档写这里应该填写 code,但是这里填写了 jssdk 原因未知
self_redirect
true:手机点击确认登录后可以在 iframe 内跳转到 redirect_uri,false:手机点击确认登录后可以在 top window 跳转到 redirect_uri。默认为 false
好了,我们构造参数尝试请求看看是什么,打开网页看到了一个登录二维码,打开控制台发现那个请求 uuid 的已经发起了,我们继续观察请求,但是并没有发现什么,回头查看第一个响应只有一个 html 页面,下滑观察 js 部分,看到一个有趣的函数。
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 function m ( ) { var e = jQuery.Deferred (), t = window .location .href .replace (/#.*$/ , '' ) + '&f=xml&' + (new Date ).getTime (); return jQuery.ajax ({ url : t, type : 'GET' , dataType : 'xml' , cache : !1 , success : function (t ) { $('.js_refresh_qrcode_loading' ).hide (), $('.js_refresh_qrcode_mask' ).hide (), G = jQuery (t).find ('uuid' ).text (), $('.js_qrcode_img' ).attr ('src' , '/connect/qrcode/' + G), setTimeout (r, 2000 ), e.resolve () }, error : function (t, n, o ) { $('.js_refresh_qrcode_loading' ).hide (), $('.js_refresh_qrcode_mask' ).hide (), $('#quick_login_error_toast' ).fadeIn ( 300 , function ( ) { var e = this ; setTimeout (function ( ) { $(e).fadeOut (300 ) }, 1000 ) } ), console .log ('qrcode img error: ' , n, o), e.resolve () } }), e.promise () }
观察登录二维码请求函数 m() 可以发现,它会在 URL 上追加 &f=xml,并设置 dataType: 'xml'。 因此可以确认,这里的 f=xml 用于指定接口返回 XML 格式数据:
1 2 3 4 5 6 7 8 9 <root > <uuid > 071O1WU436rTkl2T</uuid > <appname > 接龙管家</appname > <appdesc > 为班级群、工作群、学习群...提供接龙、打卡、填表、问卷、考试、作业、投票、报名、预约、文件签字等各式信息收集功能,支持微信、QQ和PC多端使用 </appdesc > <redirect_uri > https://i.jielong.com/login-callback</redirect_uri > <usenewdomain > 1</usenewdomain > </root >
这里的 uuid,就是我们请求登录二维码的参数。
生成二维码 这一步并不难,观察刚才的请求,我们可以发现请求的格式是
1 https://open.weixin.qq.com/connect/qrcode/{uuid}
直接构造链接并打开就可以看到一张 jpg 的二维码图片,可以正常扫描
获取 wx_code 我们可以在请求中看到多次 https://lp.open.weixin.qq.com/connect/l/qrconnect?uuid={uuid} 的 GET 请求,查看响应如下
1 window.wx_errcode=408;window.wx_code='';
阅读开发者文档发现并没有写这个 errcode 是什么,通过测试得到以下结果,我无法保证准确仅供参考。
errcode
说明
408
未扫码
402
二维码已过期
404
登入被取消
405
成功登录并返回 wx_code
这一步建议使用轮询观察状态,我们成功登录之后拿到 wx_code 准备下一步。
Payload 加密分析 登入成功之后抓到一个请求,在这时的 authorization 还是 undefined
1 https://i-api.jielong.com/api/User/OpenAuth?code=021UYGkl2cOqth41oBml2lUt3M3UYGkE
很明显这个链接后面的 code 就是前文的 wx_code, 观察它的请求头发现有一个 X-Api-Request-Payload 后面跟着一段 par0NCtm+oBuEkBOI+w4...,不难发现这是一个加密后的签名字段。
定位 Payload 构造入口 通过搜索 X-Api-Request-Payload 找到一个函数
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 async function R (e, t, n ) { const a = new AbortController , r = a.signal , o = (0 , v.L )() + (e.startsWith ('/' ) ? '' : '/' ) + e, i = { Authorization : (0 , A.gf )(), 'X-Api-Request-Payload' : S.getPayload (e.startsWith ('/' ) ? e : '/' + e, 'POST' ), 'Content-Type' : 'application/json' , 'X-Api-Request-Mode' : 'cors' }, s = new j (n.onChunkReceived ); return fetch ( o, { method : 'POST' , headers : i, mode : 'cors' , body : JSON .stringify (t), signal : r } ).then ( ( async e => { const t = e.body .getReader (); !async function e ( ) { if (r.aborted ) return ; const a = await t.read (); a.done ? (s.onChunkReceived (a.value || new Uint8Array ), n.onSuccess ()) : (s.onChunkReceived (a.value || new Uint8Array ), await e ()) }() } ) ).catch ((e => { r.aborted || n.onError (e) })), { abort : () => { null == a || a.abort () } } }
Payload 构造函数分析 发现 X-Api-Request-Payload 通过调用 D.getPayload() 构造了,那么我们去看一下这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 getPayload (e, t ) { const n = P; this ['prepareR' + n (401 ) + 'er' ](); const a = r ()[n (378 ) + n (293 )].pkcs7 [n (415 )]( r ()[n (276 ) + 's' ].utf8 [n (390 ) + n (267 )]( JSON [n (371 ) + n (292 ) + 'y' ]({ Timestamp : this .timestamp (), Host : e, RequestId : (0 , c.A )(), Referer : this [n (379 ) + n (396 )], Method : t }) ) ), o = this ['getO' + n (284 ) + 'tion' ]()[n (312 ) + 'ypt' ](a); return (0 , l.iI )(o) }
Payload 数据结构 我们通过断点拿到以下结构
1 2 3 4 5 6 7 { "Timestamp" : 1710000000000 , "Host" : "/api/User/OpenAuth" , "RequestId" : "随机UUID" , "Referer" : "https://i.jielong.com/" , "Method" : "POST" }
Payload 设计分析 可以看到 Host 和 Method 被纳入加密内容中,因此该 Payload 与具体接口绑定,无法直接复用于其他接口请求。 此外,Timestamp 字段也参与加密,说明该 Payload 同时具备一定的防重放能力。 结合 Host、Method 和 Timestamp,可以推测该字段主要用于防止请求被篡改或复用。
加密实现分析 首先我们先看看 c.A 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const c = function (e, t, n ) { if (r.randomUUID && !t && !e) return r.randomUUID (); const s = (e = e || {}).random ?? e.rng ?.() ?? function ( ) { if (!o) { if ('undefined' == typeof crypto || !crypto.getRandomValues ) throw new Error ( 'crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported' ); o = crypto.getRandomValues .bind (crypto) } return o (i) }(); if (s.length < 16 ) throw new Error ('Random bytes length must be >= 16' ); if (s[6 ] = 15 & s[6 ] | 64 , s[8 ] = 63 & s[8 ] | 128 , t) { if ((n = n || 0 ) < 0 || n + 16 > t.length ) throw new RangeError (`UUID byte range ${ n } :${ n + 15 } is out of buffer bounds` ); for (let e = 0 ; e < 16 ; ++e) t[n + e] = s[e]; return t } return a (s) }
很明显这是一个标准的 UUID v4 生成实现。
通过组合 o 变量得到 getOperation,查找找到了这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 getOperation ( ) { const e = P; if (v.O ) { const t = e (335 ) + e (364 ) + '4wvo' + e (417 ) + '59sA' + e (272 ) + e (301 ) + 'g1UX' , n = e (257 ) + e (257 ) + 'AAAA' + e (257 ); return new (r ()[e (334 ) + e (416 ) + e (299 ) + 'ion' ][e (304 )])( r ()[e (276 ) + 's' ][e (241 )][e (390 ) + e (267 )](t), r ().utils [e (241 )]['toBy' + e (267 )](n) ) } { const t = e (250 ) + e (306 ) + e (269 ) + e (400 ) + 'PFW3' + e (338 ) + e (388 ) + 'Xo1v' , n = 'AAAA' + e (257 ) + e (257 ) + e (257 ); return new (r ()[e (334 ) + e (416 ) + e (299 ) + e (319 )][e (304 )])( r ()[e (276 ) + 's' ][e (241 )][e (390 ) + e (267 )](t), r ()[e (276 ) + 's' ].utf8 [e (390 ) + e (267 )](n) ) } }
通过 n 和 t 组合而成的,可以确认这里使用的是 AES-CBC 模式,填充方式为 PKCS7,需要 key 和 iv 进行加解密。 然后通过断点拿到了它们的值。
1 2 n: "AAAAAAAAAAAAAAAA" t: "Rdb6DqK2erQtm7OZPFW3gVl5MILCXo1v"
接下来看 l.iI,跳转过去,是一个 base64 的编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 t.iI = function (e ) { for (var t, r = e.length , o = r % 3 , i = [], s = 16383 , a = 0 , l = r - o; a < l; a += s) i.push (c (e, a, a + s > l ? l : a + s)); 1 === o ? (t = e[r - 1 ], i.push (n[t >> 2 ] + n[t << 4 & 63 ] + '==' )) : 2 === o && ( t = (e[r - 2 ] << 8 ) + e[r - 1 ], i.push (n[t >> 10 ] + n[t >> 4 & 63 ] + n[t << 2 & 63 ] + '=' ) ); return i.join ('' ) }; for ( var n = [], r = [], o = 'undefined' != typeof Uint8Array ? Uint8Array : Array , i = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' , s = 0 ; s < 64 ; ++s ) n[s] = i[s], r[i.charCodeAt (s)] = s;
Payload 复现 至此,X-Api-Request-Payload 的完整生成流程可以总结如下:
1 2 3 4 5 6 7 JSON.stringify ↓ UTF-8 编码 ↓ AES-CBC 加密 ↓ Base64 编码
我们简单用 Python 实现一下,看看结果是否一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import jsonimport base64import timeimport uuidfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import padKEY = b"Rdb6DqK2erQtm7OZPFW3gVl5MILCXo1v" IV = b"AAAAAAAAAAAAAAAA" def get_payload (path, method ): data = { "Timestamp" : int (time.time() * 1000 ), "Host" : path, "RequestId" : str (uuid.uuid4()), "Referer" : "https://i.jielong.com/" , "Method" : method } raw = json.dumps(data, separators=(',' , ':' )).encode() cipher = AES.new(KEY, AES.MODE_CBC, IV) encrypted = cipher.encrypt(pad(raw, AES.block_size)) return base64.b64encode(encrypted).decode()
经过确认,结果一致,证明我们已经完整还原了 Payload 的生成逻辑。 在获取 Authorization 后,结合该算法即可稳定构造请求并调用业务接口。
总结 到这里基本可以把整个流程串起来了:
登录流程为标准微信扫码登录,核心在于获取 wx_code 并换取 Authorization
X-Api-Request-Payload 已经可以完整复现,包括内部数据结构和加密流程
Payload 会绑定接口路径和请求方法,同时带有 Timestamp,因此无法跨接口复用
实际测试中,只要自行生成 Payload 并配合 Authorization,就可以稳定请求接口,不再需要依赖抓包获取参数。
完整流程已验证可自动化实现,这里不再展开。
参考 [1] https://developers.weixin.qq.com/doc/oplatform/developers/dev/auth/h5.html [2] https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html