8

SQL注入漏洞是如何被利用的

 3 years ago
source link: https://zhuanlan.zhihu.com/p/342481037
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SQL注入漏洞是如何被利用的

公众号「古时的风筝」

标题有点臭不要脸,有标题党之嫌了,没有黑,只是网站安全性做的太差,我一个初学者随便就搞到了管理员权限。

事情是这样子的,在10年以前,某个月黑风高夜的夜里,虽然这么说有点暴露年龄了,但无所谓,毕竟我也才18而已。我打开电脑,在浏览器中输入我们高中学校的网址,页面很熟悉,很简陋,也没什么设计感,不过学校的网站从来都是这种风格,直到今天依然是这样。

这次访问与以往有些不同,因为我的目的很明确。作为学校的一员,理应为学校做些贡献的,为学校官网找些安全漏洞也算贡献的一种吧(强烈的求生欲)。

v2-441508632336b94b347e6b6cb27df8a9_720w.jpg

之所以选择学校的官网,一来是因为熟悉,先从熟悉的东西下手,一定错不了。二来是因为之前使用网站的时候碰过到一些异常的页面,直接就是异常堆栈直接抛出来。正好那段时间对网络安全比较有兴趣,在研究SQL 注入的时候正好想到在学校官网上碰到的问题好像就存在 SQL 注入的风险。于是,顺理成章,我的第一个目标就锁定了学校官网。

问题就出在一个大概这样的搜索页面,真正的网站已经改版过好多次了,以前的页面找不到了。

v2-750998bb7945a9050461039d349d08ba_720w.jpg

当时我在上面随便输入了一些内容,里面含有单引号,然后一点击搜索,页面就直接出现了异常提示,类似于下面这样子的。别惊讶,当时很多网站都是这样的,异常是直接抛出来的,别说以前了,现在也不少。

v2-60d23379444190f6e93b03ee729251f4_720w.jpg

于是我用那时候刚学会的皮毛知识,加上两个好用的工具,轻松拿到了数据库的数据,其中就包括了管理员的账号、密码,密码还是明文的,你说气人不。 然后通过后台管理员登录页面进入了管理员后台,当然了,只是进去看了看,什么都没碰,而且后台也没什么重要数据,顶多就是一些通知、新闻等数据。

我是怎么做到的呢,不说具体细节了,也确实没什么技术含量,而且时间太长也记不清了,后面就说一下 SQL 注入的原理和具体操作。

什么是 SQL 注入

SQL注入,是发生于应用程序与数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的SQL指令而运行,因此遭到破坏或是入侵。

SQL 注入一般发生在用户交互场景中,比如需要用户自已输入信息的输入框,或者下拉选择选项的这种,如果不做好输入内容的过滤,就很可能发生 SQL 注入。

就拿这个登录界面来说,用户名和密码都是你要输入的内容,点击登录按钮之后,会把你输入的值传递到服务端,服务端再到数据库进行查询。

假设后端的查询语句是这样的,不要在乎这是什么语法,只是举个例子。

String sql = "select * from `user` where  account={account} and password={password}";

正常的情况,比如 account 输入的是一个电话号码 13001980988,密码是 123456,那拼接出的 SQL 语句就是

String sql = "select * from `user` where  account='13001980988' and password='123456'";

然后,服务端通过数据库连接执行这条语句:

select * from `user` where  account='13001980988' and password='123456';

最后,数据库正常返回符合条件的记录,代码中再根据结果进行判断,执行后面的逻辑。

不正常的输入

SQL 注入就是通过不正常的输入来获取程序开发者意料之外的结果。

什么是不正常的输入呢?

比如我在用户名输入框中输入的内容是这样子的 :13001980988' or 1=1 --,密码输入框随便输入什么都无所谓,然后点击登录,传输给后端,后端拼接出来的结果就是这样的:

String sql = "select * from `user` where  account='13001980988' or 1=1 --'  and password='123456'";

然后,服务端通过数据库连接会执行下面这条语句:

select * from `user` where  account='13001980988' or 1=1 --'  and password='123456'

以 MySQL 为例, -- 是 MySQL 中的注释符号,上面的语句中 --后面的相当于是注释内容了,所以最后实际执行的 SQL 语句是这样的:

