
PoC de solo prueba para CVE-2026-98432, un use-after-free de ireq_opt en IPv4/TCP de Linux: disparador de condición de carrera y oráculo de reutilización de heap que confirma el UAF sin ejecución de código.
ireq_opt de IPv4/TCP de LinuxPrueba de concepto únicamente demostrativa para CVE-2026-98432 (CVSS 9.8): un use-after-free activable de forma remota en la pila IPv4/TCP de Linux.
Solo para pruebas de seguridad autorizadas. Ejecútalo contra sistemas que sean de tu propiedad o para los que tengas permiso explícito de probar.
El PoC activa la carrera y detecta cuándo el kernel copia bytes de un objeto
ip_options_rcu liberado a un SYN-ACK. No crea usuarios ni archivos, y
no implementa una cadena de ejecución de código.
Un acierto significa que el servidor devolvió el marcador de otra asignación
dentro de su SYN-ACK — un kernel correcto no puede hacer eso. En una prueba
controlada en Ubuntu 24.04.5 con kernel 6.8.0-147-generic (marcado como
vulnerable por Ubuntu; corrección en cola en 6.8.0-149), una ejecución en
loopback de 180 segundos produjo 21 aciertos en 320.829 intentos, y una
ejecución de 300 segundos produjo 22 aciertos en 616.911 intentos. El
kernel permaneció limpio (tainted=0, sin oops).
No se incluyen aquí capturas de paquetes ni registros de ese host; la propia salida del PoC es la evidencia.
Un SYN que transporta opciones IPv4 hace que el kernel construya un
ip_options_rcu (72 bytes, kmalloc-96) para el request socket. Cuando el
handshake se completa, tcp_v4_syn_recv_sock() entrega ese objeto al socket
hijo sin copiarlo, y luego borra el puntero en el request socket. Una
segunda CPU que ya haya consultado el request socket puede seguir dentro de
tcp_check_req() → tcp_v4_send_synack() → ip_build_and_send_pkt() →
ip_options_build(), leyendo el mismo objeto. Los sockets hijos no son
SOCK_RCU_FREE, e inet_sock_destruct() libera inet_opt con un simple
kfree(), así que si el hijo se destruye en esa ventana, el lector
desreferencia memoria liberada.
Existe una segunda variante con migración de request socket
(net.ipv4.tcp_migrate_req=1 o un programa BPF de reuseport); la corrección
usa kfree_rcu() tanto en inet_sock_destruct() como en
tcp_v4_reqsk_destructor().
Introducido en Linux 4.4 (079096f103fa); corregido en mainline 7.3 y
retroportado a 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56 y
7.2.10. Afectados: Linux 4.4 y posteriores, sin parchear.
El impacto va desde un cierre remoto (DoS) hasta la divulgación de memoria del kernel y, con una cadena de explotación completa, la ejecución de código. Este repositorio se limita a demostrar el use-after-free.
Por intento:
ireq_opt; el objetivo refleja la opción en su SYN-ACK.inet_opt, el request
socket descarta su puntero.Oráculo. Cada intento incrusta un magic único de 32 bits más una etiqueta de comprobación en los offsets 16..23 de la opción. Se producen tres reescrituras antes de que un lector en espacio de usuario vea la opción:
| etapa | ranura |
|---|---|
ip_options_compile() del servidor sobre el SYN | 4..7 (TS) / 3..6 (RR) |
ip_options_build() del servidor para el SYN-ACK | 8..11 (TS) / 7..10 (RR) |
ip_options_compile() del host receptor sobre el SYN-ACK | 12..15 (TS) / 11..14 (RR) |
El marcador queda más allá de las tres. Un SYN-ACK que refleja el magic de una
asignación diferente (con tag == magic ^ 0x5a5a5a5a) demuestra que el
lector copió de un objeto liberado y reutilizado.
CAP_NET_RAW; el filtro RST automático necesita
CAP_NET_ADMIN (root).accept().iptables/nftables para el filtro de RST puro.--proof es autocontenido en un solo host: inicia un listener en loopback y
compite contra él.
python3 poc_cve_2026_98432.py --selftest
sudo python3 poc_cve_2026_98432.py --check
Imprime el kernel en ejecución, la tabla de versiones corregidas y los sysctls relevantes.
sudo python3 poc_cve_2026_98432.py --proof 9000 \
--duration 180 --workers 6 --spray 2 --spray-rate 1000 \
--pcap /tmp/proof.pcap
Inicia un listener en 127.0.0.1:9000, ejecuta la carrera, imprime un veredicto, detiene
el listener y elimina la regla de firewall temporal. No se crea nada en
el host — solo estado TCP transitorio.
sudo python3 poc_cve_2026_98432.py --probe <TARGET>:<PORT>
Un SYN: informa si el objetivo es un listener MPTCP y si refleja la opción IPv4 (requerida por el oráculo).
sudo python3 poc_cve_2026_98432.py <TARGET>:<PORT> \
--workers 16 --spray 4 --duration 600 --pcap hits.pcap
Las direcciones públicas se rechazan a menos que se especifique --allow-public. El filtro de RST sin procesar se instala automáticamente y se elimina al salir.
python3 poc_cve_2026_98432.py --listen 9000 --mptcp
Se vincula a 127.0.0.1; añade --listen-any solo para un host atacante remoto. El
listener acepta y cierra inmediatamente cada conexión con
SO_LINGER(0), liberando el hijo rápidamente.
| campo | significado |
|---|---|
attempts | handshakes iniciados |
synacks | SYN-ACKs observados (aproximadamente dos por intento) |
hits | el SYN-ACK llevaba el marcador de otra asignación → UAF confirmado |
anomalies | marcador analizado pero etiqueta no coincidente (corrupción parcial) |
leaks | valor con aspecto de puntero del kernel en los bytes devueltos |
noopt | SYN-ACK sin la opción devuelta (syncookies/cola llena/eliminación) |
stale | retransmisión tardía de SYN-ACK filtrada (no es un hit) |
malformed | SYN-ACK con ihl < 5 (firma de underflow de optlen obsoleto) |
timeouts | sin SYN-ACK dentro de --timeout |
spray | SYNs de heap-spray enviados |
Ejemplo de hit:
[!] UAF ORACLE HIT #1 at attempt sport=63042 (expected magic f815ca68 tag a24f9032)
tcp <TARGET>:9000 -> <CLIENT>:63042 seq=c20ed8ef ack=d212b2b1 flags=0x12
SYN-ACK echoed magic d4bcdcc7 tag 8ee6869d, belongs to another allocation
ip options:
44 28 11 00 03 71 b0 b5 03 71 cc 73 03 71 cc 73
d4 bc dc c7 8e e6 86 9d 00 00 00 00 00 00 00 00
0x8ee6869d == 0xd4bcdcc7 ^ 0x5a5a5a5a: la relación de etiquetas demuestra que los
bytes devueltos son un marcador válido de una asignación diferente.
Códigos de salida: 0 = evidencia observada, 3 = ninguna, 2 = error de configuración,
1 = error de uso/ejecución.
La carrera es probabilística. En el host de prueba, --workers 6 --spray 2 --spray-rate 1000 durante al menos 3 minutos la reprodujo; una ejecución de 60 segundos con
--workers 4 --spray 1 --spray-rate 500 produjo cero aciertos en el mismo host.
No concluya "no vulnerable" a partir de una ejecución corta.