
ProFTPD の CVE-2026-42167 を実証する PoC
CVE-2026-42167 の概念実証デモです。これは ProFTPD の mod_sql ロギングパイプラインにおける SQL インジェクションの脆弱性で、認証されていない攻撃者が任意の SQL を実行したり、FTP 認証データベースにバックドアユーザーを注入したり、データベースホスト上でリモートコード実行を達成したりできます。
すべての POC は PostgreSQL 固有ですが、バックドアユーザー関連の POC は、注入するクエリを一部変更すれば MySQL および sqlite バックエンドでも動作します(変更方法は読者の課題として残しています)。
is_escaped_text() バイパス (CVE-2026-42167, CWE-89)ProFTPD の mod_sql は、SQLLog / SQLNamedQuery メカニズムを通じてすべての FTP コマンドをログに記録します。%U(元のユーザー名)や %{basename}(ファイル名コンポーネント)のようなフォーマット変数を解決するとき、フレームワークは is_escaped_text() を呼び出してエスケープが必要かどうかを判断します。そして、値が単一引用符で始まり単一引用符で終わり、内部に単一引用符を含まない場合(例: '|| (SELECT 1) ||')、エスケープを完全にスキップします。このチェックは純粋に構文上のものであり、FTP セッションからの攻撃者が制御する生の入力に対して実行されるため、「信頼できるコードによって事前にエスケープされた」値と「攻撃者が事前にエスケープされたように見せかけて作成した」値を区別できません。
標準的なドキュメント化されたパターンでは、SQL の安全性のためにフォーマット変数を単一引用符で囲みます。
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog * log_activity
SQLLog ERR_* log_activity
攻撃者が '<payload>' の形式の値を提供すると、置換によって最終的な SQL に ''<payload>'' が生成されます。空文字列リテラルが周囲の引用符を閉じ、ペイロードが生の SQL として実行されます。PostgreSQL(PQexec)または SQLite(sqlite3_exec)では、スタッククエリがサポートされているため、ペイロードを完全な INSERT、UPDATE、CREATE TABLE、または COPY TO PROGRAM ステートメントにすることができます。
脆弱なコードは特定のバックエンドではなく contrib/mod_sql.c(共有 SQL フレームワーク)に存在するため、すべての SQL バックエンドが影響を受けます。ただし、攻撃対象領域は管理者がロギングをどのように構成したかによって異なります。サーバーが悪用可能となるのは、以下の 両方 が当てはまる場合です。
管理者が SQLNamedQuery INSERT(または UPDATE)を定義しており、そのフォーマット文字列が、単一引用符で囲まれた次の攻撃者が制御可能な変数のいずれかを補間している場合 — 例: "'%U', '%m'"。攻撃者入力に由来する変数は次のとおりです。
このバイパスにより、ロギングパスはバックエンド上での任意 SQL 実行プリミティブになります。影響が最も大きい2つのシナリオは次のとおりです。
任意の特権を持つバックドアユーザーを注入する(認証バイパス)。
ProFTPD の SQL 認証バックエンドは、ユーザー名、パスワードハッシュ、uid、gid、ホームディレクトリ、シェルを、ロギングの INSERT が書き込めるようになったのと同じ users テーブルから読み取ります。スタックされた INSERT INTO users により、攻撃者が選択したアカウント(uid=0、homedir=/、平文パスワード)が仕込まれ、攻撃者はその後、FTP デーモンを介してファイルシステムへの完全なアクセス権を持って通常どおりログインできます。%U + SQLLog ERR_* パスによる認証前、または攻撃者が発行できるコマンドにバインドされた任意の攻撃者制御変数(例: %{basename} + SQLLog STOR)による認証後のどちらでも到達可能です。PostgreSQL と SQLite で動作します(どちらもスタッククエリをサポートしています)。
COPY TO PROGRAM によるデータベースホスト上でのリモートコード実行。
PostgreSQL の COPY (SELECT …) TO PROGRAM '<cmd>' は、データベースサーバー上でシェルを介して <cmd> を実行します。COPY TO PROGRAM を発行するスタッククエリインジェクションにより、postgres OS ユーザーとして任意の OS コマンド実行が可能になり、資格情報の窃取、横展開、永続化に十分です。バックドアのケースと同じトリガーパス( による認証前、または などによる認証後)で到達可能です。PostgreSQL 固有であり、ProFTPD の DB ロールがスーパーユーザー権限( の前提条件)を持っている必要があります。これは、ロールが DB 所有者であるシングルテナント展開では一般的です。
このリポジトリの POC は、PostgreSQL を特にターゲットにしています。PQexec() がスタッククエリをサポートし、COPY TO PROGRAM が直接の OS コマンド実行を提供するため、最も強力なケースです。バイパス自体は共有 SQL フレームワークにあり、すべてのバックエンドに影響します。
PQexec によるスタッククエリ、COPY TO PROGRAM による RCE。sqlite3_exec によるスタッククエリ。proftpd ワーカー内の PRIVS_ROOT で実行されます。バックドア注入は同じように機能します。RCE プリミティブは異なります(COPY TO PROGRAM に相当するものはありませんが、root プロセスの SQL バックエンド上の書き込み可能な users テーブルで十分です)。users テーブルや OS 実行に到達するのはより困難です。単一ステートメントでの操作(VALUES スロットでのサブクエリによる外部漏えい、時間ベースのブラインド、エラーベースのブラインド)は簡単ですが、それを 別の テーブルへの書き込みに変えるには、いくつかの障害を回避する必要があります:
mod_sql_mysql は CLIENT_MULTI_STATEMENTS なしで mysql_real_query() を呼び出すため、末尾の ; INSERT INTO users … は接続によって拒否されます。users テーブルまたはディスク上のファイルに書き込むために、この制限を回避する方法を考え出す必要があります。PostgreSQL セットアップでは、POC は2つの代表的なトリガーパスを示しています。上記の表の他の組み合わせも、原理的には同等です。
%U + USER による認証前。 SQLLog ERR_* により、これは完全に未認証で実行できます。%{basename} + STOR による認証後。 任意の FTP 資格情報が必要ですが、ファイルをアップロードできる能力以外の特権は不要です。setup/ — 自動環境セットアップ。これらの調査結果が報告されたコミットに固定された ProFTPD ソースツリーをクローンし、mod_sql + mod_sql_postgres を有効にしてデーモンをビルドし、シードデータと脆弱な SQLLog 構成を備えた Docker Compose クラスタ(ProFTPD + PostgreSQL)を起動します。
pocs/ — 5つのエクスプロイトスクリプト。すべて標準ライブラリのみを必要とする自己完結型の Python スクリプトです。
preauth_user_backdoor.py — 認証前の %U トリガー → 認証 DB にバックドアユーザー(uid=0、homedir=/)が注入されます。資格情報も DB スーパーユーザーも不要です。この POC は PostgreSQL 固有ですが、この問題は mysql または sqlite バックエンドでも悪用可能です。preauth_user_rce.py — 認証前の %U トリガー → COPY TO PROGRAM による PostgreSQL ホスト上での RCE。資格情報は不要。ProFTPD の DB ロールが PostgreSQL スーパーユーザーである必要があります。postauth_stor_backdoor.py — 認証後の %{basename} トリガー → バックドアユーザーが注入されます。任意の認証済み FTP ユーザーが必要です。この POC は PostgreSQL 固有ですが、この問題は mysql または sqlite バックエンドでも悪用可能です。前提条件: Docker、Git、Python 3.10+、および uv。
cd setup
./setup.sh
初回実行時は ProFTPD ソースをクローンしてサーバーをビルドします(約2〜3分)。2回目以降はビルドキャッシュを再利用するため、数秒で起動します。
セットアップが完了すると、完全な環境情報(FTP および DB エンドポイント、テスト資格情報、コピー&ペースト可能な POC コマンド)が出力されます。その指示に従って POC を実行してください。
環境を破棄するには:
cd setup
./teardown.sh
| Var | Meaning |
|---|
%A | 匿名ログインパスワード文字列 |
%J | コマンドパラメータ(動詞以降のすべて) |
%S | 応答メッセージ文字列(エラーでエコーバックされた攻撃者入力が含まれる場合があります) |
%U | USER からの元のユーザー名(認証前に設定され、失敗したログインでも利用可能) |
%d | ディレクトリ名(最後のパスコンポーネント) |
%l | RFC 1413 ident 応答(攻撃者が identd を実行している場合、攻撃者が制御可能) |
%m | FTP メソッド/動詞(攻撃者が送信するコマンドを選択) |
%r | 完全な FTP コマンド(動詞 + 引数) |
%u | 認証済みユーザー名 |
%{basename} | パス引数のファイル名コンポーネント(ディレクトリプレフィックスなし) |
%f、%F、%D は攻撃者が制御できるように見えますが、常に / で始まる絶対パスに解決されるため、is_escaped_text() の「' で始まる」という要件を満たすことはできません。
管理者がその SQLNamedQuery を、攻撃者が到達可能な FTP コマンド(またはコマンドクラス)の SQLLog ディレクティブにバインドしている場合。広くドキュメント化されている SQLLog * および SQLLog ERR_* ワイルドカードは最も広いケースであり、%U パスを認証前(pre-auth)にしています。ERR_* は失敗した USER で発火するため、資格情報は不要です。SQLLog STOR のようなコマンド単位のディレクティブは、認証済みユーザーをカバーします。
%U%{basename}COPY TO PROGRAMpostauth_stor_rce.py — 認証後の %{basename} トリガー → PostgreSQL ホスト上での RCE。任意の認証済み FTP ユーザーと PostgreSQL スーパーユーザーの DB ロールが必要です。postgres_blind_dump.py — 認証前の %U トリガー → 認証 users テーブルの時間ベースのブラインド抽出。スタッククエリを使用しないため、proftpd の DB ロールが最小限の権限(ログテーブルへの INSERT のみ)しか持たず、上記のバックドア / RCE POC がフェイルクローズする環境でも動作します。pg_sleep() による 1 ビットずつの二分探索で、passwd 列を含むすべての列のすべてのバイトを取得します。sqlmap が自動化するような処理を、サードパーティ依存なしで手組みで再現しています。