网络攻防无处不在:我的RustDesk服务竟成了DDoS的一环

作者发现云服务器因RustDesk服务被用于UDP反射放大攻击,导致公网流量欠费。经分析,攻击者向RustDesk的21116端口发送伪造PunchHoleSent消息,诱导服务端向目标发送PunchHoleResponse包,造成DDoS攻击。作者通过限制入口IP和出口速率缓解问题,并强调监控告警和账户余额管理的重要性。

文章目录

突如其来的欠费

前几日下午,照常在工位上的我突然收到云服务商的欠费通知,不由得警觉起来。笔者在云上只有一个公网流量按量付费的服务器,虽然预留的余额不多,但是服务器上平时也没什么流量,按理不会用的这么快。于是我赶紧登录上去看看是什么情况。

一看费用账单,果真都是公网流量费用,半天消耗了快10GB,一看监控,外网出入带宽居高不下。

带宽监控

但是一时半会还找不到原因,故先紧急止血:临时降低服务器带宽,避免继续快速消耗流量。

初步定位:RustDesk UDP 端口流量激增

遇到这种突发状况,我才发现自己对服务器运维知之甚少,不知道到底是哪个进程、哪个端口消耗了流量,也无从下手,只好赶紧求助运维同事。该说不说不愧是运维大哥,安装好 iftop 工具,三下五除二就定位到如下链路(均已脱敏):

172.16.0.7:21116 => ip-69-33-x-a.iad.megapath.net:25565 1.10Mb 1.25Mb 696Kb
<= 0b 0b 0b
172.16.0.7:21116 => ip-69-33-x-b.iad.megapath.net:25565 1.21Mb 635Kb 605Kb
<= 0b 0b 0b
172.16.0.7:21116 => ip-69-33-x-c.iad.megapath.net:25565 1.03Mb 577Kb 563Kb
<= 0b 0b 0b

iftop 工具中,右侧三列分别为最近2秒、10秒、40秒的平均速率,可以看出,服务器21116端口在不断往 69.33.x.x/24 网段的25565端口发送数据。

21116是 RustDesk 服务端(一个开源的远程桌面服务)hbbs 程序使用到的一个核心端口。RustDesk 服务端部署分为 hbbshbbr 两个程序,前者用于 ID 注册、心跳、P2P 打洞等核心功能,后者用于 P2P 打洞失败时走服务器中继转发。我通过智能插座 + 电脑通电自启 + 自部署 RustDesk 服务端这套组合,实现了随时随地访问家里电脑的能力。

至于对方的25565端口我可太熟了,Minecraft 服务端的默认端口就是25565。于是我在云安全组中封禁了这个网段的出入口,观察云服务监控,外网出带宽恢复正常了,但是外网入带宽、内网出带宽以及 iftop 监控发现21116端口仍在不断尝试向上述 IP 发包,即使重启 RustDesk 服务也无济于事。

这看起来有些矛盾:既然安全组已经封禁,为什么服务器里还能看到发包?原因是安全组是在云厂商网络出口侧生效。也就是说,包在本机网络栈里已经生成,但在云厂商出口处被丢弃,因此账单侧出站流量恢复正常。

结合前面21116端口与 69.33.x.x/24 网段之间只有出口流量没有入口流量,说明该网段并不是攻击者,而是受害者,没准是一个正在遭受 DDoS 攻击的 Minecraft 服务器(笑)。不过既然出口流量已经封住了,接下来就可以慢慢研究了。

抓包分析:还原攻击链路

接下来,需要搞清楚真正的攻击来源,以及为什么 hbbs 程序会向受害者端口大量发包。询问 ChatGPT 之后,我尝试用 tcpdump 监控 hbbs 程序的网络出入包,发现大量如下特征的链路:

14:57:58.432264 veth90f352c Out IP 194.247.x.x.46403 > 172.18.0.2.21116: UDP, length 548
14:57:58.432393 veth90f352c P IP 172.18.0.2.21116 > 69.33.x.x.25565: UDP, length 643

上文只列举了一个,实际上有大量不同的国外 IP 不断往21116端口发包,这下就解释得通了:并不是 hbbs 程序无故往外网大量发包,而是 多个攻击者向 hbbs 发包,hbbs 再往同一个目标发包。这个特征很符合 UDP 反射放大攻击的特征。

为了了解是 hbbs 中哪个协议被用来进行反射攻击,我继续用 tcpdump 抓包,获取完整 UDP payload:

