当前位置 博文首页 > 10年前,我就用 SQL注入漏洞黑了学校网站

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

    作者:风的姿态 时间:2021-01-07 21:01

    我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农!
    文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。

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

    事情是这样子的,在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   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 提供了丰富的命令来帮我们发现漏洞、利用漏洞。

    img

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

    https://github.com/zhuifengshaonianhanlu/pikachu

    总结

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

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

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

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

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


    这位英俊潇洒的少年,如果觉得还不错的话,给个推荐可好!

    公众号「古时的风筝」,Java 开发者,全栈工程师,bug 杀手,擅长解决问题。
    一个兼具深度与广度的程序员鼓励师,本打算写诗却写起了代码的田园码农!坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!

    下一篇:没有了