1.1.1 SQL注入基础

1.整数型注入

整数型注入即参数为整数型,参数两侧无引号或者其他符号。SQL语句类似于select*from news where id=参数,靶场环境为CTFHub技能树-WEB-SQL注入-整数型注入。

首先判断是否存在整数型注入。

SQL语句:select*from news where id=1 and 1=1。

SQL语句:select*from news where id=1 and 1=2。

然后,根据回显的不同,我们可以判断插入的语句是否被解析为SQL语法,进而判断是否存在整数型注入。

接着我们可以利用union联合查询语法注出数据。

SQL语法中的union联合查询用于合并两个或多个select语句的结果集,union内部的每个select语句必须拥有相同数量的列。在某些数据库,如Oracle数据库中,每个select语句中列的数据类型也必须一致。union注入的步骤如下。

首先确认查询的列数,一般有两种方法。

第一种是利用order by语句进行查询,代码如下。

SQL语句:select*from news where id=1 order by 1。

SQL语句:select*from news where id=1 order by 2。

SQL语句:select*from news where id=1 order by 3。

因为输入1 order by 3时返回错误,所以列数为2。

第二种是利用union语句进行查询,代码如下。

SQL语句:select*from news where id=1 union select 1。

SQL语句:select*from news where id=1 union select 1,2。

因为输入1 union select 1,2时返回正常,所以列数为2。

判断出列数后,可以直接使用union语句查询数据库名,代码如下。

SQL语句:select*from news where id=-1 union select 1,database()。

这里id=-1的原因是回显数据的时候只会显示一条数据,需要让第一个select语句查询返回空。结果如图1-1所示,数据库名为sqli。

图1-1 SQL注入查询出数据库名

MySQL5.0以上的版本中,有一个名为information_schema的默认数据库,里面存放着所有数据库的信息,比如表名、列名、对应权限等,我们可以通过它查询数据库表名,代码如下。

SQL语句:select*from news where id=-1 union select 1,table_name from information_schema.tables where table_schema='sqli'。

执行结果如图1-2所示。

图1-2 SQL注入查询出表名

虽然得到了数据库sqli中的一个表名为news,但是数据库中一般会有多个表,当出现需要查询的数据不只一条,而回显只能显示一条数据的情况时,可以通过group_concat()函数将多条数据组合成字符串并输出,或者通过limit函数选择输出第几条数据。

这里我们使用group_concat()函数一次查询出所有表名,代码如下。

SQL语句:select*from news where id=-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'。

执行结果如图1-3所示。

图1-3 使用函数查询出多个表名

得到news表与flag表,我们需要的数据就在flag表中。

查询表中的列名同样是通过information_schema数据库进行查询,代码如下。

SQL语句:select * from news where id=-1 union select 1,group_concat(column_name)from information_schema.columns where table_schema='sqli' and table_name='flag'。

执行结果如图1-4所示。

图1-4 查询列名

查询到flag表中只有一个列flag。

数据库名、表名、列名已经被我们通过注入查询出来了,下面直接查询列中的数据即可,代码如下。

SQL语句:select*from news where id=-1 union select 1,group_concat(flag) from sqli.flag。

注入结果如图1-5所示,成功通过SQL注入获取flag。

图1-5 查询列中的数据

2.字符型注入

字符型注入即参数为字符型,参数两侧受引号或者其他符号影响。与整数型注入相比,字符型注入多了一个引号闭合的步骤。

SQL语句:select*from news where id='参数'。

靶场环境:CTFHub技能树-WEB-SQL注入-字符型注入。

判断参数两边的符号,代码如下。

SQL语句:select*from news where id='1' and '1'='1'。

SQL语句:select*from news where id='1' and '1'='2'。

由此可以判断存在SQL注入,并且为字符型注入,利用union注入获取数据即可。

3.报错注入

报错注入是利用数据库的某些机制,人为制造错误条件,在报错信息中返回完整的查询结果。在无法进行union注入并且回显报错信息时,报错注入是不二之选。

下面介绍利用floor()函数报错注入的方法。

