恶意软件

PoeLLM 把控制地址藏进诗里

PoeLLM 从 GitHub 上的诗句计算控制服务器地址,把遭入侵的 AI 与开源服务主机用于挖矿和继续攻击。处置需要同时切断恶意进程、收紧服务入口并修复权限问题,追着旧 IP 封禁会留下重新连接的机会。

浅暖纸手绘概念图,一本夹着标记的诗集连向服务器,服务器侧面的机械轮正带动采矿装置。
文章导航

攻击者换了控制服务器,已经中招的主机怎样找到新地址?PoeLLM 把答案放进 GitHub 上的一首诗。恶意程序读取文字,挑出四个词,按内置词表换成四个数字,再把它们拼成 IPv4 地址。操作者改几个词,服务器就会去找另一个控制端。

Lumen 旗下威胁研究团队 Black Lotus Labs 在 10 月 7 日公开的调查把这轮活动称为 Canto Incognito,恶意程序称为 PoeLLM。活动至少从 4 月开始,目标包括暴露在互联网上的 AI 服务和其他开源应用。中招主机被用于挖矿,也被用来寻找和攻击下一批服务器。对维护这些服务的人来说,需要收回的已经包括本机算力、对外连接和程序启动权限。

诗句的特别之处,在于让“找控制端”与“控制端本身”分开。旧 IP 可以失效,保存诗句的文件仍能告诉程序下一站去哪。所以调查应当从发起读取和连接的本机进程往回追,封禁已知地址只是途中一步。GitHub 本身仍是正常的软件协作平台,把整个站点一关了之会伤到构建和更新,也绕开了主机上那个仍在执行的程序。

1 诗句负责告诉程序下一站去哪

公开分析描述的是一个相当固定的文字解析器。前三个词按固定的前后措辞定位,匹配时忽略大小写。第四个词则从 of distant servers 向前查找,并要求它前面紧接 the 。随后,每个词在程序内的字典里对应一个字节值。四个字节连起来,就是下一次连接使用的地址。这里的控制能力来自程序预先约定好的解析规则,普通读者读到的文学含义不参与计算。

用虚构词表和 192.0.2.0/24 文档保留网段,就能看清这个过程。四个词依次对应 192、0、2、10,本地模型得到 192.0.2.10。只把最后一个词换成另一个已登记的词,结果变为 192.0.2.11。改动无关文字则保持原结果。这是只处理字符串的演示,没有运行恶意程序或连接任何地址。

三个步骤,定位四个词,按词表得到四个数字,再组成文档示例地址 192.0.2.10。
图 1:文字到地址的解释模型,使用虚构词语和文档示例地址。程序必须认识词语和定位规则,普通文本变更不会自动变成有效地址。查看原图。

这个结构降低了操作者换地址的成本。防守方若只保存当时解析出的 IP,下次文件更新就可能让旧规则失去目标。反过来,解析器依赖固定文字和内置字典,也给调查留下了稳定特征。应保留主机实际读取的文件版本、发起请求的程序及随后连接的地址,才能判断一次 GitHub 访问与后面的控制流量是否属于同一过程。

调查中承载诗句的是攻击者维护的 nodejs.org 仓库分叉及其中新增的 dash.css 文件。公开材料没有把 Node.js 上游代码或官网列为被篡改对象。CSS 扩展名也不会自动赋予内容可信性,决定用途的是谁读取它、读出什么、接下来执行什么。

2 一次连接测试为什么会启动程序

把地址藏起来之前,攻击者还得让自己的程序进入服务器。PoeLLM 的公开调查在一个样本中发现了针对 LiteLLM /mcp-rest/test/connection 的利用逻辑,将 CVE-2026-42271 列为可能入口。具体主机究竟通过哪条路径失守,仍要靠该主机的请求和执行记录确认。LiteLLM 官方公告能把这个接口本身的问题解释得很清楚。

LiteLLM 可以统一管理模型请求,也能连接 MCP 工具。MCP 的 stdio 方式要在本机启动一个程序,再通过其标准输入和输出通信。因此,“测试这份 MCP 配置能否连接”本来就可能产生子进程。危险的权限差别在于,谁有权提交这份配置。厂商公告列出的受影响范围是 >=1.74.2, <1.83.7,当时持有有效代理 API key 的低权限用户也能调用测试接口,接口没有检查其管理员角色。

在固定的易受影响源码 rest_endpoints.py 第 1021—1042 行,test_connection() 取得认证对象之后,直接把提交配置交给通用执行函数。该文件第 918—960 行又把请求里的 command、args、env 装进临时 MCPServer,创建客户端并运行测试。最初只是 HTTP 请求中的几项字段,到这里已经成了本机程序的启动配置。

下游 experimental_mcp_client/client.py 第 240—248 行把这三项配置交给 StdioServerParameters 和 stdio_client。调用测试时进入这个通信上下文,程序便按代理进程拥有的权限运行。是否已经把工具“保存成功”不影响此前发生的启动动作,预览接口也必须承担执行权限检查。