Terminal window
tcpdump -ni any \
'udp and src host 194.247.x.x and dst port 21116' \
-c 5 -s 0 -vvv -XX

然后将抓到的 payload 贴到 ChatGPT 中分析。不得不说,AI 在逆向/数据分析这块还是太权威了:其思考链中通过浏览 rustdesk-server 源码仓库,查找 protobuf 消息格式,编写代码解码,到最终解析出该 UDP 包对应的 RustDesk 消息如下:

RendezvousMessage {
punch_hole_sent {
socket_addr: <11 bytes> # 解码后为 69.33.x.x:25565
id: "szpontnet"
relay_server: "ymfZoVOLVAjokGhkMbhuA2MJg71BOdqVqfEwEnAwBHn8Q7JyYk..." # 超长随机字符串,约 511 bytes
version: "1.3.0"
}
}

这个过程如果让没有相关经验的我来做,压根一点思路也没有。

解出了包的内容,再去源码里比对逻辑就清晰得多了,根据 rustdesk-server源码 ,PunchHoleSent 消息是客户端 P2P 连接过程中的一环,这个消息会进入 handle_hole_sent 方法,执行以下逻辑:

  1. 从攻击包里的 socket_addr 解出目标地址
  2. 构造 PunchHoleResponse
  3. 把攻击包里的 relay_server 原样复制进去
  4. 发给 socket_addr 指定的目标

附带 RustDesk 客户端间 P2P 连接的完整过程方便理解:

客户端 Bhbbs客户端 A客户端 Bhbbs客户端 A前提:客户端 A 和 B 已注册到 hbbsA 想连接 B,发起打洞请求hbbs 将 A 的地址告诉 BB 向 A 发送 UDP 打洞包B 通知 hbbs:已向 A 发起打洞hbbs 通知 A:B 已准备好,可尝试连接 BPunchHoleRequestPunchHoleUDP punch packetPunchHoleSentPunchHoleResponse

如此一来,真相大白。这是一次基于 UDP 的反射型 DDoS 滥用,攻击者并没有攻破我的服务器,而是利用 RustDesk 服务端协议的安全漏洞,伪造大量 PunchHoleSent 消息,诱导 RustDesk 服务端向伪造的目标地址发送 PunchHoleResponse,间接对目标地址进行 DDoS 攻击。

这类攻击的高明之处,在于只需要少量的 IP 向大量暴露在公网的 RustDesk 服务发送伪造消息,就能利用这些公开服务器对目标地址造成 DDoS攻击;同时 PunchHoleResponse 响应包略大于 PunchHoleSent 请求包(643 / 548 ≈ 1.17),因此也带有一定放大效果。

缓解方案

清楚了攻击链路,接下来就要着手解决方案,防止后续其他攻击者利用这个漏洞攻击别的目标。但是细想下来,这防御措施却并不好做。21116端口承载了 RustDesk 的核心功能,直接关掉这个端口会导致客户端无法注册到服务端,而攻击者利用的又是 RustDesk 标准协议内容,除了包体较大之外,没有能明显区分的特征。

限制入口IP

考虑到攻击方 IP 都是国外的,于是我先使用 iptables + ipset 将 21116/UDP 的访问白名单限制在国内 IP 中。

首先创建 ipset 集合,作为白名单:

Terminal window
ipset create cn_ips hash:net family inet -exist

然后找一个包含所有国内 IP 网段的 CIDR 网段合集,这里使用的是 IPDeny 提供的文件,将其下载到临时目录中:

Terminal window
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone -o /tmp/cn.zone

然后导入刚刚创建的合集,这里使用 awk 将下载下来的 zone 文件预处理为 ipset restore 格式,这样导入比按行切分 zone 文件再一个个 add 进去要快很多:

Terminal window
SET_NAME=cn_ips
ZONE_FILE=/tmp/cn.zone
awk -v set="$SET_NAME" '
/^[[:space:]]*$/ { next }
/^[[:space:]]*#/ { next }
{ print "add " set " " $1 }
' "$ZONE_FILE" | ipset restore -exist

完成之后验证一下导入的数量:

Terminal window
ipset list cn_ips | grep 'Number of entries'

现在将这个 ipset 挂到 iptables 中,首先创建一个规则链,然后将 cn_ips 这个白名单添加到链中:

