
CakePHP 5.4.2 - 高 - リモート - SQLi - WindowExpression の range/frame オフセットが SQL に連結される
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cakephp-window-sqli
解説記事: https://abraxaslabs.tech/research/cakephp-window-sqli
分類: SQLi 到達範囲: リモート
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 は FunctionsBuilder の cast / extract / dateAdd を ensureSimpleString() で塞いだ。WindowExpression::range() と frame() はその修正に含まれていなかった。rows() と groups() は ?int を受け取る。range() は今も文字列を受け取る。buildOffsetSql はオフセット、フレーム種別、そして PRECEDING / FOLLOWING を連結して OVER に埋め込む。オフセットがすでに ExpressionInterface でない限り、バインダは使われない。
リクエストデータを func()->count('*')->over()->range($start, $end) に転送するアプリは、呼び出し元の文字列をバインドもクォートもせずに RANGE BETWEEN に挿入してしまう。
| ID | CVE 未割当 |
| 分類 | SQLi |
| 到達範囲 | リモート(リクエストデータを range() / frame() に転送するアプリ) |
| CWE | CWE-89 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| 製品 | CakePHP (cakephp/database) |
| 影響を受けるバージョン | 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63) |
| 認証 | クエリ/フォームデータを ->over()->range($start, $end) / frame() に転送するアプリケーション |
| ライセンス | GNU Affero GPL v3.0 |
| ラボ | 127.0.0.1 のみ |
アプリが転送するウィンドウ境界(アナリティクスの「直近 N 日間」、レポートビルダーなど)を設定できるユーザーは、OVER 句を追加の SQL に改変できる。
| 権限 | メソッド / シンク | 種類 | ラボで実証済みか推測か |
|---|---|---|---|
SQL 断片を OVER (RANGE BETWEEN ...) に挿入する | GET /?mode=range&start= を AggregateExpression::range($start, $end) / WindowExpression::frame() に渡す | コンパイル済み SQL の完全性 | ラボで実証済み。コンパイル済み SQL は空のバインダを伴う RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING となる |
| アプリの DB ユーザーとして行を読み取りまたは変更する | 同じクエリを実行する | DB の完全性 / 偵察 | 挿入から推測。このラボはコンパイルのみ |
| 同様の方法でフレーム種別や方向を変更する | frame($type, $offset, $direction, ...) もこれらの文字列を連結する | コンパイル済み SQL の完全性 | sql() / buildOffsetSql() から推測。ラボでは range() を使用 |
同様の方法で rows() / groups() を破壊する | 型付き ?int | 修正済み | ラボで修正済みを実証。rows(3) は ROWS BETWEEN 3 PRECEDING となり、クォートもバインドもされない |
このピンで修正済み: FunctionsBuilder の cast / extract / dateAdd / jsonValue(CVE-2026-79752 / CVE-2026-77635)、Query::limit / offset(型付き int)。デフォルトの cakephp/app はクエリ文字列データを伴って range() を呼び出さない。残っているのは文書化されたウィンドウ API である。スケルトンアプリにおける未認証 RCE ではない。シェルではない。
同じピンの残り: collate SQLi、attachment CRLF、cache unserialize。
cakephp/cakephp は、2026年のFunctionsBuilder SQLiに関するGHSAを受けて 5.4.2 に固定しました。このタグで10回の調査を実施しました。この基準では、CSRF / FormProtection、View LFI、BodyParser XXE、マスアサインメント、ヘルパーXSS、および trustProxy なしの clientIp() はいずれも該当なしでした。WindowExpressionTest::testRange は依然として次をアサートしています:
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql は文字列のオフセットを決してバインドしません:
protected function buildOffsetSql(
ValueBinder $binder,
ExpressionInterface|string|int|null $offset,
string $direction,
): string {
if ($offset === 0) {
return 'CURRENT ROW';
}
if ($offset instanceof ExpressionInterface) {
$offset = $offset->sql($binder);
}
return sprintf('%s %s', $offset ?? 'UNBOUNDED', $direction);
}
$binder は、オフセットがすでに ExpressionInterface でない限り使用されません。
すでに記録されている誤った方向性: rows() をホールとして扱うこと(これは int 型です); 標準の cakephp/app が公開フォームでこれを露出していると主張すること; 注入された SQL を SQLite でオラクルとして実行すること(POC-READY はコンパイルのみ); リバースシェル。すべて見せかけです。
python3 cakephp-window-sqli-Abraxas-Labs.py
または
cd lab
./run.sh
必ず http://127.0.0.1:19440 のみを対象とすること。小さな PHP アプリ、cakephp/database 5.4.2、SQLite :memory:。GET /?mode=range&start= は文字列として range() に渡される。GET /?mode=rows&n=3 は int のコントロールである。
SUCCESS cakephp-window-sqli pin=5.4.2 range-raw=yes rows-numeric=yes bound=no quoted=no CAKEPHP-WINDOW-SQLI-WITNESS
コンパイル済みIOC SQL:
SELECT (COUNT(*) OVER (RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
制御:
SELECT (COUNT(*) OVER (ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
数値オフセットをバインドする。フレームタイプ(RANGE / ROWS / GROUPS)と方向(PRECEDING / FOLLOWING)を許可リスト化する。インターバル文字列については、クォートするか ExpressionInterface を必須とする。非 int の range() オフセットが生の SQL ではなくプレースホルダーまたはクォートされたリテラルになることを検証するテストを追加する。
887b641de9f7aba06071f98f17edcbc573cbcf63)WindowExpression.php · AggregateExpression.phpGNU Affero GPL v3.0。LICENSE を参照。ループバックラボ専用。保証なし。