首先利用floor(rand(0)*)产生预知的数字序列01101,然后利用rand()函数的特殊性和group by语法中的虚拟表,引起报错。MySQL版本号需要大于或等于4.1,代码如下。

执行结果如图1-6所示。

图1-6 利用floor()函数报错注入

下面介绍利用extractvalue()函数报错注入的方法。

extractvalue()函数语法如下。

第一个参数XML_document是String格式,表示XML文档对象的名称。第二个参数XPath_string表示Xpath格式的字符串。

extractvalue()函数的作用是从目标XML中返回包含所查询值的字符串。当第二个参数不符合XPath语法时,会产生报错信息,并且将查询结果放在报错信息中。由于extractvalue()函数是MySQL 5.1.5版本添加的,因此使用它进行报错注入时需要满足MySQL版本号大于或等于5.1.5。

执行结果如图1-7所示。

图1-7 利用extractvalue()函数报错注入

extractvalue()函数最长报错32位,在注入时经常需要利用切片函数,如substr()函数获取完整数据。

下面介绍利用updatexml()函数报错注入的方法。

updatexml()函数使用不同的XML标记匹配和替换XML块,代码如下。

第一个参数XML_document是string格式,表示XML文档对象的名称。第二个参数XPath_string代表路径,是XPath格式的字符串。第三个参数new_value是string格式,替换查找到的符合条件的数据。

使用updatexml()函数时,如果XPath_string格式出现错误,MySQL会爆出语法错误(XPath syntax)。与extractvalue()函数相同,updatexml()函数在MySQL 5.1.5版本添加,使用它进行报错注入时,需要满足MySQL版本号大于或等于5.1.5。

在MySQL中,exp()函数的作用是返回e的幂次方。当传入的参数大于或等于710时会报错,并且会返回报错信息。利用这种构造报错可以回显信息,代码如下。

~表示按位取反操作,可以达到溢出的效果。

整型溢出是利用子查询引起BITINT溢出,从而设法提取数据。我们知道,如果一个查询任务成功返回,其返回值为0,那么对其进行逻辑非运算的结果就会变成1,例如对(select * from(select user())x)进行逻辑非运算,返回值就是1。我们通过组合取反运算和逻辑非运算可以构造报错并回显信息,代码如下。

下面介绍一种在Oracle 8g、9g、10g版本中不需要任何权限就能构造报错的方法。需要注意的是,在Oracle 11g及之后的版本中,官方加强了访问控制权限,必须有网络访问权限,才能使用此方法,代码如下。

ctxsys.drithsx.sn()函数在Oracle中用于处理文本,当传入参数类型错误时,会返回异常,代码如下。

CTXSYS.CTX_REPORT.TOKEN_TYPE()函数的作用与ctxsys.drithsx.sn()函数类似,用于处理文本。

XMLType在调用的时候必须以<:开头,以>结尾。需要注意的是,如果返回的数据中有空格,返回结果会被截断,导致数据不完整。这种情况下应先转为十六进制编码,再导出。

SQL Server的报错注入主要利用的是在类型转化错误时,显示类型转换失败的值,类型转换函数如下。

下面以CTFHub技能树中的报错注入靶场为例进行介绍。打开靶场输入参数1',结果如图1-8所示。

图1-8 探测报错注入

可以看到回显了报错信息,我们尝试报错注入。

结果如图1-9所示。

图1-9 报错注入得到数据库名

运行结果表明,成功在报错信息中报出数据库名为sqli。

接着爆破出sqli库下的表,代码如下。

执行结果如图1-10所示。

图1-10 报错注入得到表名

得到news表和flag表之后可以发现,我们要找的数据在flag表中。继续爆破出flag表下的列,代码如下。

执行结果如图1-11所示。

图1-11 报错注入得到列名

flag表下只有一个flag列,直接读出数据,查询flag表下的flag列,代码如下。

执行结果如图1-12所示。

图1-12 报错注入得到部分flag

成功得到flag,注意观察回显的数据不是完整的,这是因为extractvalue()函数和updatexml()函数一次最多只能爆出32位字符,所以需要通过字符串截取函数获取剩余的字符。