Terminal window
# 创建规则链
iptables -N RUSTDESK_UDP_GEO 2>/dev/null || true
iptables -F RUSTDESK_UDP_GEO
# 挂载白名单
iptables -A RUSTDESK_UDP_GEO -m set --match-set cn_ips src -j RETURN
# 可选:记录被拦截来源,限速防止刷日志
iptables -A RUSTDESK_UDP_GEO -m limit --limit 5/min --limit-burst 10 \
-j LOG --log-prefix "rustdesk_udp21116_drop: " --log-level 4
# 其他全部丢弃
iptables -A RUSTDESK_UDP_GEO -j DROP

接下来根据服务的部署方式挂载这个规则链,笔者这里是用 Docker 部署的 RustDesk 服务端,故将规则链挂在 DOCKER-USER 前面:

Terminal window
iptables -C DOCKER-USER -p udp --dport 21116 -j RUSTDESK_UDP_GEO 2>/dev/null || \
iptables -I DOCKER-USER 1 -p udp --dport 21116 -j RUSTDESK_UDP_GEO

然后验证规则是否生效:

Terminal window
# 看 DROP 规则的 packet / bytes 是否增长
iptables -L DOCKER-USER -n -v --line-numbers
iptables -L RUSTDESK_UDP_GEO -n -v --line-numbers
# 或者查看日志
dmesg -T | grep rustdesk_udp21116_drop

限制出口速率

为了防止流量被刷,我们还可以限制 21116/UDP 端口的出口速率,这里依旧用 iptables 实现。

依旧先添加一个规则链:

Terminal window
iptables -N RUSTDESK_UDP_OUT_LIMIT 2>/dev/null || true
iptables -F RUSTDESK_UDP_OUT_LIMIT

然后挂上限速规则,将网络包速率限制在15/s,允许突发50个包:

Terminal window
iptables -A RUSTDESK_UDP_OUT_LIMIT \
-m hashlimit \
--hashlimit-name rustdesk_21116_udp_out \
--hashlimit-above 15/second \
--hashlimit-burst 50 \
--hashlimit-mode srcip,srcport \
-j DROP
iptables -A RUSTDESK_UDP_OUT_LIMIT -j RETURN

最后将其挂在 DOCKER-USER 前,限制 21116/UDP 出口方向:

Terminal window
iptables -I DOCKER-USER 1 \
-p udp --sport 21116 \
-j RUSTDESK_UDP_OUT_LIMIT

最后记得 iptables 规则是内存生效的,需要加上持久化和恢复机制,避免重启丢失规则。

复盘总结

需要说明的是,以上规则只是缓解措施,不是根治方案。入口白名单可以减少被国外来源利用的概率,但无法防止国内攻击源;出口限速可以控制最坏影响,但也不能修复协议本身的问题。真正的修复仍然需要 rustdesk-server 在协议层对 PunchHoleSent 增加上下文校验或者身份认证。Github 上已有人提了 issue,可以关注后续进展。

这次事件的整个处理流程相对还是比较稳健的,从调整带宽的紧急止血措施,到安全组封禁目标网段,再到保留现场抓包分析,并实施缓解方案防止后续同类型的攻击,每一步都见效显著。当然,这次事件也带来了几个收获,在此分享一下:

网络攻防无处不在

不要觉得你的网站没什么流量,部署的服务也是私人用途,就可以放松警惕,笔者历来对安全比较上心,仅开放必要的端口,并将各类密钥设置得尽量复杂,但也没想到会在服务器没被攻破的情况下被别人利用,成为了 DDoS 攻击的一环。

这也说明,服务器安全不只是防止被入侵。即使系统没有被攻破,一个正常开放的服务端口,也可能因为协议行为被滥用,成为攻击链路的一部分。

监控告警很重要

必须建立主动监控和告警,这次是因为公网流量把余额花光了触发了欠费通知,我才知道服务器被攻击了,而此时距离攻击开始时间点已经过去小半天了。所谓吃一堑,长一智,过后我立刻补齐了相关监控,包括出口流量异常增高,以及费用异常增高等监控,并设置告警,避免异常情况下无法及时得知。

对自建服务来说,安全组、防火墙、出口限制、流量监控和费用告警都应该作为基础设施的一部分,而不是出事之后才临时补救。

不要预留过多余额(确信)

因为我只预留了个位数的余额在云服务厂商中,所以损失还不算大。如果预留个3位数余额,哪怕是直到攻击结束了我还不知道,只能过后看这月度账单干瞪眼。攻击者没准也是考虑到这一点,才选择7月1日发起攻击。