select * from `user` where  account='13001980988' or 1=1 

于是,SQL 注入就这么发生了,显然有了 or 1=1 这个条件,表中所有的记录都符合条件。如果在用户名输入框中输入的是 adminadministrator 等已知的后台管理员账号,那就可以用管理员账号直接登录系统了。

上面就是 SQL 注入的基本原理。

SQL 注入遍地都是的年代

在9、10年前,也就是在我小时候(对,这个词好,小时候)。那时候智能手机才刚刚出来,塞班系统还很贵,根本就买不起。用着功能机,30M的流量能用坚持一个月,聊天只靠 QQ 和 短信,微信才刚要问世,更别提什么 APP 了,根本就没有。那时候,PC Web 才是根正苗红的网络主宰,如果说要在网上干点儿什么的话,那必须要有一个配套的网站才可以。

互联网还没有发展的这么成熟,用的技术也比较原始,绝大多数的网站是用 PHP 写的,还有很多用 ASP 。可能有些同学都不知道 ASP 是什么,它虽然也是微软的,但是却不是 http://ASP.NET。数据库很多用的是 MySQL ,还有一部分用的是更原始的 Access,可能又触到某些同学的盲区了,这不怪你没见识,只怪你太年轻。

一些小公司啊、学校啊、政府部门网站啊、各种论坛啊等等,各种五花八门的网站。不像现在这样,无论你用 PHP、Java 还是 Python,都有很多成熟的开发框架供你选择,成熟的框架必然会减少漏洞和降低被攻击的风险。但那时候没有这么多框架供选择,就比如很多学校会选择用 ASP + Access 组合的架构来开发自己的学校官网、教务管理系统等,功能上比较简单,但是全靠手工去写,就说 SQL 查询吧,从建立数据库连接到拼接 SQL 语句,再到执行查询处理查询结果,全都要自己实现,并没有什么 ORM 框架、数据库连接池供选择,由此就带来了 SQL 注入的风险。

而且建网站,如果不想开发的话,有很多 CMS 框架,尤其 PHP 的很多,现在依旧使用广泛的有 WordPress,当时国内的有 Discuz、DEDECMS 等一批傻瓜建站的 CMS 系统,由于代码都是开源的,而且 WordPress 还支持插件,所以会有很多相关的漏洞爆出来,尤其在多年以前,有了漏洞,想拿下一个网站真是太容易了,即使漏洞已经公布并有了解决方案,但依然有好多网站不及时修补和升级。现在在 Google 中搜索相关的 SQL注入关键词,有很多相关介绍。

说了这么多,这不都是 PHP 的代码吗?嘘,只是碰巧而已,说明 PHP 市场大呀,毕竟PHP是最好的开发语言。那 Java 中就没有了吗,当然有啊。

MyBatis 中的 SQL注入风险

最近在看一些代码,Spring Boot + MyBatis 的,偶然发现一个模糊查询的方法的 SQL 语句中用到了 like '%${keyword}%'这样的查询条件,这一看就有 SQL 注入漏洞。 大家可能都了解,MyBatis 是可以解决 SQL 注入的问题的。一般我们在使用 MyBatis 的时候都会把 SQL 语句单独的放到 xml 文件中,在 SQL 语句中支持两种格式的参数占位符,一种是 #{parameter},另一种是 ${parameter},在这两种参数占位符中,#{parameter}是安全的,不存在SQL注入漏洞,而 ${parameter}是存在 SQL 注入漏洞的。

安全的占位符格式

#{parameter} 这种占位符会在 MySQL中进行预编译,所以你观察到 MyBatis 打印出来的日志是这样的:

select * from `user` where  account=? and password=?

其实在框架底层,是 JDBC 中的 PreparedStatement 类在起作用,PreparedStatement 是我们很熟悉的 Statement 的子类,它的对象包含了编译好的SQL语句。这种预编译的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。

不知道你有没有写过直接用 JDBC 操作数据库的代码,反正大学老师就告诉我们要用占位符去做数据库查询,而不是拼接 SQL 字符串,因为用占位符的方式安全。其实,MyBatis 的预编译模式的底层实现就可以理解为下面这样的。

