
从 Abuse 邮件溯源至 Globalping
用过 Hetzner 的人大概都知道 Abuse 邮件不能随手归档。有些工单要求限时解释或处理,拖久了可能影响服务器乃至账号。不过,这次邮件正文明确写着不必回复,真正要做的是先弄清楚:我的服务器为什么会出现在一份恶意软件通信报告里?
当时正在给 Mantine 迁移到 StyleX 的工作收尾,邮箱里突然出现一封 Hetzner 转发的 CERT-Bund 报告。那台服务器没有对外提供什么公共服务,也没有运行容易触发此类告警的程序,所以第一反应自然是,被挂马了?
一、先从邮件开始
Hetzner 转发的邮件很长,真正与排查有关的内容只有下面这些:
Dear Mr Aoang,
We have received a notification from the German Federal Office for
Information Security (BSI) for (the IP address of) a server you have with us.
These notifications do not mean your server was involved in any abusive
activity. They are simply alerting you to a potential issue on your server.
You do not need to send us, or the BSI, a response.
Ticket: [CB-Report#001]
Affected systems on your network:
Format: ASN | IP | Timestamp (UTC) | Malware | SRC port | DST ip | DST port | DST host | Proto
24940 | 162.55.x.x | 2026-09-14 03:56:33 | netnut | 47830 | 85.17.70.16 | 5959 | gw.netnut.net | tcp先别被 Malware: netnut 吓到。邮件同时给出了几个更适合调查的硬信息:
- 源地址属于我的 Hetzner 服务器,ASN 是
24940; - 事件发生在
2026-09-14 03:56:33 UTC; - 服务器通过 TCP 连接了
gw.netnut.net:5959; - 报告来源称这个网络行为与
netnut恶意软件家族有关。
CERT-Bund 的报告由自动化流程发送给相应网段登记的 Abuse 联系人,服务商再转发给客户。BSI 的官方 FAQ说明了这条分发链路;邮件本身则只说数据来自可信的外部来源,没有披露本次事件的具体采集设备与判定规则。
不过大概率是旁路设备对流量做的分析,报告表明某个外部观测源看见了这条连接,并把它归入 netnut;它没有证明服务器磁盘上存在某个特定二进制文件,也没有给出进程、容器或文件哈希。
二、优先假设已被入侵
收到这类邮件时,最危险的两种反应正好相反:一种是看到 Malware 就认定整台机器失守,另一种是想起某个合理程序会联网,便立刻宣布误报。
先看了当时的日志和正在运行的服务,没有发现明显异常。系统保持更新,SSH 使用证书认证,已有监控也没有报警。这些现象降低了被入侵的可能性,却不能单独排除入侵。
接着反过来查「有什么软件会按照外部指令连接任意公网目标」。服务器上运行着一个 Globalping 探针,嫌疑一下子具体了。
Globalping 是 jsDelivr 团队维护的开放网络测量平台。用户通过网页、命令行或 API 发起 ping、traceroute、DNS、HTTP 和 MTR 等测量,平台再把任务调度到世界各地的社区探针。官方的 API 规范表明,HTTP 测量可以指定目标、协议与端口。
服务器不接收测量用户的入站连接,探针先主动连接 Globalping API,等待平台下发任务,然后从自己的公网 IP 向目标发起测量。对目标服务器、网络运营商和外部观测系统来说,流量来源仍然是我的 IP。
这就是整件事的关键:运行公共探针,不等于把服务器登录权限交给陌生人,却确实是在授权平台让这台机器替陌生人产生受约束的外连流量。
三、时区先给排查挖了一个坑
先确认容器仍在运行,随后在服务器上请求公网 IP 查询服务,返回值与邮件中的源 IP 一致。接下来按邮件时间读取日志:
docker logs globalping-probe \
--since '2026-09-14T03:50:00' \
--until '2026-09-14T04:02:00'结果时间对不上,日志从 UTC 01:50 左右开始。问题不在容器,而在我传给 Docker 的时间没有时区。
Docker 文档对 --since 和 --until 的规则写得很明确:时间末尾没有 Z 或 +08:00 之类的偏移量时,Docker 会按客户端本地时区解释。那台机器使用 Europe/Berlin,事发时是 CEST(UTC+2),所以本地 03:50 正好对应 UTC 01:50。
德国的机器自然是德国的时区,把时区都统一成 UTC+8 并不好,因为服务器的时区很有可能被扫描程序探测出来。所以给时间写上偏移量即可:
docker logs globalping-probe \
--since '2026-09-14T03:50:00Z' \
--until '2026-09-14T04:02:00Z'显式写 Z 还有一个好处:命令被复制到另一台不同时区的机器上,含义也不会改变。
四、日志只有测量 ID,目标藏在公开 API 里
探针的 DEBUG 日志出现了这样的记录:
[2026-09-14 03:56:28 +00:00] [DEBUG] [general] http request id000 received.
[2026-09-14 03:56:28 +00:00] [DEBUG] [general] http request id001 received.本地日志只给出测量类型与 ID,不直接记录目标。Globalping 提供 GET /v1/measurements/{id} 查询结果,因此可以把报告时间前后一分钟内的 HTTP 测量 ID 提取出来,再逐个核对。
docker logs globalping-probe \
--since '2026-09-14T03:55:30Z' \
--until '2026-09-14T03:57:30Z' 2>&1 \
| rg -o 'http request [[:alnum:]]+' \
| awk '{print $3}'自己手动去处理是不可能的,让 AI 写脚本来进行排查。但不会把整份服务器日志或原始邮件直接丢给第三方服务。这里很容易为了省一分钟,把姓名、公网 IP、工单号、探针标签和测量 ID 一起泄露出去。脚本可以交给 AI 写,敏感输入仍应在本地处理。
脚本逐个调用的过程中就匹配到了对应的请求:
{
"id": "id000",
"type": "http",
"target": "gw.netnut.net",
"createdAt": "2026-09-14T03:56:28.498Z",
"measurementOptions": {
"request": {
"headers": { "Host": "gw.netnut.net:5959" },
"method": "GET"
},
"protocol": "HTTP",
"port": 5959
}
}域名和端口已经与报告一致,创建时间则比外部观测时间早约 5 秒。一次 HTTP 测量本来就要经历任务下发、DNS 解析、TCP 建连和等待响应,两条系统记录相差几秒并不奇怪。
五、把探针结果与 Abuse 记录合起来
这次测量一共调度了 9 个探针。其中一个结果正是 AS24940 Hetzner,探针标签也能在我的 Globalping 账号中对应上。公开结果显示:
resolvedAddress: 85.17.70.16
statusCode: 200
rawOutput: "HTTP/1.1 200\n"
timings: { total: 13022, dns: 9, tcp: 11, firstByte: 13002 }把两边字段并排放在一起,证据链就清楚了:
| 字段 | CERT-Bund 报告 | Globalping 测量 |
|---|---|---|
| 时间 | 03:56:33 UTC | 03:56:28 UTC |
| 来源网络 | AS24940 Hetzner | AS24940 Hetzner |
| 目标域名 | gw.netnut.net | gw.netnut.net |
| 目标地址 | 85.17.70.16 | 85.17.70.16 |
| 目标端口 | 5959 | 5959 |
| 协议 | TCP | HTTP |
Globalping 结果还显示,TCP 建连耗时 11 ms,约 13 s 后收到 HTTP/1.1 200。报告中的源端口是操作系统为这次外连分配的临时端口;Globalping API 没有返回该字段,所以我不能用它再做一次独立匹配。不过,时间、来源网络、目标域名、解析地址和端口已经同时吻合,足以解释报告里的那条连接。
更严谨的结论是:CERT-Bund 报告中的网络事件与这次 Globalping HTTP 测量高度一致,现有证据不支持把这条连接归因于服务器上的 netnut 恶意程序。
这不等于给整台服务器开出「绝对未入侵」证明。它只说明触发本次报告的行为有一个可验证、与记录完全相容的正常来源。登录、进程、持久化与文件完整性检查仍然有必要,只是排查优先级可以从「立即重装」降到「完成检查并保存证据」。
六、netnut 标签到底说明了什么
NetNut 是一家代理网络服务商,gw.netnut.net 看起来是它的网关域名。但仅凭邮件中的一行记录,无法知道数据提供方采用了哪套规则:可能来自 sinkhole 或威胁情报,也可能按照已知基础设施和端口关联到某个恶意软件家族。
因此不能进一步断言「只要连接这个端口就会被标记」,也不能断言「报告系统把正常代理客户都当成木马」。能确认的只有两点:
- 外部报告把这条连接归为
netnut; - 同一时刻,Globalping 确实下发并完成了指向同一目标的 HTTP 测量。
这个区别很重要。威胁情报中的家族名有时描述的是被观测到的通信模式或关联基础设施,不一定等同于在主机上完成了恶意程序取证。Abuse 报告适合当调查入口,不适合单独充当感染结论。
七、Globalping 有防护,但风险不会归零
Globalping 并非毫无限制地把任意命令发给社区探针。它的探针文档列出了几项安全措施:探针不监听入站端口;控制连接使用安全连接;平台会阻止私有 IP 目标,使用恶意域名与 IP 列表做 API 级封禁,并对用户限速。
这些措施解决的是明显的内网探测、已知恶意目标和批量滥用,不能保证每一个尚未进入列表的公网域名都不会触发运营商、蜜罐或威胁情报告警。名单会滞后,域名解析会变化,不同安全系统的分类标准也不一致。本次测量能够成功执行,本身就说明「平台允许」与「云厂商不会收到投诉」不是同一个保证。
我还查看了探针当时的 config/default.cjs。里面有 API、更新、状态检测、统计、命令执行和 adoption server 等配置,但没有给宿主机所有者使用的目标域名、目标端口或测量类型黑名单。官方当前也明确要求不要修改探针代码或行为。
这让「自己加一点防火墙规则」变得没那么漂亮:
- 只封
5959或gw.netnut.net,只能挡住已经遇到的目标;域名还可能换地址,下一次报告也可能来自另一个端口。 - 只允许访问少量常见 Web 端口,会让大量合法的端口连通性测量失真;探针看起来在线,结果却不再可信。
- 只放行 Globalping 控制面、拒绝所有测量流量,等于运行了一台不能测量的探针。
- 修改探针逻辑或系统性改变它的行为,还会偏离项目对社区探针的一致性要求。
所以这里没有一条既保留完整测量能力,又保证永远不触发第三方风控的防火墙规则。真正干净的选择只有两个:接受这类出口风险并为它准备处置流程,或者停止运行探针。
八、回到那封邮件
我在不少运行着非重要服务、有空闲性能的服务器上部署着 Globalping,遇到这个事情倒是给自己提了个醒:
- 不要把它放在承载重要业务、邮件发送或其他重信誉服务的唯一公网 IP 上;
- 保留足够长的容器日志,确保收到报告后还能回溯;
- 记录探针版本、容器启动方式和账号中的探针对应关系,但对外发布时隐藏 adoption token、探针标签与测量 ID;
- 监控异常出站连接和容器行为,不能因为装的是开源探针就跳过主机层检查;
- 如果同类报告频繁出现,或者服务商明确不接受这类流量,就只能停止探针了。
服务器感染 netnut 了吗?并没有,报告记录的是与某类威胁基础设施相符的网络行为,而我找到了一个已知程序为什么会产生这条行为的完整解释。
所以这是一条误报?站在主机感染判断上,这次关联很可能是的;站在外部观测系统的角度,那条 TCP 连接却真实发生过。双方看到的是同一件事,只是回答了不同的问题。