修复后的两个测试入口先要求 PROXY_ADMIN,不符合时返回 403,再继续创建测试会话。修补还在命令构造处增加了白名单。管理员仍能配置解释器及其参数,因此服务进程的主机权限仍应尽量有限。角色检查关住低权限用户到启动能力的捷径,运行环境负责限制获准工具能影响哪些文件和凭据。

1.83.7 是这个问题的历史修复下限。截至 10 月 13 日,官方最新正式发行版是 v1.105.0,当前升级应评估这一维护版本及其依赖,不能把半年前的下限当作安装建议。暂时无法升级时,厂商建议在网关阻断 POST /mcp-rest/test/connection 与 POST /mcp-rest/test/tools/list,这会影响添加工具前的连接与列表预览。升级验收要检查普通用户调用被拒绝,以及管理员已有的合法工具仍能工作。升级失败就继续保持这些入口受限,恢复业务时也不要重新开放易受影响旧接口。

3 一台中招服务器,会接着寻找下一台

挖矿解释了这轮活动怎样获利,Black Lotus Labs 还看到部分被控服务器成为扫描和利用节点。AI 服务和文档转换服务常需要持续运行,有算力,也有访问外部网络的理由。一旦攻击程序获得执行机会,这些正常条件就能被复用。CPU 被占满会让人注意到性能问题,对外扫描却可能混在另一套网络日志里。即使 CPU 已恢复正常,仍要核对这台主机是否曾向外发起相关请求。

Black Lotus Labs 在样本里分析出读取诗句、计算控制地址和下载恶意文件的过程,又在网络流量中看到被控主机向更多目标的 3000、4000 端口发起大量连接,这两个端口分别对应调查关注的 Gotenberg 和 LiteLLM 部署。回查本机时,可以先找到读取那份文件的进程,再追它随后连接的地址、下载的文件和启动的子进程,最后核对是否出现相应的向外请求。端口和横幅帮助识别可能运行的应用,判断具体利用是否成功还需要执行记录。

固定版 IOC 文件列出了 12 个控制地址、各自的观测时间段和三个样本哈希,并明确将 Present 定义为 10 月 7 日。其中 92.119.165[.]74 的记录从 7 月 1 日到 7 月 23 日。这样的对象适合回查对应历史流量,不能把结束日期抹掉,再把今天访问过同一个 IP 的所有主机归为这轮受害者。

文件名也需要结合内容判断。公开样本名 libgcrypt 借用了常见加密库的名称,一个同名文件仍要核对完整哈希、实际路径、文件类型和谁启动了它。确认恶意执行后,调查范围应跟着该进程能读到什么、能访问哪些内部服务展开。模型提供商密钥、配置文件和服务令牌是需要检查的可达资产,是否已经泄露必须由日志或其他实际证据确认。

4 安装成功,还没有回答谁能访问

这类服务很容易跨过一个没有被认真检查的步骤。容器启动了,浏览器能打开,业务也能调用,部署便被视为完成。监听地址和端口发布方式却在决定另一件事,服务究竟只交给同机程序、容器网络里的应用,还是任何能连到这台机器的人。功能正常与访问范围是两项独立的验收。

Gotenberg 的安装文档明确要求把它放在防火墙之后。文档中的 3000:3000 会发布主机端口,官方另外给出 127.0.0.1:3000:3000,供只需要同机访问的部署使用。同一个 Compose 网络中的服务还可以通过 gotenberg:3000 访问,不必为此添加主机端口。选择哪一种取决于实际调用者在哪,改动后要从合法调用方完成一次转换,不能只看本机健康检查成功。

Ollama 的认证文档则区分本地 API 与云端 API。默认本地地址 http://localhost:11434 不要求认证,云端服务另用 API key。把本地服务绑定到外部接口时,云端密钥不会自动给这个新入口补上访问控制。需要远程调用的环境应在入口明确限制调用方和权限,再检查实际监听、容器端口、反向代理和防火墙是否表达了同一个范围。

关掉不需要的外部端口后,从原有内部调用方完成一次实际请求。增加认证后,分别检查无凭据、低权限用户和合法工作流的结果。模型下载和软件更新还需要外联,可以按发起进程和用途保留,避免修补时把正常依赖一起切断。

5 让服务器重新只做它自己的工作

发现相关执行记录后,先限制这台主机的外联与暴露入口,保留进程、启动项、文件、容器配置和网络日志,再确定最早能够确认的入侵时间。未记录路径和父子关系就直接终止高占用进程,会丢失部分运行态线索,也没有查清是否还有另一个启动点。已经确认被远程控制的环境,应从可信镜像或程序包重建,恢复前复核配置和挂载,避免把原来的执行入口原样带回来。

凭据处理跟着可达范围走。被控进程能读取的密钥需要轮换,权限应随实际用途收紧,替换后的密钥通过正常应用请求验证。模型调用恢复只是其中一项结果,还要观察服务有没有重新创建陌生子进程、读取那份地址文件或向未经批准的目的连接。保留正常程序的基线,新的异常才不会再次被“这个服务本来就要联网”解释过去。