Logrotate / ZajeBiste / 500 points

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 1”

Environment overview

The nc seems to enter an nsjail docker instance running an unprivileged shell.
nc 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

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)
		logrotate		← The latest logrotate executable ver 3.11.0
 	 	logrotate.conf		← logrotate main config file, includes /etc/logrotate.d/*
           logrotate	← Runs logrotate with main config file
		pwnme		← logrotate log definition

		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 {
	rotate 12
	missing ok
	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.


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.

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. 

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

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 🙂