4 min read

GitHub Enumeration 101 Writeup (CyberWarFare Labs)

GitHub Enumeration 101 Writeup (CyberWarFare Labs)
📚 Series · CyberWarFare Labs · CI/CD Security
  1. ▸ GitHub Enumeration 101 — full recon from a single PAT (this post)
  2. GCP CI/CD 01 — private repo to Cloud Run service name

Challenge Info

  • Platform: CyberWarFare Labs (CWL)
  • Category: CI/CD Security / GitHub CI/CD
  • Difficulty: Easy (10 points)
  • What you're given: a single GitHub Personal Access Token

This challenge doesn't actually ask you to do a full enumeration like I'm about to do here — all it wants is the SHA hash of the first commit. But walking through the whole thing end to end is genuinely worth it! So if you just want the answer, feel free to jump straight to Step 6.

Walkthrough

The challenge hands you a single token, and the goal is to use that one key to do credential-driven enumeration and figure out exactly what it can reach.

Step 1: Identify the token type

First, look at the token prefix:

github_pat_XXXXXXXXXXXXXXXXXXXXXXXX

A github_pat_ prefix means it's a fine-grained PAT (not a classic ghp_ token). The difference: a fine-grained PAT is bound to specific repos/permissions, and it doesn't spell out its scopes in the response headers — you have to infer its permissions empirically.

Step 2: Confirm identity and that the token is alive

TOKEN="[REDACTED_PAT]"

curl -si -H "Authorization: Bearer $TOKEN" https://api.github.com/user

The key parts of the response:

HTTP/2 200
x-accepted-github-permissions: allows_permissionless_access=true

"login": "secure-corp-devops-operator"
"public_repos": 0

The token is alive (200), its owner is secure-corp-devops-operator, and it has 0 public repos (all the value is on the private side).

Note that allows_permissionless_access=true describes the /user endpoint itself — it means "no specific permission is required to access this endpoint" — it does not mean the token has unlimited permissions.

Step 3: Enumerate the permission boundary

curl -s -H "Authorization: Bearer $TOKEN" https://api.github.com/user/orgs
curl -s -H "Authorization: Bearer $TOKEN" "https://api.github.com/user/repos?per_page=100&visibility=all"

/user/orgs returns an empty array (no orgs). /user/repos returns a single private repo — looking only at the key fields:

"full_name": "secure-corp-devops-operator/GitHub-Enumeration-101",
"private": true,
"permissions": { "admin": true, "maintain": true, "push": true, "triage": true, "pull": true }

The permissions block is handed to us directly by the API: this token has admin rights over that repo.

Step 4: Take inventory of the repo's files

curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.github.com/repos/secure-corp-devops-operator/GitHub-Enumeration-101/contents/

The most suspicious items in the listing are infinity.key (an SSL private key) and infinity.crt (a certificate), followed by the Dockerfile. The ranking criterion: how valuable each file would be to an attacker if it leaked.

Step 5: Read the files and cross-verify

Use Accept: application/vnd.github.raw to pull the raw plaintext (skipping the default base64 decoding):

curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.github.raw" \
  https://api.github.com/repos/secure-corp-devops-operator/GitHub-Enumeration-101/contents/infinity.key

infinity.key is a complete, usable RSA private key (-----BEGIN RSA PRIVATE KEY----------END-----).

Then read the Dockerfile to cross-check what it's used for:

RUN apt install apache2 ...
COPY infinity.key /etc/ssl/private/
RUN a2enmod ssl
ENV PORT 443

This confirms that infinity.key is the SSL private key of an actual production HTTPS server.

Step 6: Dig through the commit history for the first commit

The challenge asks: What is the hash of the very first commit made into the repository?

The files currently in the repo are just the surface — the commit history is where it's at. Old versions live forever, so after cloning, use --reverse to bring the oldest commit to the top:

git clone https://[email protected]/secure-corp-devops-operator/GitHub-Enumeration-101.git
cd GitHub-Enumeration-101
git log --oneline --reverse
f224060 Initial Commit          <- the first commit
89e6955 Added webserver config
a678f0c Added TLS certificates
...

--oneline only shows the 7-character abbreviation. To get the full 40-character hash, cross-reference against the API's commit list (by default the API returns newest-to-oldest, so the first commit sits at the very bottom):

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.github.com/repos/secure-corp-devops-operator/GitHub-Enumeration-101/commits?per_page=100" | grep '"sha"'

Both approaches cross-verify to the same commit.

Flag:

f224060XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Key Takeaways

  • The rhythm of credential-driven enumeration: first confirm identity and that the token is alive (/user), then work from the outside in to narrow the scope (owner → org → repo → permissions), where each step's answer decides the next one.
  • A fine-grained PAT doesn't expose its scopes in the headers, but the repo object's permissions block hands you the permission map directly — no guessing required.
  • allows_permissionless_access is a property of the endpoint, not of the token's permissions — don't misread it.
  • Prioritizing files: ask "if this file leaked, what could an attacker do with it?" Private keys, certificates, and hardcoded secrets go to the top. A single clue isn't enough — pairing the private key with the Dockerfile is what pins down its purpose.
  • A private key committed to git is effectively burned — old versions stay in the history forever, and the earliest commit is the one most likely to hide a secret.
  • A leaked SSL private key can be used to decrypt captured HTTPS traffic, or to impersonate the server and mount a MITM attack.

Handy Tricks

# Read GitHub file contents as raw plaintext (skip the base64 decode)
curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.github.raw" \
  https://api.github.com/repos/[owner]/[repo]/contents/[path]

# Pinpoint which commit introduced a secret
git show [commit]

# Secret scanning (catch credentials before they get pushed)
gitleaks detect --source .
trufflehog git file://.