10年前,我就用 SQL注入漏洞黑了学校网站

Hollis

共 9070字,需浏览 19分钟

 ·

2021-04-06 20:24

10年前,我就用 SQL注入漏洞黑了学校网站

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

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

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

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

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

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

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

我是怎么做到的呢,不说具体细节了,也确实没什么技术含量,而且时间太长也记不清了,后面就说一下 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 是什么,它虽然也是微软的,但是却不是 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 注入。

正常的输入

新闻表就三条数据,我以 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。虽然这是不可能的。


有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号


好文章,我在看❤️

浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报