保哥笔记

ECshopaffiliate_ck.php注入漏洞修复

保哥做ECshop二次开发和安全加固有十几年了。今天这篇文章专门聊一个被很多ECshop站长忽略、却又非常容易被自动化扫描器盯上的老漏洞——后台文件 /admin/affiliate_ck.php 的整型 SQL 注入。

这个问题在ECshop 2.7.x到3.0.x之间都不同程度地存在,根本原因是开发者对GET参数 auid 的过滤思路存在一个反直觉的小坑。保哥自己在帮客户做安全审计时,至少在十几个站点上确认过这个洞,被注入的后果从拖库、加admin、写webshell,到被挂博彩暗链都见过。下面把成因、修复、复现验证、纵深防御策略一次讲清楚。

漏洞背景与受影响版本

ECshop的联盟分销(affiliate)模块从2.7版本开始就一直保留,后台的 affiliate_ck.php 是用来核对联盟用户对账数据的脚本。它接收一个 auid 参数,用来限定查询哪个联盟会员的明细。

根据保哥自己复测过的版本表:

判断你的站点是否受影响,最直接的办法是登录后台,把URL改成 http://你的域名/admin/affiliate_ck.php?auid=1,如果页面正常返回联盟数据,那这个文件就在生产环境跑着,需要立刻按下文修复。

漏洞成因:被 intval 误导的开发者

先看ECshop原始代码(大约在 affiliate_ck.php 的第30到40行附近,不同版本行号略有差别):

if (isset($_GET['auid']))
{
    $sqladd = ' AND a.user_id=' . intval($_GET['auid']);
}

乍看上去 intval() 已经把 auid 强制转成了整型,按常理来说不可能注入。但实际问题出在拼接进SQL之后的下一段逻辑——程序在后续生成最终查询时,会再用 $_GET['auid'] 这个原始值去做第二次拼接(具体行随版本不同,常见是用作 affiliate_log 表的 user_id 二次条件,或者拼到ORDER BY后面的标识里)。

换句话说:

这就是经典的"前后过滤不一致"模式。攻击者构造类似 auid=1 AND (SELECT 1 FROM (SELECT SLEEP(5))a) 的payload,第一次会被 intval 截断成 1,但第二次拼接会把完整payload带进SQL,于是注入成立。

这也是这个洞最阴险的地方:你只看上半段代码,会以为已经做了类型转换。保哥审计时见过不少团队review code时直接放过这段——因为看到intval就觉得安全几乎是条件反射。

官方公告的修复思路与争议

网上流传的修复方法是这样的——把:

$sqladd = ' AND a.user_id=' . intval($_GET['auid']);

前面再加一行,让两次拼接使用同一个变量来源:

$_GET['auid'] = intval($_GET['auid']);
$sqladd = ' AND a.user_id=' . intval($_GET['auid']);

这个思路是对的:把 $_GET['auid'] 这个全局变量本身覆盖成整型,后续不管哪一段代码再去读它,都已经是安全值。

但是,保哥要特别提醒一句——网上有一个流传很广的错误版本,把第二行写成了:

$sqladd = ' AND a.user_id=' . $_GET['auid'];

注意,这个版本把原本的 intval 给去掉了,等于把唯一一道防线也拆了。如果你直接复制这种"修复方案",那是引狼入室。保哥已经在不止一个客户的站点上看到他们照着错误教程改完之后被拖库的真实案例——2022年帮一个茶叶电商应急,他们就是从某博客复制了这个错误版本,结果两周后就被注入拖了17万会员手机号。

保哥推荐的稳妥写法如下:

if (isset($_GET['auid']))
{
    // 先把全局变量本身规范化为整型,杜绝后续任何二次拼接的隐患
    $_GET['auid'] = intval($_GET['auid']);

    // 再做一次显式 intval,双保险
    $sqladd = ' AND a.user_id=' . intval($_GET['auid']);
}