执行结果如图1-13所示。

图1-13 截取后的结果

4.布尔盲注

当注入点没有直接的回显,只有True(真)和False(假)两种回显时,我们可以通过回显的结果,推断注入的语句执行结果是True还是False。即使没有直接回显数据,我们也能通过不断调整判断条件中的数值,逐个字符地枚举数据库,代码如下。

布尔盲注最重要的步骤是构造布尔条件,下面列出一些常见的绕过方法。

● 正常情况:'or bool#、true'and bool#。

● 不使用空格、注释:'or(bool)='1、true'and(bool)='1。

● 不使用or、and、注释:'^!(bool)='1、'=(bool)='、'||(bool)='1、true'%26%26(bool)='1、'=if((bool),1,0)='0。

● 不使用等号、空格、注释:'or(bool)<>'0、'or((bool)in(1))or'0。

● 其他:or(case when(bool)then 1 else 0 end)。

布尔盲注常用函数如表1-1所示。

表1-1 布尔盲注常用函数

下面以CTFHub技能树中的布尔注入靶场为例进行介绍。

输入一些测试数据,发现只有两种回显,query_success、query_error,并不会回显具体的数据,数据结果如图1-14、图1-15所示。

图1-14 正常查询

图1-15 错误查询

我们构造一个布尔条件来判断注入语句的执行结果。输入1 and(1=1),执行结果如图1-16所示。

图1-16 布尔条件为真

输入1 and(1=2),执行结果如图1-17所示。

图1-17 布尔条件为假

可以看到,当拼接后的语句正确时,回显结果为query_success,否则回显结果为query_error。我们通过不同的回显结果,逐个字符地枚举数据,代码如下。

代码中substr((select database()), 1, 1)='a'的意思是判断select database()语句查询结果的第一个字符是否为a。这样我们只需要遍历字符就能判断出数据库名的第一位字符。执行结果如图1-18所示。

图1-18 判断第一个字符

可以看到,输入1 and(substr((select database()),1,1)='s')的回显结果为query_success,说明数据库名的第一个字符为s。执行结果如图1-19所示。

图1-19 判断出数据库第一个字符

接着继续枚举第二个字符,直到枚举出所有数据,这个过程可以通过脚本实现。

5.时间盲注

时间盲注与布尔盲注类似,区别在于时间盲注是通过页面的响应时间判断语句的真假,一般格式如下。

两个常用的延时函数如下。

其他导致延时效果的方法如下。

利用笛卡儿积延时注入的代码如下。

下面以CTFHub技能树时间盲注靶场为例进行介绍。

无论输入什么,回显结果都是空的,我们无法通过回显结果来判断SQL语句是否执行成功。正常数据回显如图1-20所示。

这时候可以利用时间盲注来注出数据。输入1 and sleep(0),执行结果如图1-21所示。

图1-20 正常数据回显

图1-21 睡眠0秒响应时间

输入1 or sleep(5),执行结果如图1-22所示。

图1-22 睡眠5秒响应时间

根据响应包的时间可知,输入的延时语句确实被执行了。输入我们构造好的语句,当语句执行结果正确时执行延时函数,错误时不执行延时函数,这样就可以通过响应包的时间逐个字符枚举出数据。

例如,构造一个SQL语句,当if语句中的判断结果正确时延时3秒,代码如下。

执行结果如图1-23所示。

图1-23 语句正确时延时3秒

if语句中的判断结果错误时无延时,代码如下。

执行结果如图1-24所示。

图1-24 语句错误时无延时

利用延时来判断结果,我们通过此方法枚举数据库名的第一个字符,代码如下。

执行结果如图1-25所示。

图1-25 语句错误时无延时

当枚举到s字符时延时了3秒,说明数据库名的第一个字符为s,如图1-26所示。

图1-26 判断出数据库名的第一个字符

接着继续枚举第二个字符,直到枚举出所有数据,这个过程可以通过脚本实现。

值得一提的是,因为延时的原因,时间盲注的枚举速度慢,在有其他方法能够注出数据时一般不建议使用时间盲注。