SQL注入

总结SQL注入知识

SQL注入

所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。具体来说,它是利用现有应用程序,将(恶意的)SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。 [1] 比如先前的很多影视网站泄露VIP会员密码大多就是通过WEB表单递交查询字符暴出的,这类表单特别容易受到SQL注入式攻击

SQL注入原理

SQL注入产生需要满足两个条件:

  1. 参数可控:前端传给后端内容用户是可以控制的。

  2. 参数带入数据库查询:传入的参数拼接到SQL语句,并且带入数据库查询。

MySQL相关知识点

在MySQL5.0版本之后,MySQL默认在数据库存放了一个information_schema的数据库,其中有如下三个表需要大家了解。

SCHEMATA

SCHEMATA表存储了用户创建的所有数据库的库名,我们可以通过查询展示这一过程。

SELECT * FROM SCHEMATA
SCHEMATA

记录数据库库名的字段为SCHEMA_NAME

TABLES

TABLES 存储了用户创建的所有数据库的库名和表名,我们可以通过查询展示这一过程。

TABLES

其中,TABLE_SCHEMA 记录了数据库的库名,TABLE_NAME 记录了表名。

COLUMNS

COLUMNS 存储了用户创建的所有数据库的库名、表名和字段名,我们可以通过查询展示这一过程。

COLUMNS

其中,TABLE_SCHEMA 记录了数据库的库名,TABLE_NAME 记录了数据库的表名,COLUMN_NAME 记录了数据库的字段名。

MySQL查询语句

SELECT 要查询的字段名 FROM 库名.表名 WHERE 已知条件1的字段名='已知条件1的值' AND 已知条件2的字段名='已知条件2的值'

Limit

limit 使用格式为limit m,n 其中,m 是记录开始的位置,从0 开始,表示第一条记录,n 是取n 条记录,例如,limit 0,1 表示从第一条记录开始,取一条记录。

LIMIT

常用函数

函数名

备注

database()

当前网站使用的数据库

version()

当前MySQL的版本

user()

当前MySQL的用户

注释符

MySQL中,常见的注释符有# 或者-- (注意空格)或者/**/

内联注释

内联注释的形式:/*!code*/ ,内联注释可以用于整个SQL语句,用来执行我们的SQL语句。

SELECT * FROM `SCHEMATA` /*!UNION*/ /*!SELECT*/ 1,USER(),3,4,5

内联注释

注入方式

MySQL隐式类型注入

MySQL隐式类型注入

Union注入

如下代码:

这里是一个正常的查询操作,但是由于没有进行任何的过滤,将参数直接拼接到了SQL语句中,语句也未进行预处理,并且$_GET 参数攻击者可控,因此攻击者可以利用SQL注入漏洞对系统发起攻击。

正常访问:

正常访问

id=1后面添加'

1’返回为空

这里说明有可能存在SQL注入,为了进一步判别是否存在漏洞,我们要进行验证是否可以进行逻辑运算,比如我们访问id=1 and 1=1 ,由于1=1 为真,因此应返回与id=1 相同的页面,同样的,id=1 and 1=2 应返回不同的页面。

and 1=1
and 1=2

因此可以判断网站存在SQL注入漏洞,我们可以通过以下过程,通过漏洞模拟攻击者获得数据库相关信息。

  1. 判断数据表字段数量order by num

  2. 判断可输出字段union select 1,2,...,num

  3. 替换可显示字段,输出我们想执行操作

order by 3 显示正常
order by 4 显示不正常

说明一共有3个字段,接下来我们尝试哪个字段是可以输出的

union select 1,2,3

说明位置23 是可以输出的,替换我们想执行的操作,以user()database() 举例

union select 1,user(),database()

可以得出当前用户是root用户,数据表为users 表,我们也可以通过控制id 参数,给其设定一个不存在的值,让其返回union select 的值

id=-1 union select 1,user(),database()

Boolean注入

如下代码:

我们可以看到代码中还是通过get 方式获取id 的值,并且拼接到SQL语句中,但是前面做了一个正则匹配,如果检测到unionsleepbenchmark 就退出并返回no ,而且使用了i 修正符,因此会不区分大小写去文本中匹配 php,这意味着我们无法使用联合注入以及延时注入,并且是无法用大小写绕过的,后面的判断条件是如果SQL执行成功就会返回yes ,否则返回no ,这里可以判断为典型的boolean注入,我们可以使用如下过程模拟攻击者对系统发起攻击。

  1. 通过 ' and length(database())>=num --+ 判断数据库名称长度,由于我们可以看到id 参数被' 包围,因此我们先闭合前面的单引号,使得后面我们的payload 得以正常执行。

  2. 通过 ' and substr(database(),1,1)='t' --+ 逐字符判断的方式(不区分大小写)获取数据库名或通过 ' and ord(substr(database(),1,1)) = 115 --+使用ASCII 转换函数ord 逐字符判断,这里里substr 是截取database() 的值,从第1 个字符开始,每次只返回1 个,这里不要与上面的limit 弄混,limit 是从0 开始排序的。

' and length(database())>=6 --+

当我们测试数据库长度大于等于6 时,报了no ,说明数据库长度为5

' and substr(database(),1,1)='u' --+

当我们测试数据库第一个字符为u 时,报了yes ,说明数据库库名第一个字母为u ,我们可以写一个python脚本,来代替我们实现这一繁琐的过程。

我们可以看到数据库库名长度为5

判断数据库库名长度

我们可以看到数据库库名每个字符是什么,拼接到一起即数据库库名为users

判断数据库名称

同样的,如果substr() 被禁用了,我们也可以使用MID() 或者LEFT() 来实现上述过程。

SELECT MID(database(),1,1);

SELECT left(database(),1);

除了上述使用函数的方法之外,其实我们还可以采用正则匹配 的方法来进行注入,因为是支持正则表达式的,比如我们可以使用:

