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://.
Member discussion