Connection conn = getConn();//获得连接
String sql = "select * from `user` where  account=? and password=?"; //执行sql前会预编译号该条语句
PreparedStatement pstmt = conn.prepareStatement(sql); 
pstmt.setString(1, "13001980988");
pstmt.setString(2, "123456");
ResultSet rs=pstmt.executeUpdate(); 

不安全的占位符格式

${parameter} 这种格式是不进行预编译的,也就相当于字符串的拼接,存在 SQL注入的问题,如果非要用这种方式,那需要在程序中对参数进行安全性校验。强烈建议在使用 MyBatis 的过程中不要使用 ${parameter}这种格式的占位符,而要使用 #{parameter}这种格式。

来一波SQL注入

我模拟了一个 SQL 注入的场景,很简单,就是一个模糊查询的接口,根据用户输入的关键词查询。

数据库中有两张表,分别为用户表(user)和 新闻表(news)。

NewsMapper 类:

public interface NewsMapper {
    List<News> selectNewsLikeTitle(@Param("keyword") String keyword);
}

实际的 SQL 语句,注意,正是用到了 ${keyword},才有了 SQL 注入的问题。

<select id="selectNewsLikeTitle"  resultType="org.kite.purely.mybatis.entity.News">
    select * from news where title like '%${keyword}%';
</select>

然后我写了一个控制台,接收输入的参数,传给 selectNewsLikeTitle,以便来尝试 SQL 注入。

代码已上传至 github,链接放在了文末,需要的同学自取

正常的输入

新闻表就三条数据,我以 Docker 作为关键词,正常情况下,应该是这样的返回结果:

蓝色是我输入的关键词,后面跟着查询语句。一个标准的模糊查询语句,最后输出的结果也没问题。

select * from news where title like '%Docker%'; 

SQL 注入

如果使用者都这么守规矩就好了,但是真实情况往往并不是这样的,有些人就是喜欢躲在阴暗的角落放着冷箭,大部分情况下是有利可图,极少部分干脆就只是为了满足变态心理。

1、查询所有记录

有同学已经看出来了,输入空值不就是查询所有吗?对的,没错,但真实情况下,前端或者 Controller、Service 层会做拦截,不允许查询所有。

正常逻辑不允许,但是 SQL 注入就可以。我输入下面这样的条件参数,看看会出现什么结果呢?

Docker' or 1=1 or 1='

结果出来了,三条数据全部查询出来了。

因为构造的 SQL 语句已经完全变味儿了,SQL 语句是这样的,由于条件 or 1=1 的加持,导致任何记录都符合条件。

select * from news where title like '%Docker' or 1=1 or 1='%';

通过添加'来保证条件中字符串前后单引号的闭合。

还可以是这样的条件

Docker' or 1=1 -- 
或者
Docker' or 1=1 #

因为--#都是 MySQL 中的注释符号,用它们来注释掉关键注入后面的部分,最后构造出来的 SQL 语句是:

select * from news where title like '%Docker' or 1=1 -- %';
或
select * from news where title like '%Docker' or 1=1 #%';

所以,最后有效的部分就是注释符号前面的部分,自然,查询出来的就是所有的记录。

select * from news where title like '%Docker' or 1=1

这种情况,其实保密数据没有什么泄漏,但是,它可能会拖垮数据库,抛开 Redis 缓存什么的不谈,假设仅有 MySQL 这一层,假设数据库中有几万条、几十万条数据,黑客不断制造这样的模糊查询,你的数据库服务马上就会挂掉。

2、联查其他表,危险行为

把数据库拖垮已经很不爽了,但是更严重的,是获取数据。

我想要通过这条查询语句把 user表的数据也套出来,你看着是不是就有点儿意思了。怎么办呢,通过 union就可以。

前提是我已经知道有 user 表的存在了,别问怎么知道的,反正是已经知道了,而且黑客有很多办法能猜到。

我构造这样的参数:

Docker' union select * from `user` -- 注释掉后面的内容

执行一下,出现这样的提示:

构造出来的 SQL 没有问题,就是我们想要的。

select * from news where title like '%Docker' union select * from `user` -- 注释掉后面的内容%';

但是这用户体验很好,给出了具体的异常。体验好是对于攻击者而言的,如果每次异常都把原始异常信息抛出,那能给攻击者省不少事儿,就像下面这个异常。