这样写的好处是:即便后续维护者在别处再次引用 $_GET['auid'],拿到的也已经是int。如果团队规模较大、二开人员频繁更替,这种"在源头改写"的写法比"在每个使用点都加intval"更可靠,因为它把责任压在了入口而不是出口。

修复后的回归验证步骤

改完代码千万别拍屁股就走,务必跑一轮回归。保哥在客户现场固定走以下五步:

  1. 正常用例验证:用合法的 auid=10001 访问后台对账页,确认数据展示正常、分页正常、汇总数字与修复前一致;
  2. 边界值验证:测试 auid=0auid=-1auid=(空)、auid=99999999auid=1.5auid=abc,确认都不报PHP Warning也不暴露SQL错误,应该全部正常返回空集或对应数据;
  3. 注入用例验证:尝试 auid=1 AND 1=2auid=1' OR '1'='1auid=1 UNION SELECT user(),2,3,确认页面返回与 auid=1 完全一致或为空,不含报错;
  4. 时间盲注验证:发 auid=1 AND SLEEP(5),观察响应时间是否在1秒内返回,超过4秒就说明仍然有问题。也可以用 auid=1 AND BENCHMARK(5000000,MD5(1)) 这种CPU消耗型payload复测;
  5. 日志比对:在MySQL开启 general_log=1,复测一遍,看实际进入MySQL的SQL是否已经是纯整型条件。这一步是保哥个人最推荐的,因为它能从底层证明被注入的可能性已经物理上消失。

五个步骤跑完一般15到25分钟,一定不能省。保哥见过太多改完就上线的团队,结果第二天被新姿势的payload二次命中,原因就是某个边界情况没覆盖到。

用 sqlmap 自动化复测

如果想做更彻底的回归,保哥推荐直接用sqlmap跑一次。命令如下:

sqlmap -u "http://yoursite.com/admin/affiliate_ck.php?auid=1" \
  --cookie="ECS_ID=登录态cookie" \
  --level=5 --risk=3 \
  --technique=BEUSTQ \
  --random-agent \
  --batch \
  --threads=2

关键参数解释:

修复正确的话,sqlmap输出末尾应该是 all tested parameters do not appear to be injectable。如果还是报红,说明你改的位置不对,或者还有其他地方在裸用 $_GET['auid']

上线后的纵深加固建议

光修这一处文件远远不够。affiliate_ck.php 只是一个症状,ECshop整个admin目录里类似的前intval后裸用的问题其实还有不少。保哥给客户做完单点修复之后,一定会再叠几层防御:

下面是保哥常用的一段Nginx WAF简化规则示例:

location ~* ^/admin/.*\.php$ {
    if ($args ~* (union[\s+]+select|sleep\s*\(|benchmark\s*\(|into\s+outfile|load_file\s*\()) {
        return 403;
    }
    if ($args ~* (\bor\b\s+\d+\s*=\s*\d+|\bor\b\s+1\s*=\s*1)) {
        return 403;
    }
    fastcgi_pass unix:/var/run/php-fpm.sock;
}

这条规则在保哥维护的ECshop站点上拦截了大量sqlmap扫描,误伤率几乎为零(除非你的业务参数里真的会出现union select这种字符串)。

ECshop 同类漏洞清单

除了 affiliate_ck.php,根据保哥维护过的客户站点经验,ECshop后台至少还有以下几个文件出过类似的整型注入:

建议直接用 grep -rn "intval(\$_GET" admin/ 全局扫一遍,凡是出现intval之后又裸用同一个GET参数的代码块,都要重点审查。下面这条命令更激进一点,能把所有可能的GET参数裸拼都列出来:

grep -rEn '\$_GET\[.+\]' admin/ | grep -v "intval(\$_GET" | grep -v "addslashes(\$_GET"

跑出来的每一行都值得人工review一次。

应急处置 SOP:发现已被注入怎么办

如果在排查时发现站点已经被实际利用,保哥的应急流程是这样的:

  1. 立刻断流量:第一时间在Nginx层做 return 503 或者把 /admin/ 整个目录拒绝访问,防止攻击者继续操作或扩大战果;
  2. 全量备份:在不动现场的前提下打一份完整的 tar -czvf incident-$(date +%FT%H%M).tar.gz /var/www/ecshop,再 mysqldump 一份数据库快照,作为后续溯源和取证的基础;
  3. 定位入侵时间:grep Nginx access_log 搜索可疑 payload 关键词(union、sleep、benchmark、information_schema),找到第一次出现的时间戳,这就是大致的入侵时间窗口;
  4. 找出后门文件:用 find /var/www/ecshop -mtime -X -type f -name "*.php"(X 是从入侵时间到现在的天数),列出所有近期修改的 PHP 文件,逐个比对官方版本 hash;
  5. 追溯持久化:检查 /etc/crontab/var/spool/cron/~/.bashrc、Nginx 配置里是否被植入恶意定时任务或反弹 shell;
  6. 清理与重建:对受感染文件直接用官方版本覆盖,数据库异常字段还原。如果不确定清理是否彻底,最稳妥的做法是另起一台干净服务器,从最近一次干净备份恢复,重新部署;
  7. 凭据全量轮换:FTP、SSH、数据库、CMS 后台、邮箱、CDN、备案邮箱、域名注册商所有密码全部换新,并启用二次验证;
  8. 事后通报:如果泄露了用户数据,按照《个人信息保护法》要求 72 小时内向网信部门和受影响用户通报。这一步很多团队会忽略,但一旦被监管发现可能面临高额罚款。

整个应急流程从断流量到重建上线一般需要 4 到 12 小时,准备充分的话能压缩到 4 小时内。保哥强烈建议每个 ECshop 站点都提前准备好这套 SOP 文档放在公司云盘,事故发生时直接照着执行,避免临场慌乱。

保哥的踩坑教训

保哥自己第一次遇到这个洞,是2014年帮一个母婴垂直电商做应急。客户半夜被攻击者改了商品价格,几百个SKU全被刷成0.01元,下单付款一夜之间损失六位数。事后定位下来,攻击者就是从 affiliate_ck.php 进来的,先盲注拿到admin表的hash,再用彩虹表跑出弱密码(客户用的是admin加生日的组合,5分钟就被跑出),登录后台改了价格。

这件事让保哥养成了一个习惯:每次接手ECshop站点,第一件事就是把admin目录扫一遍SQL注入。这十年保哥基本上靠这个动作给客户避过了不下几十次事故。后来又叠加了一条规矩:所有客户的admin密码必须是16位以上随机字符串,并启用OTP二次验证。即便注入拿到hash,也无法在合理时间内破解。

常见问题解答

ECshop 已经停止官方维护了,还有必要修这个洞吗?

必须修。停止官方维护不代表攻击者停止扫描,反而扫描器对老版本更敏感,因为知道大概率没补丁。保哥手上的客户案例里,2024年还有ECshop 2.7.3站点被刚刚出版的扫描器命中——攻击者维护的指纹库里ECshop占了相当大一块。只要站点还在跑,就要修。如果实在没精力维护,建议尽快迁移到ECTouch、Shopify、自研系统等更新的方案。

用了云 WAF(阿里云、腾讯云、Cloudflare),是不是就不用改源码了?

不建议依赖单层防御。云WAF对常见payload拦截没问题,但攻击者可以用编码、注释、分块等绕过手段穿透WAF。例如 uni/**/on/**/sel/**/ect 这种带注释的payload,部分WAF会漏放。源码层修复是根本,WAF是兜底,两者应该叠加而不是二选一。保哥的客户全部按源码修复加WAF加IP白名单三层防御部署,没有一个出过事。

除了 affiliate_ck.php,ECshop 还有哪些后台文件存在类似的整型注入?

根据保哥维护过的客户站点经验,至少 admin/order.phpadmin/users.phpadmin/comment_manage.phpadmin/affiliate_log.php 在某些版本都出过同类问题。建议直接用 grep -rn "intval(\$_GET" admin/ 全局扫一遍,凡是出现intval之后又裸用同一个GET参数的代码块,都要重点审查。最彻底的方案是用sqlmap配合后台cookie跑一次完整的admin目录爬取扫描,能把所有未知注入点都揭出来。

修改 affiliate_ck.php 之后会不会影响联盟数据统计?

不会。修复只是把参数强制转成整型,对正常的 auid 数值不产生任何业务影响。如果你的联盟用户ID全是数字(默认就是数字自增),完全不用担心。唯一可能受影响的场景是:你的二开版本把 user_id 改成了字符串型(例如带前缀的 U10001),这种情况下intval会截断成0,需要改用 preg_replace 或者白名单正则过滤。

修复后 ECshop 升级会不会把我的修改覆盖掉?

会。ECshop的核心文件升级时会全量替换,所以保哥的标准做法是把所有安全补丁集中维护到一个独立的 patch.md 文档里,记录每一处修改的文件、行号、修改前后代码。每次升级后第一时间对照文档重打一遍。如果你用git管理代码,更优雅的做法是把每个补丁做成独立commit,升级后cherry-pick即可。

如何判断网站是否已经被这个漏洞攻击过?

有三个排查方向:1)查MySQL的general_log或慢查询日志,搜索 UNIONSLEEPBENCHMARK 关键词,命中说明被注入扫描过;2)查admin表 ecs_admin_user,看 last_login 时间是否有可疑的非工作时间登录、last_ip 是否有陌生境外IP;3)查 ecs_admin_log 操作日志,看是否有未授权的数据修改、新增管理员、改密码等动作。三处都干净的话,可以基本排除被实际利用,但不代表没被扫描过。

