jy无感取巧思路

查看 53|回复 10
作者:utf8   
jy无感取巧思路

目标网站:aHR0cHM6Ly9ndDQuZ2VldGVzdC5jb20v

本文纯粹取巧!!!对小萌新很不友好
本文仅供学习交流,因使用本文内容而产生的任何风险及后果,作者不承担任何责任,一起学习吧
如有错误,请大佬们指出
代码均脱敏,如有侵权,请及时联系作者删除
目标确认
记得先将网站的配置设置为无感(一键通过)

目标接口:login

响应确认(success and fail )
请求成功返回数据如下:
{
    "result": "success",
    "reason": "",
    "captcha_args": {
        省略
    },
    "status": "success"
}
请求失败返回数据如下:
{
    "result": "fail",
    "reason": "pass_token used",
    "captcha_args": {},
    "status": "success"
}
负载分析
[ol]
  • captcha_id
  • lot_number
  • pass_token
  • gen_time
  • captcha_output
    [/ol]
    以上参数均来自接口verify(直接搜就知道),那么逆向目标改变为这个接口。还是一样负载分析(搜索大法):
    如下图:


    1.png (157.8 KB, 下载次数: 0)
    下载附件
    2025-4-16 18:56 上传

    [ol]
  • 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: 一大串不用看,肯定是逆向点
    [/ol]
    分析接口load,负载如下图:


    2.png (15.45 KB, 下载次数: 0)
    下载附件
    2025-4-16 18:56 上传

    [ol]
  • callback: 这个定死无所谓,本质就是一个geetest_ + 时间戳
  • captcha_id: 这个一个js文件里面,根据不同的验证类型,他的id也不同。这个也可以定死。
  • challenge: 搜不到,可能是加密参数
  • client_type: 使用平台类型web啥的
  • risk_type: ai
  • lang: 非重要参数定死即可,使用语言
    [/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]
  • device_id :为空
  • lot_number :load包里面有
  • pow_msg: 能搜到,不多,全部打上断点
  • pow_sign:能搜到,不多,全部打上断点
  • geetest: 非重要参数
  • lang:非重要参数
  • ep:非重要参数
  • biht:暂时不清楚
  • gee_guard,So89,2e424091,em:暂时不清楚
    [/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]
  • So89 定值,我刷新了几次没变
  • 2e424091 --> 变值,自己本身就是,这个参数就在上面可以直接看到
    [/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]
  • _ 是 随机key通过rsa加密
  • u 是 之前的入参和随机key 进行aes加密
  • 最后将两个结果合并返回。
    [/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 上传

    成功了,不用扣代码了。爽!不过这个扣的难度不高。认真一点没啥问题。
    最后的结果就是--> 两个结果合并返回。
    最后测试一下,嗯哼,没毛病。无感相较于滑块简单点。滑块还有轨迹收集。

    下载次数, 下载附件

  • utf8
    OP
      


    star0angel 发表于 2025-4-17 11:40
    前面看得懂  后面看的就有些云里雾里的了

    后面大部分都是逆向经验,对加密算法很熟悉,所以看到特征就可以大胆猜测。这个直接扣代码也是可以做的。
    zhouxinhu   

    我嘞个豆,这样也可以
    夜游星河   

    不明觉厉!向大佬致敬
    xiaohan231   

    学习了,真的强
    lx2018   

    太厉害了666666666666666
    JOJO996   

    受教了 感谢大佬!
    anning666   

    大佬牛叉,小弟学习一下,tks
    yilong88888   

    楼主辛苦   大神膜拜
    ZeLin99   

    太厉害了哥,我学到了学到了
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部