vulnerabilities rfc1867 callback uaf

PHPコアUse-After-Free徹底解剖: RFC1867アップロード進捗コールバックが引き起こすダングリングポインタの深層

php2shell.run Research Lab •

PHPコアUse-After-Free徹底解剖: RFC1867アップロード進捗コールバックが引き起こすダングリングポインタの深層

<time datetime="2026-10-23">検証日: 2026年10月23日</time> · ソースコード監査対象: php-src/ext/session/session.c, php-src/main/rfc1867.c

PHP2Shellの核となる脆弱性の1つである CVE-2026-109619 は、長年にわたり安全と考えられていたPHPランタイムのRFC1867(HTMLフォームによるファイルアップロード仕様)実装とセッション管理機構(ext/session)の境界領域に潜んでいました。

本稿では、PHP C言語ソースコードの行レベルトレースを通じ、どのようにして未初期化または解放済みメモリ領域へのポインタ(Dangling Pointer)が生じ、最終的なUse-After-Free(UAF)および任意コード実行へと発展するのか、その完全なメカニズムを解き明かします。


1. RFC1867 マルチパートコールバックの設計思想と欠陥

PHP 5.4において導入された session.upload_progress 機構は、フロントエンド開発者がJavaScriptを通じてファイルアップロードの進捗状況をリアルタイムにポーリングできるように設計されました。

この処理を実現するため、PHPのコアSAPIは main/rfc1867.c においてマルチパートストリームを解析する過程で、登録された各拡張モジュールのコールバック関数を順次呼び出します。セッション拡張モジュールは php_session_rfc1867_callback を登録し、特定のフォームフィールド(PHP_SESSION_UPLOAD_PROGRESS)を監視します。

脆弱なコールバック関数の登録構造 (ext/session/session.c)

/* ext/session/session.c より抜粋 */
static int php_session_rfc1867_callback(unsigned int event, void *event_data, void **extra)
{
    php_session_rfc1867_data *data = (php_session_rfc1867_data *)*extra;

    switch (event) {
        case MULTIPART_EVENT_START:
            /* セッション進捗構造体の初期化 */
            data = emalloc(sizeof(php_session_rfc1867_data));
            PS(rfc1867_data) = data;
            break;

        case MULTIPART_EVENT_FORMDATA: {
            multipart_event_formdata *data = (multipart_event_formdata *)event_data;
            if (strcasecmp(data->name, PS(upload_progress_name)) == 0) {
                /* 【脆弱性の種】グローバルPSマクロ配下のポインタに生参照を退避 */
                PS(cached_post_bytes) = data->value;
                PS(cached_post_len)   = data->length;
            }
            break;
        }
        /* ... */
    }
    return 0;
}

2. 状態の不整合: longjmpとメモリ枯渇(OOM)の交差点

問題の本質は、C言語における例外処理機構である zend_bailout() やメモリ枯渇(Out-Of-Memory)が発生した際のクリーンアップ処理の非対称性にあります。

 正常系フロー:
   [ POSTデータ受信 ] ──> [ コールバック呼び出し ] ──> [ $_SESSION更新 ] ──> [ メモリ正常解放 ]

 異常系フロー (CVE-2026-109618 / 109619):
   [ 悪意のある特大Chunk ]
            │
            ▼
   [ Zend MM ヒープ確保失敗 / post_max_size 超過 ]
            │
            ▼
   [ zend_bailout() 実行 (内部で setjmp/longjmp) ]
            │
            ▼
   スタックフレームが巻き戻され、ローカル変数破棄
   しかし、PS(cached_post_bytes) は依然として
   解放済みチャンクのアドレスを指したまま残留!
            │
            ▼
   【結果】Dangling Pointer(浮遊ポインタ)が生成される

Zendエンジンは致命的エラーが発生した際、Cの longjmp を呼び出してスタックを巻き戻し、リクエストのシャットダウンフェーズ(RSHUTDOWN)へと強制遷移します。この時、セッション管理モジュールの破棄ルーチン php_rshutdown_session() が呼び出されますが、グローバル変数構造体にキャッシュされた PS(cached_post_bytes) のポインタ整合性チェックが欠落していました。


3. C言語コードによる詳細検証: なぜポインタが無効化されないのか

以下のコードは、リクエスト終了時の破棄処理ルーチンです:

/* ext/session/session.c :: php_rshutdown_session_globals() */
PHP_RSHUTDOWN_FUNCTION(session)
{
    if (PS(session_status) != php_session_none) {
        php_session_flush(execute_data);
    }

    /* 
     * 本来であればここで以下が実行されなければならない:
     * if (PS(cached_post_bytes)) {
     *     PS(cached_post_bytes) = NULL;
     * }
     * しかし、このクリーンアップ処理が完全に欠落していた。
     */

    return SUCCESS;
}

この結果、後続のHTTP接続を同一のワーカースレッド(FastCGIの永続ワーカー)で処理する際、あるいは同一リクエストのクリーンアップハンドラが実行される際、すでに別の用途に再利用されたメモリブロックが「有効なセッションデータ」として解釈されてしまいます。


4. Zend Memory Manager (ZMM) ヒープ再利用の悪用

攻撃者は、この浮遊ポインタが存在する状態で、巧妙にサイズを調整したマルチパートリクエストを連続して送信します。

  1. メモリ解放の誘発: 脆弱なコールバックを実行させ、ポインタを生成した直後に異常終了を発生させてバッファを解放させる。
  2. ビン(Bin)の再割り当て: Zendヒープアロケータ(ZMM)は、解放されたばかりの直前のメモリスロットを最優先で再利用(LIFO動作)します。
  3. 偽装zvalの注入: 攻撃者が送信した文字列データが、ちょうど解放されたばかりのメモリアドレスに配置されます。
  4. 型偽装(Type Confusion)の成立: エンジン側はこれを zend_string や zval 構造体として読み出し、ポインタを間接参照して関数の実行アドレスへとジャンプします。

5. 公式パッチの構造的検証

PHP開発コミュニティによってコミットされた公式修正差分は以下の通りです。

/* ext/session/session.c 修正コミット抜粋 */
- PS(cached_post_bytes) = data->value;
+ if (PS(cached_post_bytes)) {
+     efree(PS(cached_post_bytes));
+     PS(cached_post_bytes) = NULL;
+ }
+ PS(cached_post_bytes) = estrndup(data->value, data->length);

単に生のアドレスをキャッシュするのではなく、estrndup() によりセーフティバッファを独立確保し、破棄フェーズで厳格に解放とNULLクリアを行うよう設計変更されました。この修正により、未定義動作およびUse-After-Freeの発生経路が完全に遮断されます。