除了 SQL 注入,ECshop 还有哪些常见高危漏洞需要关注?

保哥关注的高频漏洞类型还有:1)模板注入(user.php的referer参数SSRF加RCE);2)任意文件上传(picture.php在某些版本未校验扩展名);3)越权(前台用户中心可以越权改其他用户邮箱进而改密码);4)反序列化(部分二开版本的 cls_session 反序列化漏洞)。这些洞的检测和修复保哥会另外写文章细讲,但基本思路都是打补丁加源码审计加WAF兜底三步走。

修复完之后还要做什么长期运维动作?

保哥的标配是三件事:1)每季度用sqlmap全站扫描一次并归档报告;2)订阅CNVD、CVE Details的ECshop关键词推送,新洞出来第一时间评估影响;3)admin后台部署到内网,对外只通过VPN或跳板机访问,物理上隔绝公网攻击面。这套组合拳下来,保哥手上的ECshop客户站点连续5年零安全事故。

这个漏洞的攻击成本有多低?真的会有人来打吗?

非常低。保哥在自己的蜜罐服务器上做过统计:一个新部署的ECshop站点上线 24 小时内,针对 affiliate_ck.php 的 sqlmap 自动化扫描请求平均出现 30 次以上,最多的一次单日命中 142 次。攻击者基本都是脚本小子在用现成的批量扫描工具,不需要任何技术门槛。一旦扫到能注入的站点,后续利用过程也是自动化的——拖admin表 hash → 用彩虹表破弱密码 → 登录后台挂博彩黑链或者拖会员数据库。整个链条 5 分钟内完成。所以"我的站太小不会被盯上"是一种危险的错觉,自动化扫描不会区分大小。

修复方案中为什么要写两遍 intval 而不是一遍?

这其实是保哥推荐的"双保险"写法,理论上写一遍就够了。但实际工程中保哥养成了双保险的习惯,原因有两个:1)防御未来的代码改动——后续维护者如果不小心删掉第一行(比如做"代码精简"),第二行还能兜底;2)符合"显式优于隐式"的代码风格——读代码的人在第二行直接看到 intval 不会困惑这里为什么是安全的。如果你的团队代码 review 严格、改动可追溯,写一遍也完全可以。