
CakePHP 5.4.2 - Alta - Remota - SQLi - Desplazamientos de rango/frame de WindowExpression insertados en SQL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cakephp-window-sqli
Write-up: https://abraxaslabs.tech/research/cakephp-window-sqli
Clase: SQLi Alcance: Remoto
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 cerró cast / extract / dateAdd de FunctionsBuilder con ensureSimpleString(). WindowExpression::range() y frame() no estaban en esa corrección. rows() y groups() aceptan ?int. range() sigue aceptando una cadena. buildOffsetSql concatena el offset, el tipo de frame y PRECEDING / FOLLOWING en OVER. El binder no se usa a menos que el offset ya sea un ExpressionInterface.
Una aplicación que reenvía datos de la solicitud a func()->count('*')->over()->range($start, $end) coloca la cadena del llamador en RANGE BETWEEN sin bindear y sin comillas.
| ID | aún sin CVE |
| Clase | SQLi |
| Alcance | Remoto (aplicación que reenvía datos de la solicitud a range() / frame()) |
| CWE | CWE-89 |
| CVSS | Alto: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Producto | CakePHP (cakephp/database) |
| Afectado | 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63) |
| Autenticación | aplicación que reenvía datos de query/formulario a ->over()->range($start, $end) / frame() |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1 |
Un usuario que puede establecer el límite de ventana que la aplicación reenvía (analítica de "últimos N días", un generador de informes) convierte la cláusula OVER en SQL adicional.
| Privilegio | Método / sink | Tipo | Probado en laboratorio o inferido |
|---|---|---|---|
Empalmar un fragmento SQL en OVER (RANGE BETWEEN ...) | GET /?mode=range&start= hacia AggregateExpression::range($start, $end) / WindowExpression::frame() | Integridad del SQL compilado | Probado en laboratorio. El SQL compilado es RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING con un binder vacío |
| Leer o modificar filas como el usuario de BD de la aplicación | La misma consulta, ejecutada | Integridad / reconocimiento de la BD | Inferido a partir del empalme. Este laboratorio es solo de compilación |
| Cambiar el tipo de frame o la dirección de la misma forma | frame($type, $offset, $direction, ...) también concatena esas cadenas | Integridad del SQL compilado | Inferido a partir de sql() / buildOffsetSql(). El laboratorio usó range() |
Romper rows() / groups() de la misma forma | Tipado ?int | Cerrado | Probado en laboratorio como cerrado. rows(3) es ROWS BETWEEN 3 PRECEDING, sin comillas, sin bindear |
Cerrado en este pin: cast / extract / dateAdd / jsonValue de FunctionsBuilder (CVE-2026-79752 / CVE-2026-77635); Query::limit / offset (int tipado). El cakephp/app por defecto no llama a range() con datos de la cadena de consulta. Lo que queda es la API de ventana documentada. No es RCE no autenticado en una aplicación esqueleto. No es una shell.
Restos de pines iguales: collate SQLi, attachment CRLF, cache unserialize.
Fijé cakephp/cakephp 5.4.2 después de los GHSAs de SQLi en FunctionsBuilder de 2026. Diez búsquedas sobre esa etiqueta. CSRF / FormProtection, LFI en View, XXE en BodyParser, asignación masiva, XSS en helpers y clientIp() sin trustProxy volvieron vacíos en este listón. WindowExpressionTest::testRange todavía afirma:
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql nunca vincula un desplazamiento de cadena:
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 no se utiliza a menos que el offset ya sea una ExpressionInterface.
Falsos caminos ya registrados: tratar rows() como el hueco (está tipado como int); afirmar que el cakephp/app estándar expone esto en un formulario público; ejecutar el SQL inyectado en SQLite como oráculo (POC-READY es solo de compilación); una reverse shell. Teatro.
python3 cakephp-window-sqli-Abraxas-Labs.py
o
cd lab
./run.sh
Apunta solo a http://127.0.0.1:19440. Pequeña app PHP, cakephp/database 5.4.2, SQLite :memory:. GET /?mode=range&start= se pasa como string a range(). GET /?mode=rows&n=3 es el control de tipo int.
SUCCESS cakephp-window-sqli pin=5.4.2 range-raw=yes rows-numeric=yes bound=no quoted=no CAKEPHP-WINDOW-SQLI-WITNESS
IOC compilado en SQL:
SELECT (COUNT(*) OVER (RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Control:
SELECT (COUNT(*) OVER (ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Vincular los desplazamientos numéricos. Permitir únicamente el tipo de marco (RANGE / ROWS / GROUPS) y la dirección (PRECEDING / FOLLOWING). Para las cadenas de intervalo, entrecomillarlas o exigir ExpressionInterface. Añadir una prueba de que un desplazamiento de range() que no sea int se convierte en un marcador de posición o en un literal entrecomillado, no en SQL sin procesar.
887b641de9f7aba06071f98f17edcbc573cbcf63)WindowExpression.php · AggregateExpression.phpGNU Affero GPL v3.0. Véase LICENSE. Solo para laboratorio en loopback. Sin garantía.