michael orlitzky

CVE-2017-14681: P3Scan privilege escalation via PID file manipulation

Product
P3Scan (transparent email proxy server)
Versions affected
3.0_rc1 and earlier (all versions)
Published on
2017-09-21
Author
Michael Orlitzky
Bug report
https://sourceforge.net/p/p3scan/bugs/33/
MITRE
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-14681
OSS-security
http://www.openwall.com/lists/oss-security/2017/09/21/7

Summary

The p3scan daemon creates its PID file after dropping privileges to a non-root user. That may be exploited (through init scripts or other management tools) by the unprivileged user to kill root processes, since when the daemon is stopped, root usually sends a SIGTERM to the contents of the PID file (which are under the control of the runtime user). P3Scan itself ships two init scripts vulnerable to this attack.

Details

The purpose of the PID file is to hold the PID of the running daemon, so that later it can be stopped, restarted, or otherwise signaled (many daemons reload their configurations in response to a SIGHUP). To fulfil that purpose, the contents of the PID file need to be trustworthy. If the PID file is writable by a non-root user, then he can replace its contents with the PID of a root process. Afterwards, any attempt to signal the PID contained in the PID file will instead signal a root process chosen by the non-root user.

The creation of the PID file can be traced to the following code in p3scan.c (modulo some whitespace changes):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cuid=getuid();

if (cuid == 0) {
  do_log(LOG_DEBUG, "Changing uid (we are root)");
  pws = getpwnam(config->runasuser);

  // Emergency: main: Unknown user in configuration file
  if (pws == NULL)
    do_log(LOG_EMERG,
           "ERR: Unknown User '%s'",
           config->runasuser);

  if (setgid(pws->pw_gid))
    do_log(LOG_DEBUG,
           "setgid returned: %s",
           strerror(errno));

  if (setuid(pws->pw_uid))
    do_log(LOG_DEBUG,
           "setuid returned: %s",
           strerror(errno));
}

cuid=getuid();
guid=getgid();
pws = getpwuid(cuid);
grp = getgrgid(guid);
do_log(LOG_DEBUG,
       "Running as user: %s group: %s",
       pws->pw_name,
       grp->gr_name);

if ((pd=fopen(config->pidfile, "w+")) != NULL) {
  fprintf(pd, "%i\n", getpid());
  fclose(pd);
}

This is commonly exploitable through init scripts that are run as root and which blindly trust the contents of their PID files. The p3scan package ships two such init scripts,

  1. etc/init.d/p3scan
  2. etc/rc.d/rc.p3scan

both containing essentially the same code:

1
2
3
4
5
6
stop)
   # Stop p3scan
   if [ -a /var/run/p3scan/p3scan.pid ]; then
      kill `cat /var/run/p3scan/p3scan.pid` &>/dev/null
      rm -f /var/run/p3scan/p3scan.pid
      echo "P3Scan terminated"

P3Scan is unable to run in the foreground, preventing the typical workaround where the init system is allowed to manage its PID file.

Exploitation

An example exploit involving an init script is,

  1. I run /etc/init.d/p3scan start to start the daemon.
  2. p3scan drops to the p3scan user.
  3. p3scan writes its PID file, now owned by the p3scan user.
  4. Someone compromises the daemon, which sits on the network.
  5. The attacker is generally limited in what he can do because the daemon doesn't run as root. However, he can write “1” into the PID file, and he does.
  6. I run /etc/init.d/p3scan stop to stop the daemon while I investigate the weird behavior resulting from the hack.
  7. The machine reboots, because I killed PID 1 (this is normally restricted to root).

Workaround

To mitigate the risk, the POSIX ps command can be used to verify the PID data. You can get the user of the process whose PID you find with

root # ps -p <pid> -o user=

and you can get the name of the command with

root # ps -p <pid> -o comm=

Init script authors should check the output of those two command against the expected values before sending a signal to a running process. That will eliminate the most serious scenarios (where the attacker kills, for example, the firewall), but still leaves open the possibility that the attacker can prevent /etc/init.d/p3scan stop from terminating his compromised process by entering junk into the PID file.