从解密验签到任意用户登录漏洞的一次真实案例
渗透测试
系统使用了Sign签名校验来防范重放攻击,sign签名采用多层编码的防护方式导致测试难度增加,通过前端动态调试解密签名的编码方式,而后利用flask server配合burpsuite的python脚本实现自签名实现签名绕过;后续测试发现系统某个接口存在缓存漏洞,通过多次重放某个接口数据包数千次可以返回多个在线用户的session会话信息,利用这些会话信息可以登录对应用户,以此实现任意用户登录的效果
前言: 系统使用了Sign签名校验来防范重放攻击,sign签名采用多层编码的防护方式导致测试难度增加,通过前端动态调试解密签名的编码方式,而后利用flask server配合burpsuite的python脚本实现自签名实现签名绕过;后续测试发现系统某个接口存在缓存漏洞,通过多次重放某个接口数据包数千次可以返回多个在线用户的session会话信息,利用这些会话信息可以登录对应用户,以此实现任意用户登录的效果 漏洞前提: 至少有一个低权限用户 漏洞发现难度: 中等 签名校验,是防护重放攻击的重要手段;常规签名校验有大致两种表现形式:一种是不能修改请求体参数,但是可以原样重放数据包;第二种是签名使用一次立即失效 本次笔者遇到的场景便是第二种,签名使用一次立即失效;一般这种情况都是因为系统加入了timestamp相关从一次性因子,因此请求在使用一次后立即失效  一般情况下,签名校验大多数都是通过前端生成的,因此可以通过尝试在前端进行js动态调试去观察签名的生成方式;当然了签名的生成方式并非是绝对的,也有一些安全系数更高的系统的签名生成确实是在后端生成的,对于后端生成的那种成本较高,对系统的负载要求也更高,因此不是特别常见,只适用针对某些特定请求的验签使用场景。 当然了,还有一些金融常用的:前端签名+风控上下文,大致效果如下: 签名 = HMAC(参数 + sessionId + deviceId + riskScore) 其中: - `sessionId`、`deviceId` 由后端下发 - `riskScore` 来自风控系统 - 即使签名算法被还原,换设备 / 换账号也会失效 这在金融 App、大厂开放平台里非常常见。 扯远了,下述为常见的第二种验签的大致流程: 客户端 服务端 │ │ │ 1. 生成 nonce = UUID │ │ 2. 生成 timestamp = now() │ │ 3. 计算 signature │ │ 4. 发送请求 ────────────────▶│ │ │ │ │ 5. 校验 timestamp 在窗口内 │ │ 6. 查 Redis: nonce 是否存在? │ │ ├─ 存在 → 拒绝(重放攻击) │ │ └─ 不存在 → 继续 │ │ 7. 重新计算 signature 比对 │ │ 8. 校验通过 → 处理业务 │ │ 9. Redis SET nonce "1" EX 300 │ │ │ ◀──────────────── 返回结果 │ 本次项目通过堆栈追踪的方式,很容易就找到了验签的点位,首次追踪到的代码如下:  `const n = (new Date).valueOf() 时间戳` `i.default.ls.get(r.k); #r.k=Access-Token 这里获取的是登录后的Authorization: Bearer efe1axxxxxaeb8` 后续追踪参数如下: `return o = m(t + "," + n + "," + a), # t值为请求接口的uri,n是时间戳,a是efe1aaxxxxx62aeb8` 进入到o = m(t + "," + n + "," + a)函数,发现首先进行拼接  再次进入下一层,发现是hmac-sha-SM3,且key是硬编码3c444xxxxxxdf4cc  不知道为啥粘进来没格式了,凑合看吧 `function g(e, t) { if (e = "string" == typeof e ? p(e) : Array.prototype.slice.call(e), t) { if ("hmac" !== (t.mode || "hmac")) throw new Error("invalid mode"); let n = t.key; if (!n) throw new Error("invalid key"); return n = "string" == typeof n ? p(n) : Array.prototype.slice.call(n), f(function(e, t) { for (t.length > 64 && (t = u(t)); t.length < 64; ) t.push(0); const n = l(t, h) , i = l(t, d) , r = u([...n, ...e]); return u([...i, ...r]) }(e, n)) } return f(u(e)) } const m = e=>g(e, { key: "3c44xxxx4cc" });` 输出的位置在return f(u(e)),所以先调用u(e)函数,然后调用f()函数,因此需要在 r = u(\[...n, ...e\]);的位置步入看看里面的内容 `function u(e) { let t = 8 * e.length , n = t % 512; n = n >= 448 ? 512 - n % 448 - 1 : 448 - n - 1; const i = new Array((n - 7) / 8) , r = new Array(8); for (let a = 0, o = i.length; a < o; a++) i[a] = 0; for (let a = 0, o = r.length; a < o; a++) r[a] = 0; t = t.toString(2); for (let a = 7; a >= 0; a--) if (t.length > 8) { const e = t.length - 8; r[a] = parseInt(t.substr(e), 2), t = t.substr(0, e) } else t.length > 0 && (r[a] = parseInt(t, 2), t = ""); const l = new Uint8Array([...e, 128, ...i, ...r]) , u = new DataView(l.buffer,0) , h = l.length / 64 , d = new Uint32Array([1937774191, 1226093241, 388252375, 3666478592, 2842636476, 372324522, 3817729613, 2969243214]); for (let g = 0; g < h; g++) { a.fill(0), o.fill(0); const e = 16 * g; for (let o = 0; o < 16; o++) a[o] = u.getUint32(4 * (e + o), !1); for (let o = 16; o < 68; o++) a[o] = (f = a[o - 16] ^ a[o - 9] ^ s(a[o - 3], 15)) ^ s(f, 15) ^ s(f, 23) ^ s(a[o - 13], 7) ^ a[o - 6]; for (let s = 0; s < 64; s++) o[s] = a[s] ^ a[s + 4]; const t = 2043430169 , n = 2055708042; let i, r, l, h, p, m = d[0], v = d[1], y = d[2], b = d[3], x = d[4], _ = d[5], w = d[6], S = d[7]; for (let u = 0; u < 64; u++) p = u >= 0 && u <= 15 ? t : n, i = s(s(m, 12) + x + s(p, u), 7), r = i ^ s(m, 12), l = (u >= 0 && u <= 15 ? m ^ v ^ y : m & v | m & y | v & y) + b + r + o[u], h = (u >= 0 && u <= 15 ? x ^ _ ^ w : x & _ | ~x & w) + S + i + a[u], b = y, y = s(v, 9), v = m, m = l, S = w, w = s(_, 19), _ = x, x = c(h); d[0] ^= m, d[1] ^= v, d[2] ^= y, d[3] ^= b, d[4] ^= x, d[5] ^= _, d[6] ^= w, d[7] ^= S } var f; const p = []; for (let a = 0, o = d.length; a < o; a++) { const e = d[a]; p.push((4278190080 & e) >>> 24, (16711680 & e) >>> 16, (65280 & e) >>> 8, 255 & e) } return p }` d = new Uint32Array(\[1937774191, 1226093241, 388252375, 3666478592, 2842636476, 372324522, 3817729613, 2969243214\]);这个初始化向量属于SM3固定写法 <https://www.btool.cn/hmac-generator> 使用在线站点验证下  继续往后走 `o = o + "," + n + "," + a + "," + C(),` 这里没什么可说的,属于hash加盐的步骤,又重新加了一遍,C()是UUID  `o = Object(v.b)(o)` 最后一层加密是SM4加密 `const i = "GJwxxxxJW" , r = new (0, n("85d2").sm4)({ key: i, mode: "cbc", iv: i, cipherType: "base64" }) , a = e=>r.encrypt(e) , o = e=>r.decrypt(e) },` 可以看到,这里的key和iv都是写死的,并且是CBC的,因此后边就不用看了,想看跟进r.encrypt(e)也可以 由此可以确定解密流程 `Step 1: 构造基础串 base = path + "," + timestamp + "," + token Step 2: 计算签名 sign = HMAC-SM3(base, fixed\_key) Step 3: 二次拼接 payload = sign + "," + timestamp + "," + token + "," + uuid Step 4: 对称加密 cipher = SM4-CBC(payload, dynamic\_key, dynamic\_iv) Step 5: Base64 final = base64(cipher) Burp插件(Jython) ↓ HTTP 本地 Python Flask 服务(gmssl) ↓ 返回:Sign / Encrypttype` 编写简单的flask server代码后运行如下  burpsuite插件安装如下  此时burpsuite已经可以做到抓包改包重放了  以上仅做到了测试的基础前置条件,接下来开始正常测试,重新进行登录,发现登录请求返回包存在多种不同的uri地址,是的,不是多个,是多种,一种像是后台自定义的短路由,一种像是后台代码里的正常路由   进入到后台是这个样子的  可以看到,其实是没有什么用户管理之类功能的,但是在apirights列表里笔者看到了很多的关于用户的东西,于是大胆的对接口进行了fuzz,fuzz后发现果然是看到了一些奇怪的东西,,其中一个接口泄露了其他用户的用户名和密码,并且密码字段与登录请求密码字段是一致的  尝试使用登录接口对获取的账户密码进行测试,很可惜,存在二次登录认证  而后发现fuzz的接口里有一个退出登录接口,但是退出接口已经执行了,按理说会话也注销了,后面还在fuzz的接口依然没有被影响,说明此处存在会话滞留漏洞,但是目前这个洞并没有什么卵用 于是继续翻看接口,其中有一个接口吸引了我的注意力  这个接口并不是登录接口,但是他却返回了和登录成功请求的数据返回包几乎一样的数据,于是笔者将这个数据包发送至重放界面,本打算像测试一下重放,结果却出人意料,第一次重放依然是test1用户  再次重放,数据内容竟然发生了变化  这就很奇怪,于是反复的将数据包重放,发现重放的次数越多,蹦出来的其他用户的东西记录越大,于是直接扔intruder里进行枚举10000次,果然不出所料,成功的拿到了一个信息管理员的用户模版  拿到这个的比这想的第一个打法,就是重新登录,输入任意用户和密码,然后直接构造返回包,把这个数据包整个贴进去,然后再伪造一条set-cookie:JSESSIONID,这样又能任意用户登录,又能绕过二次认证,还不用知道密码 模拟正常登录,用户名密码乱写  替换掉拿到的高权限用户模版,手动把JSESSIONID构造进去  构造结束后,一个包一个包的过,然后发现进入到了后台,此时数据还没加载出来,因为数据包是一个一个走的  但是数据包放着放着,笔者发现莫名其妙的就退出去了  按理说页面都加载进来了,不应该会退出去,随便找一个401状态码的数据包转发到repeater  这个时候发现,他的header头里面的cookie这么长一串,并且似乎有两个JSESSIONID,怀疑是不是这里冲突了,也可能是那个LSESSIONID有问题,于是把后边的都给他删了,重新发包,发现有数据了  因此结论大概就是请求体冲突,这个问题很好解决,直接利用burpsuite的替换功能,把cookie做一个全局替换  再次模拟登录,这个时候,神奇的一幕发生了  好吧,最后发现其实是系统缓存问题,只需要把浏览器关了重启,然后直接替换请求就能实现任意用户登录的操作; 经过对接口的多次请求,笔者尝试重复请求10000次、20000次发现,返回的用户数据都是单用户的,甚至有的时候会把别的用户的密码也返回出来,并且一个用户数据可能会重复几百次几千次,于是笔者有了大胆的猜测: - 当前线程残留用户 - 缓存污染 - Session 获取异常 - 网关上下文串号 - 或分布式鉴权状态错乱 结束,下机,漏洞目前已经修复了,但是没问客户是用什么手法修复的,也不知道漏洞成因具体是哪一种,你们遇到过这种缓存漏洞吗?
发表于 2026-08-20 09:46:46
阅读 ( 4902 )
分类:
渗透测试
8 推荐
收藏
0 条评论
vlan911
7 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!