select user() regexp '^[a-z]';

我们的数据库用户是root ,比如这里我们可以用正则表达式逐位判断是否用户名都是由小写字母组成的以及每一位是什么,比如,我们猜解第一位,当我们猜测正确时,返回值为1

select user() regexp '^r[a-z]';

当我们猜测不正确时,返回值为0

select user() regexp '^x[a-z]';

由此我们就可以逐位猜解每个位置上的字母:

POC:

同样的,我们可以写成脚本来代替我们进行操作:

在MySQL中,与上述正则类似,我们还可以用like 匹配

select user() like 'ro%'

注意:

报错注入

如下代码:

从代码得知,只要是数据库连接正常,SQL语句正常执行,就会返回ok ,否则返回数据库连接错误信息,并显示到页面上,所以我们使用以下过程来模拟攻击者攻击的过程。

  1. 让数据库执行语句出错,例如加入',导致错误。

  2. 利用报错payload 来模拟攻击者攻击系统

Duplicate entry报错:

这是由于多次查询插入重复键值导致count报错从而在报错信息中带入了敏感信息。

Payload:

这其中有一个很奇特的现象就是,我们看到POC中有rand(0) ,在经过测试后发现,当数据表中有1条数据时,rand()rand(0) 均不报错,在数据表中有2 条数据时,rand() 随机报错,rand(0) 不报错,在数据表中有3条及以上数据时,rand() 随机报错,rand(0) 稳定报错,在探究这个报错原因之前,我们需要了解以下几个函数:

函数名

作用

count()

计算总数

concat()

连接字符串

floor()

向下取整

rand()

随机产生0~1的随机数

rand(0)

伪随机

这里主要是探究一下rand()rand(0) 的区别,我们都知道rand() 是随机产生一个0~1的随机数,那么rand(0) 呢?

我们首先看一下rand() ,我们执行如下语句。

SELECT `floor(rand()*2) FROMtest```

rand()

我们发现在执行多次后,结果符合随机要求,那么我们再看一下rand(0) ,执行如下语句。

SELECT `floor(rand(0)*2) FROMtest```

rand(0)

我们发现,不论执行多少次,他的序列始终是一个固定值,按照011011... 排列,我们可以看到报错内容是Duplicate entry '15.5.53' for key 'group_key',意思是说group_key条目重复,需要了解的是当我们使用group by进行分组查询的时候,数据库会生成一张虚拟表,整个过程是这样的,开始查询数据,取数据库数据,然后查看虚拟表是否存在,不存在则插入新记录,存在则count(*)字段直接加1,在这张虚拟表中,group by后面的字段作为主键,MySQL官方有给过提示,就是查询的时候如果使用rand()的话,该值会被计算多次,就是第一次是将group by后面的字段值到虚拟表中拿去对比前,首先获取group by后面的值;第二次是假设group by后面的字段的值在虚拟表中不存在,那就需要把它插入到虚拟表中,这里在插入时会进行第二次运算。 那么我们在看整个过程如下:

查询前,默认建立空的虚拟表如下:

Title

Title

key

count(*)

Content

Content

取第一次记录的时候,首先执行floor(rand(0)*2) ,我们从rand(0) 的图中也可以知道,第一次值为0 ,表中不存在,则继续执行插入操作,在插入操作的时候,进行了第二次运算,同理我们从图中得知,值为1 ,插入虚表的key中,由于是第一条数据,所以count(*) 的值也为1 ,此时第一条信息查询完毕,结果如下。

key

count(*)

1

1

接下来查询第二条记录的时候,再次计算floor(rand(0)*2) ,从图中得知,已经进行到了第三次运算,它的值是1 ,这时候发现数据已经存在,所以不再运算,count(*) 的值直接加1 ,此时结果如下。

key

count(*)

1

2

接下来查询第三条记录,再次计算floor(rand(0)*2) ,从图中得知,已经进行到了第四次运算,它的值是0 ,虚拟表中没有响应的键值,则准备进行插入操作,进行了第五次运算,它的值为1 ,然而1 这个主键已经存在于虚拟表中,由于键值必须唯一,因此导致报错。

从而我们也得知了为什么前面解释的数据量不同,rand()rand(0) 会有不同的反响。由于rand() 本身的随机性,因此有几率触发报错,而rand(0) 的稳定序列011011... 导致其在3 条数据以上稳定报错。

在研究明白上面的过程后,我们已经可以得到我们想要的Duplocate entry错误了,接下来就是加入我们的子查询了,我们用concat() 拼接,比如我们想要查询数据库的用户,命令如下。

select count(*) ,concat(user(),floor(rand(0)*2))x from test group by x

我们可以看到已经看到想要的内容了,那么是不是直接拼接到后面就行了呢?

结果是不行的,因为我们在数据库中的代码如上,

我们构建的select语句的结果是一个结果表,而and需要一个布尔值,也就是0非零的值,所以我们需要再嵌套一个查询,由于select 的结果是一个结果表,那我们就可再从这个表执行查询,这不过这次select的值是非零数字:

到此我们也就完成了我们的报错注入。

当然我们也发现,这个poc中需要知道数据库相应的表名test.test ,那么如果我们不知道关键表名或者关键表名被禁用了怎么办呢?

我们可以使用如下语句来进行注入:

select count(*) from (select 1 union select null union select !1)a group by concat(version(),floor(rand(0)*2))

换成POC:

zhangsan' and(select 1 from (select count(*) from (select 1 union select null union select !1)a group by concat(version(),floor(rand(0)*2)))a) --+

这里我们首先需要知道如下规则:

语句

含义

UNION

将多个不同的select语句执行的结果合并到一个结果集并返回

SELECT 1;

直接返回字面量1的值

SELECT null;

返回null本身的“值”。NULL是mysql中的一个常量,表示“没有值”,不是空字符串或0。

SELECT !1

