CVE-2026-31431 (Copy Fail) Forensics
For those that have missed it, new CVE just dropped. Simple and reliable privesc for every Linux system since 2017. Links and background first. Skip down a section for the forensics. I'll try to update this with other distros and new evidence as things unfold.
- 13Cubed episode
- The CVE site: https://copy.fail/
- The Reddit topic: https://www.reddit.com/r/sysadmin/comments/1szajkx/copy_fail_cve202631431_is_a_trivially_exploitable/
- Minimized code: https://github.com/theori-io/copy-fail-CVE-2026-31431
- Un-minimized code: https://gist.github.com/grenkoca/b82281a4706e936072979acf54b608df
Here's the CyberChef (copy/paste it, or it'll break) for the zlib content, which has a hash of 7ef953069578beef7daf2d984d16351b0c60c6adcbf8062ffbfaac6ce944c1da
https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')Zlib_Inflate(0,0,'Adaptive',false,false)To_Hexdump(16,false,false,false/disabled)SHA2('256',64,160/disabled)&input=NzhkYWFiNzdmNTcxNjM2MjY0NjQ4MDAxMjYwNjNiMDYxMGFmODJjMTAxY2M3NzYwYzAwNDBlMGMxNjBjMzAxZDIwOWExNTRkMTY5OTllMDdlNWMxNjgwNjAxMDg2NTc4YzBmMGZmODY0YzdlNTY4ZjVlNWI3ZTEwZjc1Yjk2NzVjNDRjN2U1NmMzZmY1OTM2MTFmY2FjZmE0OTk5NzlmYWM1MTkwYzBjMGMwMDMyYzMxMGQzThe sample is a minimal ELF binary designed for privilege escalation and shell execution. It directly invokes syscalls to set the process UID to 0 (root) via sys_setuid and then executes '/bin/sh' via sys_execve. This behavior is characteristic of a local privilege escalation (LPE) exploit payload or a backdoor designed to provide root access.
And the hexdump, because hexdumps look cool
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 78 00 40 00 00 00 00 00 |..>.....x.@.....|
00000020 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |@...............|
00000030 00 00 00 00 40 00 38 00 01 00 00 00 00 00 00 00 |[email protected].........|
00000040 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 |................|
00000050 00 00 40 00 00 00 00 00 00 00 40 00 00 00 00 00 |..@.......@.....|
00000060 9e 00 00 00 00 00 00 00 9e 00 00 00 00 00 00 00 |................|
00000070 00 10 00 00 00 00 00 00 31 c0 31 ff b0 69 0f 05 |........1À1ÿ°i..|
00000080 48 8d 3d 0f 00 00 00 31 f6 6a 3b 58 99 0f 05 31 |H.=....1öj;X...1|
00000090 ff 6a 3c 58 0f 05 2f 62 69 6e 2f 73 68 00 00 00 |ÿj<X../bin/sh...|Artifacts
So what do we see when we run this? I'll be referencing some timestamps, check the next section for the order of events.
Update 2026-04-30 - I'm learning that this exploit seems to only affect the in-memory copy of /usr/bin/su. This can be demonstrated by running the PoC and rebooting the system, once the restart finishes, /usr/bin/su will be back to it's original hash. That said, once the TA gets root they could easily create persistence, via this or any other means, that would give them root after a reboot. Any system affected by this should be rebuilt from trusted media. Forensically, this impacts the search for modifications of the su file below. There are also statically compiled versions appearing that don't rely on Python or dependencies.
/var/log/auth.log
Starting with auth.log. Our exploit was run at 00:36:39. We see the exit of the previous root session at 00:35:08, and the start of the next legitimate one at 00:37:22. There isn't a root session between for our exploit though, since we're bypassing the typical auth methods. If you see root activity during a period where there was no root session, that is in itself an artifact.
2026-04-30T00:35:08.650748-04:00 ubuntu sudo: pam_unix(sudo:session): session closed for user root
2026-04-30T00:37:22.559989-04:00 ubuntu sudo: mike : TTY=pts/0 ; PWD=/home/user ; USER=root ; COMMAND=/usr/bin/su -journalctl
I ran this several times, and each time got the PF_ALG registration and the su launched with empty string. I don't know how common these things are in a live environment though. Edit: I've search three long running Ubuntu instances, one of them having been in-place upgraded since 2018, and did not find matches for rg -F -z "PF_ALG" /var/log or the same for the string with NULL argv. So these may be very unique (or my systems are).
Apr 30 00:36:39 ubuntu kernel: NET: Registered PF_ALG protocol family
Apr 30 00:36:39 ubuntu kernel: process 'su' launched '/bin/sh' with NULL argv: empty string added/var/log/kern.log
Roughly the same message we got from journalctl, PF_ALG registration and su launched with empty string.
2026-04-30T00:36:39.557587-04:00 ubuntu kernel: NET: Registered PF_ALG protocol family
2026-04-30T00:36:39.565569-04:00 ubuntu kernel: process 'su' launched '/bin/sh' with NULL argv: empty string addedModified /usr/bin/su file
Easy win, the exploit changes the binary, if it doesn't match what's supposed to be in the repo, something is wrong. In the case of Ubuntu 24.04, for me anyway, the "evil" MD5 hash is consistently 0b8ebf43eed68c716fc345cf55d87284. This file has been uploaded to VirusTotal as well, ignore the name someone gave it.
user@ubuntu:/var/log/unattended-upgrades$ cat /var/lib/dpkg/info/util-linux.md5sums | grep usr/bin/su
e1e5e66539ac01f221e92d2f287c2983 usr/bin/su
user@ubuntu:/var/log/unattended-upgrades$ md5sum /usr/bin/su
0b8ebf43eed68c716fc345cf55d87284 /usr/bin/su
user@ubuntu:/var/log/unattended-upgrades$ You can also use debsums, on Ubuntu anyway.
user@ubuntu:/var/log/unattended-upgrades$ sudo apt install debsums
user@ubuntu:/var/log/unattended-upgrades$ sudo debsums -s
debsums: changed file /usr/bin/su (from util-linux package)Behavior
Notice in the output below that when I first executed sudo su -, I got a normal prompt with a user and hostname. After the exploit, I got a basic # prompt.
Test Environment
ike@ubuntu:~$
user@ubuntu:~$ date
Thu Apr 30 12:34:24 AM EDT 2026
user@ubuntu:~$ vim copyfail.py
user@ubuntu:~$
user@ubuntu:~$
user@ubuntu:~$ date
Thu Apr 30 12:34:42 AM EDT 2026
user@ubuntu:~$
user@ubuntu:~$ chmod +x copyfail.py
user@ubuntu:~$
user@ubuntu:~$ date
Thu Apr 30 12:34:54 AM EDT 2026
user@ubuntu:~$
user@ubuntu:~$ sudo su -
[sudo] password for user:
root@ubuntu:~#
root@ubuntu:~# date
Thu Apr 30 12:35:06 AM EDT 2026
root@ubuntu:~#
root@ubuntu:~# exit
logout
user@ubuntu:~$
user@ubuntu:~$
user@ubuntu:~$ date & ./copyfail.py
[1] 3071
Thu Apr 30 12:36:39 AM EDT 2026
#
# date
Thu Apr 30 00:36:43 EDT 2026
#
# exit
[1]+ Done date
user@ubuntu:~$
user@ubuntu:~$
user@ubuntu:~$ date
Thu Apr 30 12:37:17 AM EDT 2026
user@ubuntu:~$
user@ubuntu:~$ sudo su -
#
#
# date
Thu Apr 30 00:37:24 EDT 2026
# exit
user@ubuntu:~$
user@ubuntu:~$ Tangent, on date formats
During the course of this, I looked at auth.log, journalctl, I referenced the date command in my output. I also looked at (though didn't find any evidence in) file stat, ausearch, and throwing in last for good measure. Here's what those timestamps look like:
Auth.log 2026-04-30T00:35:08.650748-04:00
Journal Apr 30 00:36:39
Date cmd Thu Apr 30 12:34:42 AM EDT 2026
Stat cmd 2026-03-06 11:00:54.000000000 -0500
ausearch 04/29/2026 '21:13:00'
last cmd Sat Mar 6 13:04:07 2021See the pattern?
No?
THAT'S THE PROBLEM!

I swear to god developers think there is a requirement that the timestamp format they use cannot and must not be reused anywhere else in the history of software. Each and every one takes it as a personal challenge to craft something brand new and unique for this world. STOP IT! There is a wonderful intersection of RFC 3339 and ISO 8601 that you need to aim for, and it looks like this 2026-04-30T08:42:35.253Z .
- Never leave off the timezone. Logs should be in UTC (Z), but if you want to write local time, include the offset as a + or - (e.g.
2026-04-30T08:46:24-04:00) Do not use three letter abbreviations, do not use 'America/New York'. Yes I'm being picky because if I leave you up to your own devices, I'll be parsing time zones like 'SE US-LOS ANGELES-SUMMER'. - Feel free to include fractional seconds. The more accurate the better in my opinion. You're cut off at nanoseconds though, so get your timezone in before that.
- Always use 24-hour time. If I have to parse one more timestamp with AM and PM, I'm going to start hunting devs down and beating them with a wiffle ball bat.
- Yes I'm aware that many Linux utilities, such as last, allow you to format the timestamp. You're welcome to leave that and people can tweak it till their hearts content. Use 3339/8601 as the default though.