TL;DR
2026年2月27日至3月22日期间,威胁行为者TeamPCP对Aqua Security的Trivy漏洞扫描器、其GitHub Actions以及超过60个npm包进行了多阶段开源软件供应链攻击。本文将多个厂商报告中的时间线、技术手段、IOC指标和修复指南整合为一份完整的参考资料。
如果您在2026年3月19日至22日期间使用了Trivy、trivy-action或setup-trivy,请假设受到以下影响:
- 工作流中所有可访问的机密信息(GitHub令牌、云凭证、SSH密钥、Docker注册表令牌)均已被窃取,使用RSA-4096加密,并发送至攻击者基础设施
- CI/CD运行器的内存被扫描,收集所有标记为
isSecret:true的GitHub Actions机密 - 在CI环境外运行Trivy v0.69.4的开发人员机器上存在持久的systemd后门,每5分钟向ICP容器轮询获取任意载荷
- 从CI运行器窃取的npm令牌被用于在60秒内发布64+个包的恶意版本
- 从被入侵的运行器可访问的Kubernetes集群可能存在具有主机级访问权限的攻击者DaemonSet
事件起源:AI机器人与配置错误的工作流
2026年2月27日,一个名为hackerbot-claw的自主GitHub机器人对Trivy仓库提交了PR #10252。该PR立即被关闭,但这并不重要。Trivy的"API Diff Check"工作流使用了pull_request_target,该工具在执行来自攻击者分支的代码时使用仓库级别的密钥运行。这就是著名的"Pwn Request"模式。
该机器人将一个个人访问令牌(PAT)窃取至recv.hackmoltrepeat[.]com。截至2月28日,攻击者已完全控制仓库:他们将仓库设为私有、删除了全部178个GitHub Releases(v0.27.0至v0.69.1),并向Open VSIX发布了一个恶意VS Code扩展。
Aqua Security做出响应并轮换了凭证。但轮换并非原子操作。攻击者观察到或拦截了刷新的令牌,通过aqua-bot服务账户维持了访问权限。
第二次入侵:v0.69.4与标签投毒
3月19日17:43 UTC,aqua-bot向Trivy推送了一个恶意的v0.69.4标签,触发了自动化发布流程。该恶意二进制文件通过GitHub Releases、GHCR、Docker Hub、ECR Public、deb/rpm仓库和get.trivy.dev分发,在约21:36 UTC被移除前暴露了约3小时。
该恶意的Trivy二进制文件在载荷运行的同时执行了合法扫描器,从用户视角掩盖了入侵行为。在开发人员机器上(通过GITHUB_ACTIONS != "true"检测),它安装了一个持久性后门,作为systemd用户服务位于~/.config/systemd/user/sysmon.py,每5分钟向一个ICP容器轮询获取任意载荷。
标签投毒
更大的影响范围来自GitHub Actions。在数小时内,aqua-bot强制推送了76个trivy-action标签中的75个(0.0.1至0.34.2版本)以及全部7个setup-trivy标签至恶意提交。超过10,000个GitHub工作流引用了trivy-action。任何使用aquasecurity/[email protected]的工作流会静默解析至攻击者的代码。
该技术经过精心设计以避免检测:攻击者从master HEAD开始,仅替换entrypoint.sh,克隆原始提交的元数据(作者、邮箱、时间戳、提交信息),然后强制推送标签。这些伪装的提交甚至冒充了知名开发者:"rauchg"(Guillermo Rauch,Vercel CEO)用于actions/checkout注入,"DmitriyLewen"(Aqua维护者)用于Trivy提交。
检测特征:原本经过web签名且有GPG签名的提交缺失签名,时间线不可能(日期为2021年的提交其父提交来自2026年3月),以及每个被篡改的提交仅更改了一个文件,而原始提交更改了多个文件。
凭证窃取器的行为
恶意的entrypoint.sh包含一个三阶段载荷,共204行:
- 收集:在GitHub托管的运行器上,恶意软件通过无密码
sudo提权至root,定位Runner.Worker进程,并通过/proc/<pid>/mem扫描其内存,搜索Actions存储掩码机密的JSON模式{"value":"<secret>","isSecret":true}。它还扫描了50+个文件系统路径以获取SSH密钥、AWS/GCP/Azure凭证、Kubernetes令牌、Docker配置和加密货币钱包。 - 加密:收集的数据使用RSA-4096混合加密(OAEP填充)的AES-256-CBC加密,捆绑为
tpcp.tar.gz。只有攻击者的私钥可以解密。 - 窃取:主通道是通过HTTPS POST到
scan.aquasecurtiy[.]org(aquasecurity.org的拼写错误域名,解析至45.148.10.212)。备用通道:如果INPUT_GITHUB_PAT可用,恶意软件会在受害者的GitHub账户上创建一个公开的tpcp-docs仓库,并将加密的捆绑包作为release资产上传。
合法Trivy扫描在恶意软件之后运行(第106-204行),因此用户看到的是正常的扫描输出。
CanisterWorm:npm实现自我传播
在Trivy被入侵后不到24小时,CI运行器中被窃取的npm令牌被武器化。3月20日20:45 UTC,TeamPCP部署了CanisterWorm,这是一种自我传播的npm蠕虫,代表了首次观察到使用互联网计算机协议(ICP)容器作为恶意软件C2基础设施。
该蠕虫的传播逻辑:
- 从
~/.npmrc、项目.npmrc和环境变量中收集npm令牌 - 通过
/-/whoami认证,通过维护者搜索枚举可发布的包 - 递增补丁版本,保留原始README,使用
--access public --tag latest发布 - 在finally块中恢复原始文件
28个@emilgroup包在60秒内被入侵。该蠕虫随后传播至@opengov(16个包)、@teale.io/eslint-config、@airtm/uuid-base32和@pypestream/floating-ui-dom。截至3月21日,Socket报告了跨64+个独立包的135个恶意构件。
载荷架构是一个三阶段链:Node.js postinstall加载器解码一个base64 Python脚本,将其写入~/.local/share/pgmon/service.py,并安装为伪装成PostgreSQL监控的systemd用户服务(pgmon.service)。该Python后门休眠5分钟(沙箱规避),然后每50分钟向ICP容器tdtqy-oyaaa-aaaae-af2dq-cai轮询获取指向当前载荷的URL。终止开关:如果URL包含"youtube.com",植入物进入休眠状态。
载荷快速迭代:kamikaze.sh
3月22日,TeamPCP通过ICP容器在约5小时内轮换了6个载荷版本,每个版本在下一轮询时被所有受感染机器获取:
| 时间 (UTC) | 版本 | 功能 |
|---|---|---|
| ~11:45 | v1 | 一体化K8s DaemonSet,具备主机逃逸、systemd持久化、针对伊朗的擦除器 |
| 12:45 | v2 | 模块化加载器,获取kube.py,C2切换至Cloudflare隧道 |
| 13:00 | v3 | 纯SSH/Docker蠕虫(扫描本地/24网段的22、2375端口) |
| 13:25 | v3.1 | 模块分离:kube.py(损坏的K8s)+ prop.py(SSH/Docker蠕虫) |
| 14:56 | v3.2 | 工作的ICP后门,kubectl重新启用,新的Cloudflare隧道 |
| 16:15 | v3.3 | WAV隐写术:载荷隐藏在音频文件中(bg_kube.wav、bg_prop.wav) |
针对伊朗的擦除器(由ramimac发现)部署了一个独立的DaemonSet,检测Asia/Tehran时区或fa_IR区域设置并执行rm -rf / --no-preserve-root。这是该活动中唯一的破坏性、非窃取性组件。
截至16:00 UTC,TeamPCP还直接推送了恶意Docker Hub镜像(v0.69.5、v0.69.6),绕过了GitHub release流水线。20:31-20:32 UTC,aquasec-com组织中的44个内部Aqua仓库被篡改。ICP容器于21:31 UTC被列入黑名单。
安全团队的IOC指标
网络指标
| 指标 | 角色 |
|---|---|
scan.aquasecurtiy[.]org / 45.148.10.212 | 主C2(TECHOFF SRV LIMITED,阿姆斯特丹) |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io | ICP容器死滴(2026/03/22列入黑名单) |
plug-tab-protective-relay.trycloudflare[.]com | Cloudflare隧道(凭证窃取) |
investigation-launches-hearings-copying.trycloudflare[.]com | Cloudflare隧道(kamikaze v2) |
championships-peoples-point-cassette.trycloudflare[.]com | Cloudflare隧道(v3/v3.1) |
create-sensitivity-grad-sequence.trycloudflare[.]com | Cloudflare隧道(v3.2/v3.3) |
recv.hackmoltrepeat[.]com | 阶段1 PAT窃取 |
受影响的构件
| 组件 | 受影响版本 | 暴露时间 |
|---|---|---|
| Trivy二进制文件 | v0.69.4(所有仓库),Docker Hub v0.69.5/v0.69.6 | ~3小时 |
trivy-action | 标签0.0.1-0.34.2(75个标签) | ~12小时 |
setup-trivy | 标签v0.2.0-v0.2.5 | ~4小时 |
| npm(@emilgroup、@opengov等) | 135+个构件,64+个包 | 各不相同 |
恶意容器摘要(v0.69.4)
根据Aqua的安全公告:
| 架构 | 摘要 |
|---|---|
| Generic | sha256:27f446230c60bbf0b70e008db798bd4f33b7826f9f76f756606f5417100beef3 |
| amd64 | sha256:12c702212dee1cbec9471e9261501a3335963321fe76e60e5a715b5acd3c40a2 |
| arm64 | sha256:2d7cee41048988eec27615412e7c6e2e21046f2b5faa888c24e11ca6764058ed |
文件系统指标
| 路径 | 组件 |
|---|---|
~/.config/systemd/user/sysmon.py | Trivy开发人员机器后门 |
~/.config/systemd/user/pgmon.service | CanisterWorm持久化 |
~/.local/share/pgmon/service.py | CanisterWorm Python后门 |
/tmp/pglog | 载荷暂存 |
/tmp/.pg_state | 状态跟踪 |
狩猎查询
- GitHub:在您的组织中搜索名为
tpcp-docs或匹配tpcp-docs-*的仓库。其存在表明通过死滴备用通道成功窃取了凭证。 - GitHub Actions:审查3月19日至20日引用
aquasecurity/trivy-action或aquasecurity/setup-trivy的工作流运行。检查"Run Trivy"和"Setup environment"步骤中是否有异常输出。 - Kubernetes:在
kube-system命名空间中搜索名为host-provisioner-std或host-provisioner-iran的DaemonSet。 - systemd:检查匹配的
sysmon、pgmon、pgmonitor或internal-monitor用户服务。 - npm:针对
@emilgroup、@opengov、@teale.io、@airtm、@pypestream范围中的包版本与预期版本进行审计。
现在需要做什么
如果您在2026年3月19日至22日期间使用了Trivy:
- 固定到安全版本:Trivy使用v0.69.3或更早版本,trivy-action使用v0.35.0,setup-trivy使用v0.2.6。Aqua的安全公告有完整的版本矩阵。
- 轮换受影响工作流可访问的所有机密信息:GitHub令牌、云提供商凭证、Docker注册表令牌、SSH密钥。
- 在网络边界阻止C2域名和IP。
- 使用cosign签名对照已知良好摘要验证容器完整性。
长期加固措施:
- 将所有GitHub Actions固定到完整提交SHA,而不是版本标签。标签可以被强制推送;提交SHA不能。
- 使用StepSecurity Harden-Runner或等效工具来监控和限制CI运行器的出站网络流量。
- 审计
pull_request_target工作流,确保它们永远不会用提升的权限检出不受信任的分支代码。 - 将凭证轮换视为原子操作:首先撤销,然后重新颁发,旧的和新的凭证之间没有重叠窗口。
可独立验证的证据
本文中的多项声明可以使用gh CLI直接对照GitHub的公共API进行验证。我们于2026年3月23日运行了这些查询。您可以复现它们。
release缺口确实存在
阶段1中缺少了178个release(v0.27.0至v0.69.1):
$ gh api repos/aquasecurity/trivy/releases --paginate \
--jq '.[].tag_name' | sort -V | tail -20
# ...
v0.25.4
v0.26.0
v0.69.2 # ← gap: 43 minor versions missing
v0.69.3恶意的v0.69.4标签已被删除:
$ gh api repos/aquasecurity/trivy/git/ref/tags/v0.69.4
# {"message":"Not Found","status":"404"}冒充提交存在且未签名
提交1885610c仍在Trivy的对象存储中。它声称由"DmitriyLewen"(真实的Aqua维护者)创作,但没有签名。合法的DmitriyLewen提交通过GitHub web合并进行,由GitHub进行GPG签名。
$ gh api repos/aquasecurity/trivy/git/commits/1885610c6a34811c8296416ae69f568002ef11ec \
--jq '{author: .author.name, email: .author.email, message: .message,
verified: .verification.verified, reason: .verification.reason}'
{
"author": "DmitriyLewen",
"email": "[email protected]",
"message": "fix(ci): Use correct checkout pinning",
"verified": false,
"reason": "unsigned"
}setup-trivy恶意提交也存在相同的模式,该提交冒充了"Tomochika Hara"(thara):
$ gh api repos/aquasecurity/setup-trivy/git/commits/8afa9b9f9183b4e00c46e2b82d34047e3c177bd0 \
--jq '{author: .author.name, verified: .verification.verified, reason: .verification.reason}'
{
"author": "Tomochika Hara",
"verified": false,
"reason": "unsigned"
}setup-trivy:仅一个标签存留
标签v0.2.0至v0.2.5在修复后未恢复。只有干净的v0.2.6保留下来,匹配Aqua的安全公告:
$ gh api repos/aquasecurity/setup-trivy/tags --jq '.[].name'
v0.2.6setup-trivy事件API实时捕获了事件响应:
$ gh api repos/aquasecurity/setup-trivy/events \
--jq '[.[] | {type, actor: .actor.login, created_at,
ref: .payload.ref, release: .payload.release.tag_name}]'
# ...
# 21:07 UTC - nikpivkin deletes v0.2.5 tag (incident response begins)
# 21:34 UTC - itaysk adds emergency collaborator
# 21:43 UTC - simar7 publishes clean v0.2.6 releasetrivy-action标签现在全部带有v前缀
原始无前缀标签(0.0.1至0.34.2)在修复期间被删除,无法重新创建,因为GitHub的不可变release功能锁定了攻击者强制推送的版本。现在存在74个标签,全部带有v前缀:
$ gh api repos/aquasecurity/trivy-action/tags --paginate \
--jq '.[].name' | wc -l
74
$ gh api repos/aquasecurity/trivy-action/tags --paginate \
--jq '.[].name' | sort -V | tail -5
v0.33.0
v0.33.1
v0.34.0
v0.35.0 # ← only safe tag (pointed to master HEAD during attack)死滴窃取方法有效
Wiz的分析描述了一个备用通道,其中恶意软件在受害者GitHub账户上创建tpcp-docs仓库。这些仓库可通过公开搜索找到:
$ gh search repos "tpcp-docs" --json fullName,createdAt
[
{"createdAt": "2026-03-22T18:30:12Z", "fullName": "kaufmann-digital/tpcp-docs"},
{"createdAt": "2026-03-22T21:59:30Z", "fullName": "BEUMERGroupBot/tpcp-docs"},
{"createdAt": "2026-03-22T23:08:47Z", "fullName": "cloud-team-si-it/tpcp-docs"},
{"createdAt": "2026-03-22T23:25:10Z", "fullName": "dhoppe/tpcp-docs"}
]全部创建于2026年3月22日。如果您在组织中找到tpcp-docs仓库,请假设该工作流运行中的所有机密信息已被泄露。
讨论区垃圾信息可见
讨论 #10420有772条评论。API显示协调机器人账户在同一时间戳发送的相同消息:
$ gh api "repos/aquasecurity/trivy/discussions/10420" \
--jq '{title, comments}'
{"title": "Why did this discussion about the Trivy incident get removed/closed",
"comments": 772}
$ gh api "repos/aquasecurity/trivy/discussions/10420/comments?per_page=5" \
--jq '[.[] | {created_at, body: .body[0:60]}]'
[
{"created_at": "2026-03-19T23:56:52Z", "body": "To be explicit, it wasn't just discussion on the previous..."},
{"created_at": "2026-03-20T00:01:16Z", "body": "sugma and ligma, teampcp owns you"},
{"created_at": "2026-03-20T00:01:17Z", "body": "sugma and ligma, teampcp owns you"},
{"created_at": "2026-03-20T00:08:33Z", "body": "this solved my issue, thanks"},
{"created_at": "2026-03-20T00:08:33Z", "body": "thanks for the detailed explanation"}
]相同时间戳(00:08:33 UTC)的相同消息来自协调的机器人账户。
tfsec和traceeshark确认横向移动
两个仓库在3月19日显示pushed_at时间戳,与Wiz记录的aqua-bot注入一致:
$ gh api orgs/aquasecurity/repos --paginate \
--jq '.[] | select(.name | test("tfsec$|traceeshark")) | {name, pushed_at}'
{"name": "tfsec", "pushed_at": "2026-03-19T23:03:55Z"}
{"name": "traceeshark", "pushed_at": "2026-03-19T22:56:37Z"}更大的图景
这次活动是当前供应链攻击的基准。TeamPCP(也被追踪为PCPcat、ShellForce、DeadCatx3)将AI辅助侦察、CI/CD凭证窃取、自我传播的跨生态系统蠕虫、去中心化C2基础设施和破坏性载荷整合为单一行动。他们在5小时内迭代了6个载荷版本,比大多数组织召集事件响应团队的速度还要快。
令人不安的教训:一个配置错误的GitHub Actions工作流导致了一条链式反应,入侵了一家安全供应商的二进制分发系统、75个被10,000+个工作流使用的GitHub Action标签、64+个npm包、Docker Hub镜像和内部基础设施。CI/CD凭证泄露的影响范围不是线性的,而是复合的。
- supply-chain-security
- trivy
- github-actions
- npm
- ci-cd-security
- incident-response
- malware
来自SafeDep博客的最新内容
关注以获取开源安全与工程的最新更新和洞察