|
| 1 | +# AWS CodeBuild - Untrusted PR Webhook Bypass (CodeBreach-style) |
| 2 | + |
| 3 | +{{#include ../../../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +This attack vector appears when a **public-facing PR workflow** is wired to a **privileged CodeBuild project** with weak webhook controls. |
| 6 | + |
| 7 | +If an external attacker can make CodeBuild execute their pull request, they can usually get **arbitrary code execution inside the build** (build scripts, dependency hooks, test scripts, etc.), and then pivot to secrets, IAM credentials, or source-provider credentials. |
| 8 | + |
| 9 | +## Why this is dangerous |
| 10 | + |
| 11 | +CodeBuild webhook filters are evaluated with regex patterns (for non-`EVENT` filters). In the `ACTOR_ACCOUNT_ID` filter, this means a weak pattern can match more users than intended. |
| 12 | +If untrusted PRs are built in a project that has privileged AWS role permissions or GitHub credentials, this can become a full supply-chain compromise. |
| 13 | + |
| 14 | +Wiz showed a practical chain where: |
| 15 | + |
| 16 | +1. A webhook actor allowlist used an **unanchored regex**. |
| 17 | +2. An attacker registered a GitHub ID that matched as a **superstring** of a trusted ID. |
| 18 | +3. A malicious PR triggered CodeBuild. |
| 19 | +4. Build code execution was used to dump memory and recover source-provider credentials/tokens. |
| 20 | + |
| 21 | +## Misconfigurations that allow external PR code execution |
| 22 | + |
| 23 | +The following are high-risk mistakes and how attackers abuse each one: |
| 24 | + |
| 25 | +1. **`EVENT` filters allow untrusted triggers** |
| 26 | + - Common risky events: `PULL_REQUEST_CREATED`, `PULL_REQUEST_UPDATED`, `PULL_REQUEST_REOPENED`. |
| 27 | + - Other events that can also become dangerous if tied to privileged builds: `PUSH`, `PULL_REQUEST_CLOSED`, `PULL_REQUEST_MERGED`, `RELEASED`, `PRERELEASED`, `WORKFLOW_JOB_QUEUED`. |
| 28 | + - Bad: `EVENT="PUSH, PULL_REQUEST_CREATED, PULL_REQUEST_UPDATED"` in a privileged project. |
| 29 | + - Better: use PR comment approval and minimize trigger events for privileged projects. |
| 30 | + - Abuse: attacker opens/updates PR or pushes to a branch they control, and their code executes in CodeBuild. |
| 31 | + |
| 32 | +2. **`ACTOR_ACCOUNT_ID` regex is weak** |
| 33 | + - Bad: unanchored patterns like `123456|7890123`. |
| 34 | + - Better: exact-match anchoring `^(123456|7890123)$`. |
| 35 | + - Abuse: regex over-match allows unauthorized GitHub IDs to pass allowlists. |
| 36 | + |
| 37 | +3. **Other regex filters are weak or missing** |
| 38 | + - `HEAD_REF` |
| 39 | + - Bad: `refs/heads/.*` |
| 40 | + - Better: `^refs/heads/main$` (or an explicit trusted list) |
| 41 | + - `BASE_REF` |
| 42 | + - Bad: `.*` |
| 43 | + - Better: `^refs/heads/main$` |
| 44 | + - `FILE_PATH` |
| 45 | + - Bad: no path restrictions |
| 46 | + - Better: exclude risky files like `^buildspec\\.yml$`, `^\\.github/workflows/.*`, `(^|/)package(-lock)?\\.json$` |
| 47 | + - `COMMIT_MESSAGE` |
| 48 | + - Bad: trust marker with loose match like `trusted` |
| 49 | + - Better: do not use commit message as a trust boundary for PR execution |
| 50 | + - `REPOSITORY_NAME` / `ORGANIZATION_NAME` |
| 51 | + - Bad: `.*` in org/global webhooks |
| 52 | + - Better: exact repo/org matches only |
| 53 | + - `WORKFLOW_NAME` |
| 54 | + - Bad: `.*` |
| 55 | + - Better: exact workflow name matches only (or avoid this as trust control) |
| 56 | + - Abuse: attacker crafts ref/path/message/repo context to satisfy permissive regex and trigger builds. |
| 57 | + |
| 58 | +4. **`excludeMatchedPattern` is misused** |
| 59 | + - Setting this flag incorrectly can invert intended logic. |
| 60 | + - Bad: `FILE_PATH '^buildspec\\.yml$'` with `excludeMatchedPattern=false` when intent was to block buildspec edits. |
| 61 | + - Better: same pattern with `excludeMatchedPattern=true` to deny builds touching `buildspec.yml`. |
| 62 | + - Abuse: defenders think they deny risky events/paths/actors, but actually allow them. |
| 63 | + |
| 64 | +5. **Multiple `filterGroups` create accidental bypasses** |
| 65 | + - CodeBuild evaluates groups as OR (one passing group is enough). |
| 66 | + - Bad: one strict group + one permissive fallback group (e.g., only `EVENT=PULL_REQUEST_UPDATED`). |
| 67 | + - Better: remove fallback groups that do not enforce actor/ref/path constraints. |
| 68 | + - Abuse: attacker only needs to satisfy the weakest group. |
| 69 | + |
| 70 | +6. **Comment approval gate disabled or too permissive** |
| 71 | + - `pullRequestBuildPolicy.requiresCommentApproval=DISABLED` is least safe. |
| 72 | + - Overly broad approver roles reduce the control. |
| 73 | + - Bad: `requiresCommentApproval=DISABLED`. |
| 74 | + - Better: `ALL_PULL_REQUESTS` or `FORK_PULL_REQUESTS` with minimal approver roles. |
| 75 | + - Abuse: fork/drive-by PRs auto-run without trusted maintainer approval. |
| 76 | + |
| 77 | +7. **No restrictive branch/path strategy for PR builds** |
| 78 | + - Missing defense-in-depth with `HEAD_REF` + `BASE_REF` + `FILE_PATH`. |
| 79 | + - Bad: only `EVENT` + `ACTOR_ACCOUNT_ID`, no ref/path controls. |
| 80 | + - Better: combine exact `ACTOR_ACCOUNT_ID` + `BASE_REF` + `HEAD_REF` + `FILE_PATH` restrictions. |
| 81 | + - Abuse: attacker modifies build inputs (buildspec/CI/dependencies) and gets arbitrary command execution. |
| 82 | + |
| 83 | +8. **Public visibility + status URL exposure** |
| 84 | + - Public build/check URLs improve attacker recon and iterative testing. |
| 85 | + - Bad: `projectVisibility=PUBLIC_READ` with sensitive logs/config in public builds. |
| 86 | + - Better: keep projects private unless there is a strong business need, and sanitize logs/artifacts. |
| 87 | + - Abuse: attacker discovers project patterns/behavior, then tunes payloads and bypass attempts. |
| 88 | + |
| 89 | +## Token leakage from memory |
| 90 | + |
| 91 | +Wiz's write-up explains that source-provider credentials are present in build runtime context and can be stolen after build compromise (for example, via memory dumping), enabling repository takeover if scopes are broad. |
| 92 | + |
| 93 | +AWS introduced hardening after the disclosure, but the core lesson remains: **never execute untrusted PR code in privileged build contexts** and assume attacker-controlled build code will attempt credential theft. |
| 94 | + |
| 95 | +For additional credential theft techniques in CodeBuild, also check: |
| 96 | + |
| 97 | +{{#ref}} |
| 98 | +aws-codebuild-token-leakage.md |
| 99 | +{{#endref}} |
| 100 | + |
| 101 | +## Finding CodeBuild URLs in GitHub PRs |
| 102 | + |
| 103 | +If CodeBuild reports commit status back to GitHub, the CodeBuild build URL usually appears in: |
| 104 | + |
| 105 | +1. **PR page** -> **Checks** tab (or the status line in Conversation/Commits). |
| 106 | +2. **Commit page** -> status/checks section -> **Details** link. |
| 107 | +3. **PR commits list** -> click the check context attached to a commit. |
| 108 | + |
| 109 | +For public projects, this link can expose build metadata/configuration to unauthenticated users. |
| 110 | + |
| 111 | +<details> |
| 112 | +<summary>Script: detect CodeBuild URLs in a PR and test if they look public</summary> |
| 113 | + |
| 114 | +```bash |
| 115 | +#!/usr/bin/env bash |
| 116 | +set -euo pipefail |
| 117 | + |
| 118 | +# Usage: |
| 119 | +# ./check_pr_codebuild_urls.sh <owner> <repo> <pr_number> |
| 120 | +# |
| 121 | +# Requirements: gh, jq, curl |
| 122 | + |
| 123 | +OWNER="${1:?owner}" |
| 124 | +REPO="${2:?repo}" |
| 125 | +PR="${3:?pr_number}" |
| 126 | + |
| 127 | +for bin in gh jq curl timeout; do |
| 128 | + command -v "$bin" >/dev/null || { echo "[!] Missing dependency: $bin" >&2; exit 1; } |
| 129 | +done |
| 130 | + |
| 131 | +tmp_commits="$(mktemp)" |
| 132 | +tmp_urls="$(mktemp)" |
| 133 | +trap 'rm -f "$tmp_commits" "$tmp_urls"' EXIT |
| 134 | + |
| 135 | +gh_api() { |
| 136 | + timeout 20s gh api "$@" 2>/dev/null || true |
| 137 | +} |
| 138 | + |
| 139 | +# Get all commit SHAs in the PR (bounded call to avoid hangs) |
| 140 | +gh_api "repos/${OWNER}/${REPO}/pulls/${PR}/commits" --paginate --jq '.[].sha' > "$tmp_commits" |
| 141 | +if [ ! -s "$tmp_commits" ]; then |
| 142 | + echo "[!] No commits found (or API call timed out/failed)." >&2 |
| 143 | + exit 1 |
| 144 | +fi |
| 145 | + |
| 146 | +echo "[*] PR commits:" |
| 147 | +cat "$tmp_commits" |
| 148 | +echo |
| 149 | + |
| 150 | +echo "[*] Searching commit statuses/check-runs for CodeBuild URLs..." |
| 151 | + |
| 152 | +while IFS= read -r sha; do |
| 153 | + [ -z "$sha" ] && continue |
| 154 | + |
| 155 | + # Classic commit statuses (target_url) |
| 156 | + gh_api "repos/${OWNER}/${REPO}/commits/${sha}/status" \ |
| 157 | + --jq '.statuses[]? | .target_url // empty' 2>/dev/null || true |
| 158 | + |
| 159 | + # GitHub Checks API (details_url) |
| 160 | + gh_api "repos/${OWNER}/${REPO}/commits/${sha}/check-runs" \ |
| 161 | + --jq '.check_runs[]? | .details_url // empty' 2>/dev/null || true |
| 162 | +done < "$tmp_commits" | sort -u > "$tmp_urls" |
| 163 | + |
| 164 | +grep -Ei 'codebuild|codebuild\.aws\.amazon\.com|console\.aws\.amazon\.com/.*/codebuild' "$tmp_urls" || true |
| 165 | + |
| 166 | +echo |
| 167 | +echo "[*] Public-access heuristic:" |
| 168 | +echo " - If URL redirects to signin.aws.amazon.com -> likely not public" |
| 169 | +echo " - If URL is directly reachable (HTTP 200) without auth redirect -> potentially public" |
| 170 | +echo |
| 171 | + |
| 172 | +cb_urls="$(grep -Ei 'codebuild|codebuild\.aws\.amazon\.com|console\.aws\.amazon\.com/.*/codebuild' "$tmp_urls" || true)" |
| 173 | +if [ -z "$cb_urls" ]; then |
| 174 | + echo "[*] No CodeBuild URLs found in PR statuses/check-runs." |
| 175 | + exit 0 |
| 176 | +fi |
| 177 | + |
| 178 | +while IFS= read -r url; do |
| 179 | + [ -z "$url" ] && continue |
| 180 | + final_url="$(timeout 20s curl -4 -sS -L --connect-timeout 5 --max-time 20 -o /dev/null -w '%{url_effective}' "$url" || true)" |
| 181 | + code="$(timeout 20s curl -4 -sS -L --connect-timeout 5 --max-time 20 -o /dev/null -w '%{http_code}' "$url" || true)" |
| 182 | + |
| 183 | + if echo "$final_url" | grep -qi 'signin\.aws\.amazon\.com'; then |
| 184 | + verdict="NOT_PUBLIC_OR_AUTH_REQUIRED" |
| 185 | + elif [ "$code" = "200" ]; then |
| 186 | + verdict="POTENTIALLY_PUBLIC" |
| 187 | + else |
| 188 | + verdict="UNKNOWN_CHECK_MANUALLY" |
| 189 | + fi |
| 190 | + |
| 191 | + printf '%s\t%s\t%s\n' "$verdict" "$code" "$url" |
| 192 | +done <<< "$cb_urls" |
| 193 | +``` |
| 194 | + |
| 195 | +Tested working with: |
| 196 | + |
| 197 | +```bash |
| 198 | +bash /tmp/check_pr_codebuild_urls.sh carlospolop codebuild-codebreach-ctf-lab 1 |
| 199 | +``` |
| 200 | + |
| 201 | +</details> |
| 202 | + |
| 203 | +## Quick audit checklist |
| 204 | + |
| 205 | +```bash |
| 206 | +# Enumerate projects |
| 207 | +aws codebuild list-projects |
| 208 | + |
| 209 | +# Inspect source/webhook configuration |
| 210 | +aws codebuild batch-get-projects --names <project-name> |
| 211 | + |
| 212 | +# Inspect global source credentials configured in account |
| 213 | +aws codebuild list-source-credentials |
| 214 | +``` |
| 215 | + |
| 216 | +Review each project for: |
| 217 | + |
| 218 | +- `webhook.filterGroups` containing PR events. |
| 219 | +- `ACTOR_ACCOUNT_ID` patterns that are not anchored with `^...$`. |
| 220 | +- `pullRequestBuildPolicy.requiresCommentApproval` equal to `DISABLED`. |
| 221 | +- Missing branch/path restrictions. |
| 222 | +- High-privilege `serviceRole`. |
| 223 | +- Risky source credentials scope and reuse. |
| 224 | + |
| 225 | +## Hardening guidance |
| 226 | + |
| 227 | +1. Require comment approval for PR builds (`ALL_PULL_REQUESTS` or `FORK_PULL_REQUESTS`). |
| 228 | +2. If using actor allowlists, anchor regexes and keep them exact. |
| 229 | +3. Add `FILE_PATH` restrictions to avoid untrusted edits to `buildspec.yml` and CI scripts. |
| 230 | +4. Separate trusted release builds from untrusted PR builds into different projects/roles. |
| 231 | +5. Use fine-grained, least-privileged source-provider tokens (prefer dedicated low-privilege identities). |
| 232 | +6. Continuously audit webhook filters and source credential usage. |
| 233 | + |
| 234 | +## References |
| 235 | + |
| 236 | +- [Wiz: CodeBreach - AWS CodeBuild ACTOR_ID regex bypass and token theft](https://www.wiz.io/blog/wiz-research-codebreach-vulnerability-aws-codebuild) |
| 237 | +- [AWS CodeBuild API - WebhookFilter](https://docs.aws.amazon.com/codebuild/latest/APIReference/API_WebhookFilter.html) |
| 238 | +- [AWS CLI - codebuild create-webhook](https://docs.aws.amazon.com/cli/latest/reference/codebuild/create-webhook.html) |
| 239 | +- [AWS CodeBuild User Guide - Best practices for webhooks](https://docs.aws.amazon.com/codebuild/latest/userguide/webhooks.html) |
| 240 | + |
| 241 | +{{#include ../../../../banners/hacktricks-training.md}} |
0 commit comments