The challenge (as stated in the 35C3 website)
“Logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large. It also gives you a root shell.
For your convenience, I added a suid binary to run cron jobs. Enjoy. Files at:
https://35c3ctf.ccc.ac/uploads/logrotate-6c103367e3c15cb6873403e16b38f540.tar And get your shell here: nc 35.242.226.147 1”
Environment overview
The nc seems to enter an nsjail docker instance running an unprivileged shell.
nc 35.242.226.147 1
+ echo ‘Setting up chroot.’
Setting up chroot.
+ cp -R –preserve=mode /skel /tmp/
+ mount -o bind /proc /tmp/skel/proc
+ touch /tmp/skel/dev/null
+ touch /tmp/skel/dev/zero
+ touch /tmp/skel/dev/random
+ touch /tmp/skel/dev/urandom
+ mount -o bind /dev/null /tmp/skel/dev/null
+ mount -o bind /dev/zero /tmp/skel/dev/zero
+ mount -o bind /dev/random /tmp/skel/dev/random
+ mount -o bind /dev/urandom /tmp/skel/dev/urandom
+ cp -R –preserve=mode /home/user /tmp/skel/home/
+ chmod u+s /tmp/skel/home/user/run_cron
+ cp –preserve=mode /tmp/skel/etc/cron.daily/logrotate /tmp/skel/etc/cron.d/
+ cp –preserve=mode /home/user/pwnme /tmp/skel/etc/logrotate.d/
+ cp /flag /tmp/skel/
+ chmod 400 /tmp/skel/flag
+ echo ‘Setup done.’
Setup done.
+ exec /usr/sbin/chroot /tmp/skel /home/user/unpriv
uname -a
Linux NSJAIL 4.14.65+ #1 SMP Thu Oct 25 10:42:50 PDT 2018 x86_64 GNU/Linux
id
uid=1000(user) gid=1000(user) groups=1000(user),0(root)
The interesting parts of the file system
/ /flag ← The flag file permissions 400 (readable only by root) /bin /proc /usr /sbin logrotate ← The latest logrotate executable ver 3.11.0 /etc logrotate.conf ← logrotate main config file, includes /etc/logrotate.d/* /cron.d logrotate ← Runs logrotate with main config file /logrotate.d/ pwnme ← logrotate log definition /home /user chroot.sh ← The docker startup script, seen above in the nc output run_cron ← Runs all the files found in /etc/cron.d/ as root(!) /tmp/ ← Empty tmpfs
Please note that all the above files and directories, barring the /flag file, are owned by root and and have read and execute permissions to all.
Looking at the pwnme logrotate config file, we see:
cat /etc/logrotate.d/pwnme /tmp/log/pwnme.log { daily rotate 12 missing ok notifempty size 1K }
This concludes the environment overview.
Privilege Escalation
Basic idea
For P.E., purposes we should take note that two things are out of the ordinary
- We can invoke logrotate as root (using the /home/user/run_cron.sh)
- We control the entire path of the log file /tmp/log/pwnme.log
Inspecting man logrotate we expect the following to happen when the logrotate is invoked
- Delete /tmp/log/pwnme.log.12
- Rename /tmp/log/pwnme.log.11 to /tmp/log/pwnme.log.12
- Rename /tmp/log/pwnme.log.10 to /tmp/log/pwnme.log.11
- . .
- . .
- Rename /tmp/log/pwnme.log.1 to /tmp/log/pwnme.log.2
- Rename /tmp/log/pwnme.log to /tmp/log/pwnme.log.1
- Touch /tmp/log/pwnme.log with ownership of user:user
The basic idea, that comes to mind, is to introduce a race between step 7 and step 8, i.e.,
7. Rename /tmp/log/pwnme.log to /tmp/log/pwnme.log.1
→ Replace /tmp/log with a symbolic link to /etc/cron.d
8.Touch /tmp/log/pwnme.log with ownership of user:user
Hence, Step 8, will create an empty file owned by user:user inside /etc/cron.d instead of /tmp/log.
The replacement of /tmp/log with a symbolic link can simply be done by the equivalent of
mv /tmp/log /tmp/dummy && ln -s /etc/cron.d/ /tmp/log
Issues and Solutions
Issue 1
Logrotate runs the stat() function against /tmp/log/pwnme.log at some point.
This test fails, due to the the “protected_symlinks”, security feature (/proc/sys/fs/protected_symlinks to 1). The feature prevents the root user from following symbolic links of sticky files and directories with stat(), and, of course, /tmp is sticky.
Solution
The same time of check / time of use issue exists here as well.
Since the security feature only affects stat(), and not open(). The symbolic link should be created after the stat() has succeeded.
Issue 2
While we were able to win the race on our local machine, it was not easy to win the race on the remote machine, where the number of attempts was significantly smaller, and the timing was different.
We searched for ways to increase the chances of winning the race.
Solution
We went over the logrotate code and realized that what was actually happening was:
- Rename /tmp/log/pwnme.log.12 to /tmp/log/pwnme.log.13 ← 13 (!)
- Rename /tmp/log/pwnme.log.11 to /tmp/log/pwnme.log.12
- ..
- ..
- Rename /tmp/log/pwnme.log.1 to /tmp/log/pwnme.log.2
- Rename /tmp/log/pwnme.log to /tmp/log/pwnme.log.1
- if /tmp/log/pwnme.log exist? ← Note: it was renamed in the previous step
- Write to STDOUT “Renaming file .. “
- Rename /tmp/log/pwnme.log to /tmp/log/pwnme.log.backup.HH.MM.DD
- If rename failed exit()
- Touch /tmp/log/pwnme.log with ownership of user:user
- Unlink /tmp/log/pwnme.log.13
Note: The above pseudo code is a simplified version of the actual code. The actual code is a bit more involved.
The replacement of /tmp/log to the symbolic link that points to /etc/cron.d should happen just before step 7.
The “write to STDOUT” in step 7.1 is the closest printout to the race that we could find.
We created an executable that ran ‘run_cron.sh’ but replaced it’s STDOUT with a pipe that had a full write-end, which means that any additional writes to the pipe, by the process (such as ‘write to STDOUT’ in step 7.1) would block.
The above executable created the /tmp/log/pwnme.log in a tight loop, trying to get in between of 6 and 7.
Once the root-logrotate process was blocked, our executable did the following:
- Read from the pipe, to release the root-logrotate process, that was stuck in step 7.1
- Tight loop of X cycles, so that step 7.2 finishes, but before step 8
- mv /tmp/log /tmp/dummy && ln -s /etc/cron.d/ /tmp/log
By trying increasing X values for the delay, the race was won.
Finally
ls -l /etc/cron.d/
-rw-r–r– 1 root root logrotate
-rwxr-xr-x 1 user user pwnme.log
Note that pwnme.log is executable, since logrotate created the file with the original permissions of the /tmp/log/pwnme.log, that, in turn, is under our control.
The end was trivially
echo “#!/bin/sh” >> /etc/cron.d/pwnme.log
echo cat /flag >> /etc/cron.d/pwnme.log
./run_cron
35c3_rotating_as_intended
After thoughts
The race was won pretty crudely, and it may also be the case, that we were “helped” by other people running in parallel on the same machine, attempting to solve this or other challenges.
We’re pretty sure that there are much better ways to win this race, perhaps using the inotify interface, or even if permitted, a fuse mount.
The inotify interface can reveal when files are created, indicating the right time to make the symbolic link.
A fuse mount over /tmp or /tmp/log can be used to block logrotate, and give ample time to create the symbolic link.
We would like to see other solutions to this challenge, and learn from them 🙂