4 min read

GitHub Enumeration 101 writeup (zh-TW)

GitHub Enumeration 101 writeup (zh-TW)

題目資訊

  • 平台: CyberWarFare Labs (CWL)
  • 分類: CI/CD Security / GitHub CI/CD
  • 難度: Easy (10 points)
  • 題目給予: 一組 GitHub Personal Access Token

這個題目並沒有要求要像筆者這樣完整的enumeration,題目要求很簡單就是要第一個commit的sha hash值而已,但是完整走一遍還是會很有收獲的喔! 所以如果要答案,可以直接跳到Step 6即可。

解題過程

題目只給一組 token,目標是靠這把鑰匙做 credential-driven enumeration,摸出它能碰到什麼。

Step 1: Token 型號判讀

先看 token 前綴:

github_pat_XXXXXXXXXXXXXXXXXXXXXXXX

github_pat_ 開頭為 fine-grained PAT(非 classic 的 ghp_),差別在於 fine-grained 綁定特定 repo/權限,且不會在回應 header 明列 scope,權限得靠實測反推。

Step 2: 身分與存活確認

TOKEN="[REDACTED_PAT]"

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

回應重點:

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

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

token 存活(200),擁有者為 secure-corp-devops-operator,公開 repo 為 0(價值全在私有側)。

注意 allows_permissionless_access=true 是描述 /user 這個 endpoint 本身「不需特定權限即可存取」,並非代表 token 有無限權限。

Step 3: 權限邊界列舉

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 回傳空陣列(無 org)。/user/repos 回傳一個私有 repo,只看關鍵欄位:

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

permissions 區塊由 API 直接給出,這把 token 對該 repo 有 admin 權限。

Step 4: repo 檔案盤點

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

清單中最可疑者為 infinity.key(SSL 私鑰)與 infinity.crt(憑證),其次 Dockerfile,排序依據:該檔外流後對攻擊者的價值。

Step 5: 讀檔與交叉印證

Accept: application/vnd.github.raw 取原始純文字(省去預設 base64 解碼):

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 為完整可用的 RSA 私鑰(-----BEGIN RSA PRIVATE KEY----------END-----)。

再讀 Dockerfile 交叉印證用途:

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

確認 infinity.key 即為某台正式 HTTPS 伺服器的 SSL 私鑰。

Step 6: 翻 commit 歷史取第一個 commit

題目問: What is the hash of the very first commit made into the repository?

現有檔案只是表面,commit 歷史才是重點——舊版本永遠留存,clone 後用 --reverse 讓最舊的排最前:

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

--oneline 僅顯示 7 碼縮寫,取完整 40 碼可用 API commit 清單交叉核對(API 預設由新到舊,第一個 commit 在最底):

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

兩法互驗指向同一 commit。

Flag:

f224060XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

學習重點

  • credential-driven enumeration 節奏: 先確認身分與存活(/user),再由外而內縮小範圍(擁有者 → org → repo → 權限),每一步的答案決定下一步。
  • fine-grained PAT 不在 header 自曝 scope,但 repo 物件的 permissions 區塊會直接給出權限地圖,不需瞎猜。
  • allows_permissionless_access 是 endpoint 屬性,非 token 權限,勿誤讀。
  • 檔案優先序判斷: 問「此檔外流攻擊者能拿去幹嘛」,私鑰/憑證/寫死 secret 排最前;單一線索不夠,私鑰配 Dockerfile 才確定其用途。
  • 私鑰進 git 等於作廢——舊版本永遠留在歷史,最早的 commit 最常藏 secret。
  • SSL 私鑰外流可用於解密側錄的 HTTPS 流量,或偽裝伺服器發動 MITM。

實用技巧

# 讀 GitHub 檔案內容取原始純文字(免 base64 解碼)
curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.github.raw" \
  https://api.github.com/repos/[owner]/[repo]/contents/[path]

# 定位 secret 是哪個 commit 引入的
git show [commit]

# secret 掃描(push 前攔截誤入的憑證)
gitleaks detect --source .
trufflehog git file://.