逻辑非,非真即假,mysql中使用数字值1、0代表true和false,可用“SELECT TRUE, FALSE;”验证

其次,我们也要清楚,select 可以当作运算器,例如:

因此语句的最终结果为:

与上同理,正好满足三条的需要,最终多次查询插入重复键值导致count报错从而在报错信息中带入了敏感信息。

如果rand 被禁用了,我们可以通过用户变量来报错:

select min(@a:=1) from information_schema.tables group by concat(version(),@a:=(@a+1)%2);

这里我们需要注意,由于浏览器会进行urlencode ,所以我们语句中的+ 会被转为空格 ,会导致我们的语句无法正常的传入数据库,因此我们需要用%2b 替换+ ,因此最终POC如下:

ddd%20%27%20and%20(select%20min(@a:=1)%20from%20test%20group%20by%20concat(version(),@a:=(@a%2b1)%2))%20--+

Xpath报错:

MySQL 5.1.5版本中添加了对XML文档进行查询和修改的两个函数:extractvalue、updatexml

名称

描述

使用XPath表示法从XML字符串中提取值

返回替换的XML片段

通过这两个函数可以完成报错注入

由官网文档可知, ExtractValue(xml_frag, xpath_expr) 有两个字符串参数, 一个XML标记片段 xml_frag和一个XPath表达式 xpath_expr(也称为定位器); 它返回CDATA第一个文本节点的text(),该节点是XPath表达式匹配的元素的子元素。

例如:SELECT ExtractValue('<a><b><b/></a>', '/a/b'); 就是寻找前一段xml文档内容中的a节点下的b节点,这里如果Xpath格式语法书写错误的话,就会报错。这里就是利用这个特性来获得我们想要知道的内容, 注意:extractvalue()能查询字符串的最大长度为32,就是说如果我们想要的结果超过32,就需要用substring()函数截取,一次查看32位,且不支持低版本 MySQL。

比如我们输入:

会引发错误:

我们仍然可以利用concat函数将想要获得的数据库内容拼接到第二个参数中,报错时作为内容输出。

这时我们已经可以在报错信息中看到版本号了:

POC:

UpdateXML(xml_target, xpath_expr, new_xml) ,由官网文档可知,此函数用来更新选定XML片段的内容,将XML标记的给定片段的单个部分替换为 xml_target 新的XML片段 new_xml ,然后返回更改的XML。xml_target替换的部分 与xpath_expr 用户提供的XPath表达式匹配。

如果xpath_expr未找到表达式匹配 ,或者找到多个匹配项,则该函数返回原始 xml_targetXML片段。所有三个参数都应该是字符串。使用方式如下:

同理,和上面的extractvalue函数一样,当Xpath路径语法错误时,就会报错,报错内容含有错误的路径内容:

POC:

同extractvalue()函数,updatexml()函数能查询字符串的最大长度也是32,如果超过则也需要使用substring()函数截取,一次查看32位。

整形溢出报错:

BIGINT Overflow Error Based SQL Injection 中,作者发现当MySQL版本在5.5.5 及其以上时,会存在BIGINT溢出 的现象(实测5.5.29 复现成功,5.7.26 以及8.0.12 失败,当mysql版本>5.5.53时,无法利用exp()函数)

数据类型对应的范围如下:

也就是说如果我们使用例如加法 的运算表达式使得数值超越了最大值,就会触发BIGINT value is out of range” error

例如:

我们如果对0 取反,就会得到这个BIGINT的最大值18446744073709551615

因此,我们对~0 进行加减操作同样会造成溢出。

因此我们只需要利用子查询引起BITINT溢出,从而设法提取数据。我们知道,如果一个查询成功返回,其返回值为0,所以对其进行逻辑非的话就会变成1

例如:

由上我们就可以组合进行注入了:

我们已经成功的注入出了用户为root

但是我们一定要注意,+ 在浏览器中会被编码为空格 ,因此我们可以使用- 替换+ 或者用%2b 替换+

与之类似的还有如下POC:

由于对函数也可以进行取反操作,所以以下方式也是可以的:

作者经过测试,发现以下函数都是可以的(包括但不限于):

在发现BIGINT 溢出之后,作者发现,当传递一个大于709的值时,函数exp()就会引起一个溢出错误。 exp()即为以e为底的对数函数,如等式:

当涉及到注入时,我们使用否定查询来造成“DOUBLE value is out of range”的错误。作者之前的博文提到的,将0按位取反就会返回“18446744073709551615”,再加上函数成功执行后返回0的缘故,我们将成功执行的函数取反就会得到最大的无符号BIGINT值。

你可以通过load_file()函数来读取文件,但作者发现有13行的限制,该语句也可以在BIGINT overflow injections中使用。

注意,你无法写文件,因为这个错入写入的只是0

数据重复报错:

在MySQL中,列名重复会报错,所以name_const()函数就是利用这一特性,重新定义一个重复的列名来让数据库报错。

因此我们可以利用这一特点来利用报错带出我们的信息:

或者可以利用join 来查询

几何函数报错:

mysql有些几何函数,例如:

geometrycollection()multipoint()polygon()multipolygon()linestring()multilinestring(),这些函数对参数要求是形如(1 2,3 3,2 2 1)这样几何数据,如果不满足要求,则会报错。经测试,在版本号为5.5.47上可以用来注入,而在5.7.17上则不行:

延时注入:

如下代码:

我们可以看到,程序获取GET 参数ID ,通过preg_match 判断参数ID 中是否存在UNION 危险字符,将参数ID 拼接到SQL语句中,从数据库中查询SQL语句,成功会返回yes ,否则会返回no ,除了用boolean注入 的方式之外,我们还可以利用时间注入 的方式,这样我们就可以解决一些类似于页面无回显无报错信息 甚至是无法用布尔判断真假 的情况, 提交对执行时间铭感的函数sql语句,通过执行时间的长短来判断是否执行成功,比如:正确的话会导致时间很长,错误的话会导致执行时间很短,这就是所谓的延时注入,属于盲注的一种类型。我们可以使用sleep() 或者benchmark 等方法来验证是否存在注入。

sleep():

我们可以看到这是一个休眠函数,我们对duration 参数设定相应的参数值,就会执行相应的休眠操作。

这时候我们就可以结合IF() 函数来结合进行判断。

if 函数的语句如上,即如果expr1 为真,则返回值为expr2 ,否则为expr3

因此我们可以利用:

来进行判断,即如果数据库名称的长度大于1,则休眠5 秒,否则查询1

POC:

我们可以看到,当判断数据库名称长度>1 时,完成时间在5 秒左右。

而当条件改成>10 的时候,完成时间只有115 毫秒。

说明数据库名称长度在1~10 之间,我们同样可以写一个脚本来完成数据库长度的遍历工作。

同样的,我们也可以用之前的substr() 等函数来进行爆字符的操作。

POC:

我们这里首先了解如下函数

ORD(string)

ORD() 函数返回字符串第一个字符的 ASCII 值。

我们可以看到t 的ascii值为116 ,我们可以优化以下我们的脚本,来爆出数据库名称。

至此,我们已经得到数据库名称test

同样的,我们也可以利用benchmark() 函数来完成同样的操作,首先我们先了解一下这个函数是做什么的。

BENCHMARK()用于测试函数的性能,参数一为次数,二为要执行的表达式。可以让函数执行若干次,返回结果比平时要长,通过时间长短的变化,判断语句是否执行成功。(这是一种边信道攻击,在运行过程中占用大量的cpu资源。)

我们可以看到,消耗时间为5.6445 秒。

POC:

我们只需将代码中的sleep() 相应的benchmark 即可。

之前做过一个中科院的题目,其中将sleepbenchmark 都禁用了,当时查资料的时候发现了一些其他的方式,这里借用pwnhub的代码:

我们可以看到在$sql我们传入的参数会被强转int,这里显然就不存在注入了,然而下面update 处直接拼接到了SQL语句中,报错被禁止,因此不能进行报错注入,load_file 被禁用,而且不知道是不是root 权限,因此dns_log 方法也放弃,又因为页面没有回显,因此普通注入也无法进行,唯一剩下延时注入,其中sleepbenchmark 均被禁用,所以我们需要找到其他的延时函数。

通过查询资料,我们发现有如下几种方法。

笛卡尔积:

这种方法又叫做heavy query,原理就如方法的名字:大负荷查询 即用到一些消耗资源的方式让数据库的查询时间尽量变长 而消耗数据库资源的最有效的方式就是让两个大表做笛卡尔积,这样就可以让数据库的查询慢下来 而最后找到系统表information_schema数据量比较大,可以满足要求,所以我们让他们做笛卡尔积。

例如:

我们可以看到,通过不断地让大表做笛卡尔积,时间也在增加,达到我们延时的目的。

GET_LOCK

我们可以先看一下文档中是怎么描述的:

GET_LOCK(str,timeout)

Tries to obtain a lock with a name given by the string str, using a timeout of timeout seconds. A negative timeout value means infinite timeout. The lock is exclusive. While held by one session, other sessions cannot obtain a lock of the same name.

get_lock是MySQL的锁机制,我们可以看到其中描述写道“当一个会话持有时,其他会话无法获取同样的名字。”,get_lock会按照str来加锁,别的客户端再以同样的str加锁时就加不了了,处于等待状态。 当调用release_lock来释放上面加的锁或客户端断线了,上面的锁才会释放,其它的客户端才能进来。也就是说我们可以通过在一个session中可以先锁定一个变量例如:select get_lock('do9gy',1)

然后通过另一个session 再次执行get_lock函数 select get_lock('do9gy',5),此时会产生5 秒的延迟,其效果类似于sleep(5)。

所以我们的攻击过程如下 先上锁 再进行盲注

  1. 先执行 1' and get_lock(1,2)%23 给key=1上锁

  2. 等待1-2分钟,让服务器将我们下一次的查询当做客户B

  3. 然后就可以盲注了 1' and if(1,get_lock(1,2),1)%23 再次执行同样的语句会产生延时

SESSION A
SESSION B

我们可以看到,我们通过这种方式,已经获得了延时的效果。

但是需要注意的是,这种方法的使用情况有限,即长连接 一般在php5版本系列中,我们建立与Mysql的连接使用的是mysql_connect(),题中使用的是

这两者的区别如下:

函数名

功能

mysql_connect()

脚本一结束,到服务器的连接就被关闭

mysql_pconnect()

打开一个到 MySQL 服务器的持久连接

因此,如果使用的是mysql_connect()mysql_connect()一结束,就会立刻关闭连接,这就意味着,我们刚刚对资源d09gy加完锁就立刻断开了,我们get_lock() 的利用条件也就被破坏了。即第一次加锁后,需要等待1~2分钟,再访问的时候服务器就会判断你为客户B,而非之前加锁的客户A 此时即可触发get_lock()

RLIKE

通过rpadrepeat构造长字符串,加以计算量大的pattern,通过repeat的参数可以控制延时长短。

我们同样也得到了延时的效果。

堆叠注入:

我们知道,数据库基本操作有“增删改查”,我们前面说过的注入都是对原来sql语句传输数据的地方进行相关修改,注入情况会因为该语句本身的情况而受到相关限制,例如一个select语句,那么我们注入时也只能执行select操作,无法进行增、删、改。因此我们可以利用堆叠注入,可以堆一堆sql注入进行注入,这个时候我们就不受前面语句的限制可以为所欲为了。其原理也很简单,就是将原来的语句构造完后加上分号,代表该语句结束,后面在输入的就是一个全新的sql语句了,这个时候我们使用增删查改毫无限制。

注意:堆叠注入受到API或者数据库引擎(例如oracle不能使用堆叠注入),又或者权限的限制,使用情况有限,比如:mysqli_multi_query()函数就支持多条sql语句同时执行,而现实中,PHP为了防止sql注入机制,往往使用调用数据库的函数是mysqli_ query()函数,其只能执行一条语句,分号后面的内容将不会被执行。

如下代码:

这里我们可以看到,程序获得GET 参数ID ,使用PDO 的方式进行数据查询,但是仍然将参数ID 直接拼接到了查询语句中,PDO 没有起到预编译的效果,仍存在注入。

POC:

我们可以看到数据已经注入进数据库了,我们也可以利用sleep() 等函数,将判断语句加入POC来得到我们想要的信息。

我们执行过后,发现程序延时了3 秒,说明数据库名称第一位为t ,此内容已在上文详细阐述,此处不再重复。

二次注入:

为了防止sql注入的产生,很多开发人员会对数据进行转义操作,然而如果在取出时,没有做相应的操作,就会产生二次注入的风险。

如下代码:

第一部分代码我们模拟了注册用户的操作,代码会在GET 参数ID 中获取usernamepassword ,对username 参数使用addslashes 进行转义操作(转义了单引号,使得语句无法闭合),参数password 进行了MD5哈希 ,因此此处不存在注入,而在第二部分代码中,我们模拟了个人主页查询余额的功能,代码会在GET 参数ID 转成了INT 类型,这里我们也无法拼接SQL语句,无法进行注入,然后将到user 表中获取ID 对应的username ,然后接着到person 表中查询username 对应的数据,而此处并未对数据进行转义,存在注入。

例如我们:

我们可以发现,数据已经被插入数据库中:

当我们插入用户名为test1' 时,

数据被保存在数据库中,由于addslashes 的存在,这里的' 被转义为\' ,因此不会报错,而在MySQL中,会自动去除转义字符也就是反斜杠\ 。因此其实我们使用该语句时,数据库中真实执行的命令如下:

因此我们这里将test1' 插入到了数据库中,而我们当对double2.php 进行使用时,我们首先尝试正常的test1 ,由上文我们知道它的id1 ,我们进行查询:

这里用正常的id去对应person 表中的数据,并取出相应的username 以及money ,但是这里并没有进行任何限制,因此当我们取之前的test1' ,由于' 拼接到了SQL语句中,因此会使得数据库发生报错:

至此,我们就可以利用这一过程进行二次注入的攻击,我们的攻击过程是:

  1. 构造攻击语句

  2. 将攻击语句存储到数据库中

  3. 由于数据库在取出数据时,并没有对数据进行检查,语句得以执行

如上案例,我们可以结合之前联合注入的知识,很容易构造出如下POC

我们首先通过double1.php 将该语句插入数据库中:

接下来利用double2.php 来取出数据:

我们看到,我们的语句已经成功执行了。

宽字节注入:

前面我们讲到,MySQL中为了防止注入问题的发生,一般会采用addslashes ,会将' 转义为/' ,类似的函数还有mysql_real_escape_string()(PHP 4 >= 4.3.0, PHP 5 本扩展自 PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除),mysql_escape_string (PHP 4 >= 4.0.3, PHP 5, 注意:在PHP5.3中已经弃用这种方法,不推荐使用),还有magic_quotes_gpc ,而宽字节注入问题主要是利用MySQL的一个特性,MySQL在使用GBK编码的时候,会认为两个字符是一个汉字。类似的还有GB2312GB18030BIG5Shift_JIS等,但是要注意,GB2312 不存在宽字节注入问题,这主要是GB2312编码取值范围的事情,它高位范围0xA1~0xF7,低位范围是0xA1~0xFE\是%5c,是不在低范围中的,即其根本不是GB2312编码,故其不会被吃掉。故只要低位的范围中含有0x5c的编码,就可以进行宽字节的注入会出现吞字符的现象,在PHP中,通过iconv() 进行编码转换时,也可能存在宽字节注入的问题,我们后面详解。

如下代码:

这里还是上文中二次注入的代码稍加改造了一下,header("Content-type:text/html;charset=GBK"); 是为了将我们数据库中实际执行的代码打印出来,便于查看。mysqli_set_charset($con,'gbk'); 是将字符设置为GBK 编码,接下来我们可以尝试一下:

当我们正常属于id1 时:

我们可以看到数据库中正常执行了select * from users where id=1 并且打印出了后面数据库中对应的内容,当我们想要利用' 进行闭合时:

我们发现,因为有了addslashes 的作用,我们的语句变成了select * from users where id=1\',并且正常执行了,并没有引发报错,那么我们利用%df' 尝试一下:

我们发现无法正常显示了,这事由于GBK编码的编码范围是0x8140~0xFEFE(不包括xx7F),在遇到%df(ascii(223)) >ascii(128)时自动拼接%5c,因此吃掉\,而%27%20小于ascii(128)的字符就保留了,由于\ 被吞并,因此' 又得以逃逸,我们就可以发起注入攻击:

相同的,其它的宽字符集也是一样的分析过程,要吃掉%5c,只需要低位中包含正常的0x5c就行了,我们只需在前面加入奇数%df 类似于这种能与后面的%5c 组成汉字的字符都可以利用(两个%df%df ,多出来的那个%df 就可以吞并\)即可,

iconv() 情况比较奇特,首先我们要知道他的用法:

这里存在两种情况,就是从GBK 转成utf-8 与上文讲述相同,这里不再赘述,而从utf-8 转为GBK 利用方式还是要讲述一下,我们这里的POC:

因为utf-8编码为0xe98ca6GBK 编码为0xe55c ,也就是说,在iconvutf-8转换成gbk后,变成了%e5%5c,而后面的'addslashes变成了%5c%27,这样组合起来就是%e5%5c%5c%27,两个%5c就是\,正好与后面的\组合为\\,导致'逃逸出单引号,产生注入,数据库中实际执行的语句为:

Base64注入:

如下代码:

我们可以从代码得知,代码除了对GET 参数id 进行了base64 加密处理,并未做其他限制,例如我们知道,1base64 编码后结果为MQ== ,我们可以进行访问:

我们看到,正常的查询到了id1 的数据,由上面可知,注入POC:

由于代码中会对传入参数解密一次,因此我们先将POC通过base64 加密一次:

XFF注入:

如下代码:

在PHP中,getenv() 用于获取一个环境变量的值,类似于$_SERVER_ENV ,返回环境变量对应的值,如果变量不存在则返回FALSE

在代码中,程序首先判断是否存在HTTP头部参数HTTP_CLIENT_IP ,如果存在,则赋值给$ip ,如果不存在,则判断是否存在HTTP头部参数HTTP_X_FORWARDED_FOR ,如果存在,则赋值给$ip ,如果不存在,则将HTTP头部参数REMOTE_ADDR 赋值给$ip ,接下来将$ip 拼接到select 查询语句中。

由于HTTP头部参数是可以伪造的,所以我们可以添加一个头部参数CLIENT_IPX_FORWARDED_FOR ,我们首先设置X_FORWARDED_FOR127.0.0.1 ,我们看到顺利查询到了对应的数据。

由于拼接地方未做任何限制,因此我们直接如同上文,拼接查询语句即可:

order by注入:

如下代码:

这里我们可以通过代码得知,我们的注入点在order by 后,它不同于我们where 后面的语句,不能使用union 注入,我们首先可以使用?sort=1 desc 或者?sort=1 asc 来观察结果是否相同来判断是否存在注入,如果不同,则证明可以利用此条件进行注入。

我们可以通过构造sort 后面的参数进行注入:

比如:

利用True 以及False 的返回结果不同我们就可以按照布尔盲注来进行判别,比如:

两次结果不同我们就可以看到是可以利用布尔注入进行注入的,(待考证,两次结果个人感觉无法判断,两次都是乱序的)同理我我们也可以利用报错注入以及延时注入进行注入,POC:

报错注入:

延时注入:

上文我们介绍了limit 注入,其实这里我们可以扩展以下思路:

这里我们分析前面又order by 的语句,我们知道limit后面能够拼接的函数只有intoprocedureinto可以用来写文件,在Limit后面 可以用 procedure analyse()这个子查询,这里我们可以利用extractvalue 报错注入和 benchmark 函数进行延时(不可以使用sleep )。

报错注入:

当然了我们也可以利用into 写文件:

当然我们也可以利用上文中讲过的利用16进制代码上传网马。

利用方式

拖库:

正如上文介绍,数据库中的数据会受到威胁,这里不再详细介绍。

导入导出文件:

首先我们了解一下load_file() 函数

load_file() 函数可以读取文件并且将文件内容以字符串的形式返回。

注意:

MYSQL新特性secure_file_priv对读写文件的影响:

  • ure_file_priv的值为null ,表示限制mysqld 不允许导入|导出

  • 当secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下

  • 当secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

我们可以使用如下命令查看secure-file-priv参数的值:

本地测试的时候,windows下我们可以通过修改mysql.ini 文件,在[mysqld] 下加入secure_file_priv =Linux 下修改my.cnf[mysqld]内加入secure_file_priv =来更改这一配置,更改过后需要重启mysql 服务。

之后我们将路径中的\ 更改为\\ ,就可以读取文件了:

这里我们要注意,load_file() 使用需要以下几个条件:

  1. 必须有权限读取并且文件必须完全可读。

  2. 文件必须在服务器上

  3. 必须指定文件完整的路径

  4. 文件必须小于max_allowed_packet

下面给大家列举一些常用的load_file() 路径:

WINDOWS下:

位置

说明

c:/boot.ini

查看系统版本

c:/windows/php.ini

php配置信息

c:/windows/my.ini

MYSQL配置文件,记录管理员登陆过的MYSQL用户名和密码

c:/winnt/php.ini

php配置信息

c:/winnt/my.ini

MYSQL配置文件,记录管理员登陆过的MYSQL用户名和密码

c:\mysql\data\mysql\user.MYD

存储了mysql.user表中的数据库连接密码

c:\Program Files\RhinoSoft.com\Serv-U\ServUDaemon.ini

存储了虚拟主机网站路径和密码

c:\Program Files\Serv-U\ServUDaemon.ini

存储了虚拟主机网站路径和密码

c:\windows\system32\inetsrv\MetaBase.xml

查看IIS的虚拟主机配置

c:\windows\repair\sam

存储了WINDOWS系统初次安装的密码

c:\Program Files Serv-U\ServUAdmin.exe

6.0版本以前的serv-u管理员密码存储于此

c:\Program Files\RhinoSoft.com\ServUDaemon.exe

6.0版本以前的serv-u管理员密码存储于此

C:\Documents and Settings\All Users\Application Data\Symantec\pcAnywhere*.cif

存储了pcAnywhere的登陆密码

c:\Program Files\Apache Group\Apache\conf\httpd.conf

查看WINDOWS系统apache文件

C:\apache\conf\httpd.conf

查看WINDOWS系统apache文件

c:/Resin-3.0.14/conf/resin.conf

查看jsp开发的网站 resin文件配置信息.

c:/Resin/conf/resin.conf /usr/local/resin/conf/resin.conf

查看linux系统配置的JSP虚拟主机

d:\APACHE\Apache2\conf\httpd.conf

Apache配置文件

C:\Program Files\mysql\my.ini

Mysql配置文件

C:\mysql\data\mysql\user.MYD

存在MYSQL系统中的用户密码

LUNIX/UNIX 下:

位置

说明

/usr/local/app/apache2/conf/httpd.conf

apache2缺省配置文件

/usr/local/apache2/conf/httpd.conf

Aphache配置文件

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf

虚拟网站设置

/usr/local/app/php5/lib/php.ini

PHP相关设置

/etc/sysconfig/iptables

从中得到防火墙规则策略

/etc/httpd/conf/httpd.conf

apache配置文件

/etc/rsyncd.conf

同步程序配置文件

/etc/my.cnf

mysql的配置文件

/etc/redhat-release

系统版本

/etc/issue

系统版本

/etc/issue.net

系统版本

/usr/local/app/php5/lib/php.ini

PHP相关设置

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf

虚拟网站设置

/etc/httpd/conf/httpd.conf

查看linux APACHE虚拟主机配置文件

/usr/local/apche/conf/httpd.conf

查看linux APACHE虚拟主机配置文件

/usr/local/resin-3.0.22/conf/resin.conf

针对3.0.22的RESIN配置文件查看

/usr/local/resin-pro-3.0.22/conf/resin.conf

针对3.0.22的RESIN配置文件查看

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf

Apache虚拟主机配置

/etc/httpd/conf/httpd.conf

查看linux APACHE虚拟主机配置文件

/usr/local/apche/conf /httpd.conf

查看linux APACHE虚拟主机配置文件

/usr/local/resin-3.0.22/conf/resin.conf

针对3.0.22的RESIN配置文件查看

/usr/local/resin-pro-3.0.22/conf/resin.conf

同上

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf

Apache虚拟主机查看

/etc/sysconfig/iptables

查看防火墙策略

load_file(char(47))

可以列出FreeBSD,Sunos系统根目录

replace(load_file(0×2F6574632F706173737764),0×3c,0×20)

查看/etc/passwd(16进制编码了)

replace(load_file(char(47,101,116,99,47,112,97,115,115,119,100)),char(60),char(32))

查看/etc/passwd(Ascii编码

LOAD DATA INFILE:

用于从一个文本中读取行,并装入一个表中,文件名称必须为一个字符串。

注意:load data 需要有处理文件的权限, GRANT FILE ON . TO user@host;

例如:

这里我们指定分隔符, 这里我们要清楚,如果错误代码为2 ,文件不存在;错误代码为13 说明没有权限。

SELECT INTOOUTFILE:

可以把选择的行写入一个文件中,用法如下:

因为我们是要创建一个文件,因此必须拥有文件写入权限(FILE权限)后,才能使用此语法。同时,“目标文件”不能是一个已经存在的文件。

我们一般有两种用法,一种是直接使用select 将内容导入文件中:

还可以修改文件结尾,这里一开始我也没明白是什么意思,直到看完sqlmap os shell解析,大致清楚了是什么意思。

首先介绍语法:

这里我是这样理解的,代码的意思是将内容写入到文件中去,其中LINES TERMINATED BY则是into outfile的参数,意思是行结尾的时候用by后面的内容,一般情况下为‘/r/n’,但是这里我们可以将by后的内容修改为后面的16进制的文件。

比如作者在分析SQLMAP--os-shell 参数时候,抓包发现,用的就是这一句法:

我们解码其中16进制内容代码后发现内容如下:

实现的就是上传文件,同时根据phpversion ,将上传的文件的权限进行修改。

这里需要注意的是要根据具体环境分析文件路径需要不需要转义,在load_file() 前台无法导出数据的时候,我们也可以利用如下命令将服务器中的内容导出到WEB服务器目录下,这样就可以得到数据了:

Dnslog注入:

前面我们也讲过了,没有回显与报错信息的情况下,我们要使用盲注的方法来进行测试,这种注入速度非常慢,需要一个一个字符猜解,而且延时注入还会受到网速的影响,因此我们这里介绍一种其他的方法。

上面我们讲到了load_file ()我们就可以利用它来完成DNSLOG。load_file()不仅能够加载本地文件,同时也能对诸如 \\www.test.comURL发起请求,我们利用该过程不仅可以加快速度,也可以绕过一些防护设备的策略。

首先我们要知道什么是DNS

DNS是进行域名和与之相对应的IP地址转换的服务器。

知道了什么是DNS 之后,我们要了解一下DNS 的解析过程:

当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”。然后让本地服务器进行后续的查询。根域名服务器通常是把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询。顶级域名服务器在收到本地域名服务器的查询请求后,要么给出所要查询的IP地址,要么告诉本地服务器下一步应当向哪一个权限域名服务器进行查询。最后,知道了所要解析的IP地址或报错,然后把这个结果返回给发起查询的主机。

这过程中,红色部分是可以控制的,我们只需要搭建一个红色部分的DNS服务器,并将要盲打或盲注的回显,放到自己域名的二级甚至三级域名上去请求,就可以通过DNS解析日志来获取到它们。

知道了上述过程,我们可以构造利用DNS从有漏洞的数据库中提取数据,要注意的是: DBMS中需要有可用的,能直接或间接引发DNS解析过程的子程序。

Microsoft SQL Server

Oracle

Mysql

PostgreSQL

接下来在讲一下UNC路径

UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器.。UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。UNC路径就是类似\softer这样的形式的网络路径

格式: \servername\sharename ,其中 servername 是服务器名,sharename 是共享资源的名称。 目录或文件的 UNC 名称可以包括共享名称下的目录路径,格式为:\servername\sharename\directory\filename

我们这里可以自己搭建DNS 服务器,也可以利用在线相关平台进行测试,下面给出相关资源:

DNSLOG工具

Bugscan

在线DNSLOG平台

我们这里以MySQL举例,

稍等片刻我们就可以在DNSLOG平台接收到结果,

由于我们自己用了hex() ,因此解密一下即可:

这样我们向查询的信息就被外带出来了。

注意:

比如我们拿SQLI-Labs 举例:

我们稍后可以看到数据库名称已经被记录:

里使用concat函数将(select database())得到的内容作为查询url的一部分,和我们的平台三级域名拼接组合成一个四级域名,而load_file函数会通过dns解析请求,所以我们在dnslog平台就可以看到查询的包含着我们注入出的数据记录。

其实DNSLOG 平台还可以结合很多其他的漏洞进行利用,我们会在之后的章节里面与大家介绍。

绕过技术

大小写绕过:

如果遇到仅对or 以及AND 进行检测,因此我们可以使用OR 或者and 绕过。

双写绕过:

在某一些简单的waf中,将关键字select等只使用replace()函数置换为空,这时候可以使用双写关键字绕过。例如select变成seleselectct,在经过waf的处理之后又变成select,达到绕过的要求。

内联注释绕过:

内联注释就是把一些特有的仅在MYSQL上的语句放在 /*!...*/ 中,这样这些语句如果在其它数据库中是不会被执行,但在MYSQL中会执行。

注释符号绕过:

常用的注释符有:

特殊编码绕过:

十六进制绕过:

ascii编码绕过:

Test 等价于 CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)

