付费进群系统源码泄露怎么办:订单号是拼的

先说结论,省得你翻到最后。

如果你的系统是「付款成功后才给网盘链接 / 兑换码」这种玩法,先去把订单号的生成逻辑看一眼。只要它是几段固定值拼起来的 —— 比如分站 ID、群组 ID、再加一个时间戳 —— 那你手上这套东西等于没锁。有人拿几百个请求就能把所有已付款的链接拉走,不用登录,不用付费,也不用碰你的数据库。

我自己那套付费进群源码就是这样被自己翻出来的。

我一开始怀疑是模板目录没关

先说排查过程,因为一开始我完全跑偏了。

源码泄露这个事,站长第一反应基本都是「是不是 .git 没删、备份文件放在网站根目录、runtime 缓存能被直接访问」。这三样我也查了。

ls -la /www/wwwroot/ 站点目录/runtime/temp/ | head -30
find 站点目录 -name "*.sql" -o -name "*.zip" -o -name "*.bak" | head
curl -sI https://站点域名/.git/config | head -1

第一条列出来的是一堆编译后的 PHP 模板缓存,看不出文件名规律。第二条啥也没搜到。第三条返回 404。这三条路全断。

到这里我以为没事了 —— 源码在服务器上,外人拿不到。

alt:五轮排查对照表,前三轮都判成没事,第四轮才看见真问题
alt:五轮排查对照表,前三轮都判成没事,第四轮才看见真问题

第二轮怀疑是校验写松了

接着我去看订单这条链路到底卡在哪几道门。这套系统是 ThinkPHP 写的,付款成功之后跳到一个页面,页面里输出网盘链接内容。

// application/group/controller/Index.php:371
// 校验在这里:单号要对上,状态还得是已支付
$info = $db->where('bl_sncode', $sncode)->where('bl_status', 2)->find();

看着没问题。裸访问一个没付钱的单号,这里就挡住了。

还有一道更漂亮的:支付成功后种一个 cookie,里面是订单 ID、过期时间和一个 HMAC 签名,密钥在服务端。这一套是防「没付款直接刷成功页」的,24 小时有效期,伪造不出来。写这段代码的人是想得很周全的。

所以到这一步我心里的结论是:这套东西拦得住。

真凶在订单号自己身上

不对的地方在这儿 —— 生成订单号的这一行:

// application/group/controller/Index.php:227
$sncode = $dinfo['su_id']."_".$id."_".time();

分站 ID、群组 ID、秒级时间戳,三段拼起来当订单号。

三段全是能猜的。群组 ID 那 30 个是 176 到 205,页面上本来就能看到;时间戳就更好猜了,一个付款高峰日内循环几万个整数;分站 ID 自己站就一个。校验再严也没用 —— 它要拿这个单号去数据库里查,查得到就吐内容。这不是绕过校验,是喂给校验一个正确答案。

到这里已经能定性了:不是源码泄露,是链接被白嫖。

alt:订单号三段组成对照表,分站 ID、群组 ID、time() 三段全部可猜
alt:订单号三段组成对照表,分站 ID、群组 ID、time() 三段全部可猜

直接经济损失其实接近零

我得说清楚,免得读者学完去跟人打官司。

网盘链接这个东西,谁拿到都能用。而且它是一码一物 —— 同一批人看到的本来就是同一段文本,我那套系统里连库存表都没有,更没有发货记录。别人白嫖走的链接,我自己也能拿去用。

真正的麻烦在别处:我卖的是系统,客户拿去卖他东西。东西被白嫖,客户会来找退款。 赔的不是那几块钱,是信誉。

还有一条我一直没查完,留个提醒:几个站是不是用了同一个签名密钥,我到现在没确认。如果是一样,那支付回调就能互相伪造,那比订单号可猜严重得多。这块我还没动手,先记着。

改一行就够,但要记得清缓存

$sncode = $dinfo['su_id']."_".$id."_".time()
        ."_".substr(md5(uniqid(mt_rand(), true)), 0, 6);

尾部加六位随机。uniqid 加微秒和随机种子,md5 之后截六位,二十三个字符,易支付那边要求的三十二字符以内还够用。

改完必须清 ThinkPHP 的模板缓存,不然文件在磁盘上、缓存里还是老的,行为不变 —— 这条我被坑过一次,就是那次做部署安全网的时候发现的,[部署PHP到生产站的三道安全网](https://www.budingwz.cn/php-deploy-safety-nets.html) 里写的就是这个。

rm -f 站点目录/runtime/temp/*.php
ls -l 站点目录/runtime/temp/ | head

清完再自己下一单验证:新单号的长度变了,就说明走的是新代码。

还有一点,别指望这个改完就万事大吉。它只是把门锁拧紧了一圈,门框松不松是另一回事。这跟「接口返回成功」不代表「对方真的处理了」是一个道理 —— 推送成功不等于收录,我这篇说的也是同一件事,只是方向反过来。

改完我又去查了一遍群组 ID 的分布,发现自己还漏了一处:后台导出订单那个接口没走同一个生成函数。这个是后来补的,不在第一次的修复里。

发表评论