
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
Clase: SQLi Alcance: Remoto
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 cerró FunctionsBuilder cast / extract / dateAdd con ensureSimpleString(). WindowExpression::range() y frame() no estaban en esa corrección. rows() y groups() aceptan ?int. range() todavía acepta 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 app que reenvía datos de la petición 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 (app que reenvía datos de la petición 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) |
| Auth | aplicación que reenvía datos de query/form a ->over()->range($start, $end) / frame() |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Un usuario que puede establecer el límite de ventana que la app reenvía (analíticas de "últimos N días", un generador de informes) convierte la cláusula OVER en SQL adicional.
| Privilegio | Método / sink | Tipo | Probado en lab o inferido |
|---|---|---|---|
Insertar un fragmento SQL en OVER (RANGE BETWEEN ...) | GET /?mode=range&start= hacia AggregateExpression::range($start, $end) / WindowExpression::frame() | Integridad del SQL compilado | Probado en lab. 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 app | La misma consulta, ejecutada | Integridad / reconocimiento de la BD | Inferido a partir de la inserción. Este lab 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 de sql() / buildOffsetSql(). El lab usó range() |
Romper rows() / groups() de la misma forma | Tipado ?int | Cerrado | Probado en lab como cerrado. rows(3) es ROWS BETWEEN 3 PRECEDING, sin comillas, sin bindear |
Cerrado en este pin: FunctionsBuilder cast / extract / dateAdd / jsonValue (CVE-2026-79752 / CVE-2026-77635); Query::limit / offset (int tipado). El cakephp/app por defecto no llama a range() con datos de query-string. Lo que queda es la API de ventana documentada. No es RCE no autenticado en una app esqueleto. No es una shell.
Restos del mismo pin: 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 en ese tag. CSRF / FormProtection, View LFI, BodyParser XXE, mass assignment, helper XSS y clientIp() sin trustProxy salieron vacíos en este listón. WindowExpressionTest::testRange todavía afirma:
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql nunca bindea un offset de tipo 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 usa a menos que el offset ya sea un ExpressionInterface.
Caminos equivocados ya registrados: tratar rows() como el agujero (está tipado como int); afirmar que el cakephp/app de serie 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
Objetivo solo http://127.0.0.1:19440. App PHP mínima, cakephp/database 5.4.2, SQLite :memory:. GET /?mode=range&start= se pasa como cadena 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 del SQL compilado:
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
Bindear los offsets numéricos. Permitir por lista blanca el tipo de frame (RANGE / ROWS / GROUPS) y la dirección (PRECEDING / FOLLOWING). Para cadenas de intervalo, entrecomillarlas o exigir ExpressionInterface. Añadir un test que verifique que un offset de range() que no sea int se convierte en un placeholder o en un literal entrecomillado, no en SQL en crudo.
887b641de9f7aba06071f98f17edcbc573cbcf63)WindowExpression.php · AggregateExpression.phpGNU Affero GPL v3.0. Ver LICENSE. Solo lab en loopback. Sin garantía.