
Scripts de reproducción y PoC para CVE-2026-96512 (SudoTimeWarp), donde el TZ del invocador desplaza las ventanas NOTBEFORE/NOTAFTER de sudoers, además de comprobaciones de mitigación.
sudo: el TZ del invocador decide NOTBEFORE/NOTAFTER
Material de reproducción para SudoTimeWarp (CVE-2026-96512). Una regla de sudoers cuyo timestamp Date_Spec omite
la Z final es convertida por mktime(), que vuelve a leer getenv("TZ") en cada llamada.
Como sudo es setuid-root y el environ del invocador cruza execve() intacto, el
invocador sin privilegios elige la zona horaria en la que se evalúa su propia ventana de validez.
| Nombre | SudoTimeWarp |
| CVE | CVE-2026-96512 |
| CVSS v3.1 | AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — 7.8 Alta |
| CWE | CWE-863 (registrado); el mecanismo es CWE-807 |
| Afecta a | sudo 1.8.20 hasta 1.9.17p2, y main anterior a 1820a349 |
| Corregido en | 1820a349 (2026-08-29) |
| Desplazamiento | hasta 24 h 59 m 59 s por dirección; intervalo total 49 h 59 m 58 s |
El problema de base — que TZ influye en NOTBEFORE/NOTAFTER — fue reportado de forma privada por el
XlabAI Team of Tencent Xuanwu Lab, el Atuin Automated Vulnerability Discovery
Engine, y Guannan Wang, Zhanpeng Liu y Guancheng Li, y acreditado en el commit
db669167c (2026-03-14).
SudoTimeWarp (CVE-2026-96512) es el hallazgo de que db669167c es incompleto. La protección que
instala cubre la caché de zonas horarias de glibc — y con ella los timestamps de los logs — pero no
alcanza el mktime() en gentime.c:156, donde se decide la autorización. En un árbol que
ya contiene ese commit, el desplazamiento de la ventana sigue siendo totalmente reproducible. El mantenedor
registra el punto en el mensaje de 1820a349:
el cambio anterior "no fue efectivo ya que la función mktime() vuelve a leer la variable de entorno TZ cada vez que se llama"
Una regla de sudoers que (1) concede un comando al invocador, (2) lleva NOTBEFORE= o
NOTAFTER=, y (3) escribe el timestamp sin sufijo Z y sin un offset
explícito.
El punto (3) no es exótico: el manual de sudoers documenta la forma sin sufijo como una extensión
soportada, muestra 20151201235900 como uno de sus cuatro timestamps de ejemplo
(docs/sudoers.man.in:1820), y la forma aparece en el propio corpus de regresión del proyecto
(plugins/sudoers/regress/testsudoers/test13.sh).
Siempre en un contenedor o VM desechable. Todos los scripts de aquí reescriben /etc/sudoers.
Hacen copia de seguridad y lo restauran, pero un error ahí te deja fuera de una máquina real — los
scripts se niegan a ejecutarse fuera de un contenedor a menos que pases --i-know.
docker build -t sudotimewarp .
docker run --rm -it sudotimewarp
O sin construir una imagen:
docker run --rm -it -v "$PWD:/m" debian:trixie bash -c \
'apt-get update >/dev/null && apt-get install -y sudo >/dev/null && bash /m/poc.sh'
Salida esperada en una compilación afectada:
=== probes ===
rule valid, no TZ : uid=0(root) gid=0(root) groups=0(root)
rule expired, no TZ : sudo: a password is required
rule expired, TZ=UTC : sudo: a password is required
rule expired, TZ=XXX24 : uid=0(root) gid=0(root) groups=0(root)
expired + 'Z', TZ=XXX24 : sudo: a password is required
XXX es una abreviatura de zona horaria arbitraria de tres letras y 24 es un offset POSIX. No
hay ningún archivo involucrado y no hace falta que exista ninguno — el canal explotable es únicamente la cadena POSIX
en línea. La forma tzfile (TZ=:/tmp/evil.tz y variantes) es rechazada por la
guarda __libc_enable_secure de glibc bajo setuid y mide exactamente 0 s de desplazamiento, que es lo que
separa esto de CVE-2014-9680.
Las líneas 1–3 son controles, y son importantes: un error de parseo produciría el mismo ALLOW que el bug.
| Línea | Comprueba |
|---|---|
| 1 | Que la regla funciona en absoluto dentro de su ventana |
| 2 | Que la denegación en ausencia de la línea 4 proviene realmente de NOTAFTER |
| 3 | Que establecer TZ no es en sí mismo la causa — TZ=UTC decide como lo hace la ausencia de TZ |
| 4 | El bug: la regla expirada se ejecuta como root |
| 5 | El límite: con la Z documentada, se toma la rama timegm() y muere |
poc.sh sale con 0 cuando está afectado, 1 cuando no, 2 cuando los controles no se cumplieron.
poc.sh usa NOPASSWD para poder ejecutarse de forma no interactiva. Eso no es una condición del
bug. Los scripts divididos muestran el límite de privilegios explícitamente — la parte 1 hace solo lo que
un administrador hace legítimamente, la parte 2 se ejecuta como el usuario sin privilegios y no usa ningún
privilegio de ningún tipo:
bash repro-admin.sh escalation # as root: writes the policy
su - poc -c 'bash /poc/repro-attacker.sh'
bash repro-admin.sh --cleanup
repro-admin.sh admite cuatro escenarios:
| Escenario | Política |
|---|---|
expired (por defecto) | una regla, NOTAFTER una hora en el pasado, sin zona |
valid | la misma regla aún dentro de su ventana — control |
expired-z | la misma regla expirada con la Z documentada — control, forma no afectada |
escalation | una concesión permanente estrecha más una amplia expirada — la forma que realmente adopta una concesión de mantenimiento o de acceso de emergencia |
Con una regla que requiere contraseña, el invocador sigue autenticándose a través de PAM, y una contraseña incorrecta sigue fallando. Esto no es un bypass de autenticación — lo que se mueve es la decisión de autorización.
PR:L, no PR:N.TZ deniegan.C:L/I:N/A:N). El vector 7.8 puntúa el caso en que la regla con fecha es
más amplia que el acceso permanente del invocador.El límite de ~25 h se aplica a la ventana de acceso, no a la duración del impacto: un uso exitoso dentro de ella basta para establecer persistencia que sobrevive a la ventana.
Añade Z a cada timestamp NOTBEFORE/NOTAFTER — fuerza la rama timegm().
grep -rE 'NOT(BEFORE|AFTER)=' /etc/sudoers /etc/sudoers.d/
No audites con sudo -l. Formatea cada Date_Spec a través de gmtime() y siempre
añade una Z (plugins/sudoers/display.c:229), así que una regla escrita
NOTAFTER=20260827221423 se muestra como NOTAFTER=20260827221423Z. La salida normaliza
justo el detalle que lo decide. cvtsudoers y fmtsudoers se comportan igual.
Lee /etc/sudoers directamente.
1820a349: https://github.com/sudo-project/sudo/commit/1820a349687522f51023d1ae5925125f59679a8cReportado al mantenedor el 2026-08-28, sin plazo límite. Un parche candidato llegó el mismo día; la corrección pública se publicó el 2026-08-29. CVE asignado por Red Hat, actuando como CNA-LR, y publicado el 2026-09-23. No hay embargo: todo lo de aquí es público desde el commit de corrección.
SudoTimeWarp / CVE-2026-96512: Ermenson Junior, investigación independiente, registrado por Red Hat como "Independent security research". El informe original del problema subyacente pertenece al XlabAI Team of Tencent Xuanwu Lab, el Atuin Automated Vulnerability Discovery Engine, y Guannan Wang, Zhanpeng Liu y Guancheng Li.
Publicado para uso defensivo: verificar si un host está afectado, y validar la mitigación
de la Z.