Unicode 编码:

base64编码绕过:

如同上文中举例情况,如果程序中对传入参数进行了base64 加密处理,我们也可以利用其绕过检测,这里可以根据具体情况具体分析。

两次 URL 编码:

后端对请求参数额外做了一次 URL 解码,WAF 认为安全的参数经过解码后变得不安全 例如:

后端对 id 参数进行 URL 解码,得到0//UNION//SELECT

空格过滤绕过:

一般绕过空格过滤的方法有以下几种方法来取代空格:

与空格等价的字符:

特殊字符:

过滤or and xor not 绕过:

如果orandxornot 被过滤,我们可以采取如下方式绕过:

过滤等号=绕过:

不加通配符like执行的效果和=一致,所以可以用来绕过。

正常加上通配符的like

不加上通配符的like可以用来取代=

rlike:模糊匹配,只要字段的值中存在要查找的 部分 就会被选择出来 用来取代=时,rlike的用法和上面的like一样,没有通配符效果和=一样:

regexp:MySQL中使用 REGEXP 操作符来进行正则表达式匹配

使用大小于号来绕过

<> 等价于 != 所以在前面再加一个!结果就是等号了

等号绕过也可以使用strcmp(str1,str2)函数、between关键字等。

过滤大小于号绕过:

我们可以使用如下方法来绕过对< 以及> 的过滤:

greatest(n1, n2, n3…)返回n中的最大值:

同理我们还可以使用:least(n1,n2,n3…)返回n中的最小值。

strcmp(str1,str2)函数是string compare(字符串比较)的缩写,用于比较两个字符串并根据比较结果返回整数,若根据当前分类次序,第一个参数小于第二个,则返回 -1,其它情况返回 1。

in关键字

between a and b:范围在a-b之间:

我们可以利用这种特性来判断是不是相等的:

过滤引号绕过:

使用十六进制:

宽字节:

常用在web应用使用的字符集为GBK时,并且过滤了引号,上文我们已经分享过相关内容了。

过滤逗号绕过:

我们上文中许多函数都用到了, ,如果被绕过了,我们可以采用如下方式绕过:

from pos for len,其中pos代表从pos个开始读取len长度的子串:

也可使用join关键字来绕过:

其中union select * from (select 1)a join (select 2)b join(select 3)c 等价于union select 1,2,3

使用like关键字:

适用于substr()等提取子串的函数中的逗号:

使用offset关键字:

适用于limit中的逗号被过滤的情况 limit 2,1等价limit 1 offset 2

过滤函数绕过:

sleep() ,延时函数上文已经介绍,这里不再赘述。

ascii()hex()bin(),替代之后再使用对应的进制转string即可。

group_concat()concat_ws()

substr()substring()mid()可以相互取代, 取子串的函数还有left()right()

user() --> @@userdatadir–>@@datadir

ord()–>ascii() :这两个函数在处理英文时效果一样,但是处理中文等时不一致。

HTTP参数污染:

通过提供多个参数=相同名称的值集来混淆WAF。例如

在某些情况下(例如使用Apache/PHP),应用程序将仅解析最后(第二个) id= 而WAF只解析第一个。在应用程序看来这似乎是一个合法的请求,因此应用程序会接收并处理这些恶意输入。

HPP(HTTP Parameter Polution)

HPP又称做重复参数污染,最简单的就是?uid=1&uid=2&uid=3,对于这种情况,不同的Web服务器处理方式如下:

Web服务器

参数获取函数

获取到的参数

PHP/Apache

$_GET(“par”)

Last

JSP/Tomcat

Request.getParameter(“par”)

First

Perl(CGI)/Apache

Param(“par”)

First

Python/Apache

getvalue(“par”)

All (List)

ASP/IIS

Request.QueryString(“par”)

All (comma-delimited string)

HPF (HTTP Parameter Fragment)

这种方法是HTTP分割注入,同CRLF有相似之处(使用控制字符%0a、%0d等执行换行)

HPC (HTTP Parameter Contamination)

RFC2396定义了以下字符:

不同的Web服务器处理处理构造得特殊请求时有不同的逻辑:

以魔术字符%为例Asp/Asp.net会受到影响:

缓冲区溢出(Advanced):

缓冲区溢出用于对付WAF在内的软件本身有不少WAF是C语言写的而C语言自身没有缓冲区保护机制因此如果WAF在处理测试向量时超出了其缓冲区长度就会引发bug从而实现绕过。

示例0xA*10000xA后面A重复1000次一般来说对应用软件构成缓冲区溢出都需要较大的测试长度,这里只是举例,还需按具体情况分析。

整合绕过:

参考资料

  1. 《Web安全攻防 渗透测试实战指南》

  2. 《PHP WEB 安全开发实战》

  3. 《SQL注入攻击与防御第2版》

Last updated

Was this helpful?