Cause: java.sql.SQLException: The used SELECT statements have a different number of columns

这是因为 news 表和 user 表的列数不一致导致的,前后列数不一致,那这时候怎么办呢?

构造出下面这样的查询语句可以试探出 news 表的列数,其中 select 1,2 from user中的 1,2表示假设 news 表有两列,可以从 1 到 n,当尝试到哪一个而不出错或者正常返回的时候,表示 news 表就有多少列了。

select * from news where title like '%Docker' union select 1,2 from `user`;

要构造这样的语句,需要输入的参数是:

Docker' union select 1,2 from user # 

因为 news 表只有两列,所以上面的参数可以成功执行。

盲注
大多数网站都不会将异常信息直接返回的,当攻击者拿不到即时的异常反馈时,就像是合着眼睛去猜,这种情况就叫做盲注。盲注是需要极大的耐心、高超的技术以及丰富的经验的,所以说黑客真不是好当的。

当试探出 news 表的列数后,再去配合它筛选 user 表的列就可以了,user 表的列名其实也是靠各种猜测的,比如常规的命名 name、account、phone、mobile、password 等,或者根据你返回给前端的属性对应着猜,比如返回的用户名是 userName,那你数据库中的字段就很有可能是 user_name。

假设我猜 user 表有 phone 这个字段,那我构造的参数就可以是下面这样的:

Docker' union all select 1,phone from user # 注释掉后面的内容 ,来确定news表有两个字段

最后执行下来,结果是这样的,四条 user 记录全出来了。

或者我还确定 user 表中有 password 列,那就可以直接把 password 再取出来了,如果 password 再是明文的,那就热闹了。

有了用户名和密码,是不是就有点危险了。

3、更危险的高权限

还有更危险的呢,假设你程序中数据库连接用到的账号是高权限的,比如 root 账号,有好多中小应用都这么用,别惊讶。

发散思维想一想,这时候能干嘛?

由于权限够高,那意味着可以执行任何合法的 SQL 语句了。在 MySQL 中有数据库 information_schema,它存储着当前数据库实例中的很多重要信息,而且其中的表结构都是公开透明的,那这样一来呢,我们就可以通过这个数据库掌握当前数据库服务的几乎所有内容了。

不仅如此,高权限用户还能通过 MySQL 的读写文件功能实现更多的功能,比如配合 webshell 上传木马,获取服务器的控制权,从而实现脱裤(拖库)。

当然了,说的轻松,实现起来就困难了,不过只要有漏洞,就会被利用。

一些工具

俗话说,工欲善其事,必先利其器。漏洞哪儿那么容易挖,很多有价值的漏洞确实是厉害的黑客手工挖出来的。

为了方便的挖掘常见漏洞及利用漏洞,有很多网络安全专家开放出来的工具,可以让我等小白简单上手。比如我上学时候用到的「啊D注入工具」。可以用来扫码注入点、SQL注入检测、管理入口检测等。

还有比较专业的 SQL 注入工具「SQLMap」,它是一款命令行工具。SQLMap 提供了丰富的命令来帮我们发现漏洞、利用漏洞。

另外,如果你想学习 SQL 注入的一些基础,可以直接整个靶场来玩玩儿。比如这个 Pikachu 就不错。

https://github.com/zhuifengshaonianhanlu/pikachu

总结

本文并不是为了教各位如何完成 SQL注入,毕竟,我也没这个实力。当然,制造漏洞的实力还是有的。

只是想说,在写代码的时候一定要注意,稍有不慎就可能写出有漏洞的 SQL,所以,尽量用成熟框架的标准写法,不要图省事自己拼 SQL。

不光是 SQL 这部分,其他的涉及到用户交互的地方都要注入,比如用户表单,有时候可能产生 xss 漏洞,还有文件上传的部分,别用户上传了木马都不知道怎么回事。

愿你的代码没有 bug。虽然这是不可能的。

文中的测试代码已放至仓库:https://github.com/huzhicheng/SQL_Injection,有需要的同学自取。

絮叨

大家好,我是风筝,一个坚持原创的程序员,如果觉得文章对你有帮助,欢迎分享给你的朋友,这对我非常重要,谢谢你们,我们下次见!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK