Java SQL 注入
Java 中的 SQL 注入代码审计
代码审计基于这个项目 https://github.com/JoyChou93/java-sec-code/
0x01 前言
出于一些个人原因,决定回过头来再过一遍 Java 基础的代码审计
也不说那么多没什么营养的话了,直接开始正文。
0x02 由于 jdbc 拼接不当导致的 SQL 注入
jdbc 存在两种方法执行 SQL 语句,分别为 PreparedStatement 和 Statement。
普通的 Statement 的语句一般会这样写:
1 | String checkUserQuery = "select userid from sql_challenge_users where userid = '" + username_reg + "'"; |
Statement 会直接拼接 SQL 语句,注意!,99.99999% 的 Statement 直接拼接,都会导致 SQL 注入。
相比 Statement,还有一种执行 SQL 语句的方式是 PreparedStatement,它会对 SQL 语句进行预编译,一般长这样。
1 | var statement = connection.prepareStatement("select password from sql_challenge_users where userid = ? and password = ?"); |
Statement SQL 注入
我们下个断点在 SQLI 这里,66 行,然后访问 /sqli/jdbc/vuln?username=123
,看一下发生了什么。
因为执行查询是我们的 SQL 语句,现在的 sql 变量的值是 ————select * from users where username = '123'
攻击方式的话,用万能密码攻击即可。
原理就不说了,太基础了,就是 SQL 语句拼接而已。
攻击的话:payload 如下
1 | username=123' or '1'='1 |
Statement SQL 注入的修复手段:预编译
后面 /jdbc/sec
接口,就是修好的代码,我们不妨也打断点看看它的工作流程。
payload 直接用 admin' or '1'='1
这个时候的 sql 语句始终如一都是 select * from users where username = ?
;那么预编译到底是怎么实现的呢?我们的 username 是何时放进去呗查询的呢?
不妨再深入一点跟进看一下。跟进 executeQuery()
进来之后,第 762 行,看着像是 sql 语句的查询,跟进去 getOriginalSql()
方法看看。
得到的结果是屁都没有,继续往下,768 行,是 executeInternal()
方法,一般 Internal
结尾的方法都是一些内部处理的方法,跟进。
这个 executeInternal()
方法主要做了两件事:第一件事:返回查询结果,第二件事:返回查询时间。如图
这些信息都包含在返回的 result 里面。
所以我们能够很清楚的看到,在预编译当中,输入和 SQL 语句是完完全全分开的
总结 JDBC 易产生漏洞点
未使用占位符
- PreparedStatement 只有在使用”?”作为占位符才能预防sql注入,直接拼接仍会存在sql注入漏洞
使用 in 语句
删除语句中可能会存在此类语句,由于无法确定delIds含有对象个数而直接拼接sql语句,造成sql注入。
1 | String sql = "delete from users where id in("+delIds+"); //存在sql注入 |
- 解决方法为遍历传入的 对象个数,使用“?”占位符。
使用 like 语句
- 使用like语句直接拼接会造成sql注入
1 | String sql = "select * from users where password like '%" + con + "%'"; //存在sql注入 |
%和_
- 没有手动过滤
%
预编译是不能处理这个符号的, 所以需要手动过滤,否则会造成慢查询,造成 dos。
Order by、from 等关键字无法预编译
通过上面对使用 in 关键字和 like 关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止 SQL 注入,然而事实并非如此,当使用 order by 语句时是无法使用预编译的,原因是 order by 子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,简单来说就是会报错。
然而使用 PreapareStatement 将会强制给参数加上’,所以,在使用 order by 语句时就必须得使用拼接的 Statement,所以就会造成 SQL 注入,这里其实算是 SQL 的原生问题了;
解决问题的话需要进行手动过滤。
1 | String sql = "Select * from news where title =?" + "order by '" + time + "' asc" |
0x03 Mybatis 下的 SQL 注入
我们的 SQL 语句一般是写在 Mapper 里面的,正常的应该是 Controller 层调 Service 层调 pojo 层,SQL 语句是写在 Mapper 文件里面的。所以如果是从代码审计的角度来看的话,我们可以直接来看 Mapper 层的代码。
mybatis 下的 SQL 注入
mybatis 下的 SQL 注入主要是这一种情况:${Parameter}
${Parameter}
有类似的一些 CMS 0day 漏洞,比如 RuoYi <= 4.6.1 的 SQL 注入漏洞。就是这样导致的
对应的这个项目当中,有问题的 SQL 语句如下
1 | //Mybatis |
这里是存在漏洞的,因为 ${username}
的方式就是直接拼接,和之前的 jdbc 是一样的。
payload 如下
1 | admin' or '1'='1 |
这里这道题目,怎么说呢;和我们平常见到的 mybatis 语句很不一样,我们平常的 SQL 语句都是写在 xxxMapper.xml 里面的,但是这个里面不是这样的。
测试一下
mybatis 下的 SQL 注入防护:预编译
- 这种 SQL 注入的防护 ———— 预编译,其实是这一种方式
#{Parameter}
代码如下
1 |
|
Mapper 层代码
1 | //Mybatis |
这样就防住了
MyBatis易产生SQL注入的三种情况
like 关键字的模糊查询
在这种情况下使用 #{}
程序会报错,新手程序员就把 #号
改成了 $
,这就照样导致了拼接的问题了。
- 源码如下
1 | <select id="findByUserNameVuln02" parameterType="String" resultMap="User"> |
正确写法如下:
1 | <select id="findByUserNamesec" parameterType="String" resultMap="User"> |
正确写法:
1 | mysql: |
使用 in 语句
使用in语句时直接使用 #{}
会报错,可能会存在使用 ${}
直接拼接,造成sql注入
这个项目里面并没有这一模块,所以需要自行添加,XML 和 接口编写如下
1 | <select id="findByUserNameVuln04" parameterType="String" resultMap="User"> |
1 | // http://localhost:8080/sqli/mybatis/vuln04?id=1)%20or%201=1%23 |
测试成功!
这里,当时 RuoYi 的漏洞就是这一种,in 语句使用 ${}
的拼接
正确用法为使用 foreach,而不是将#替换为$
这里前面的写法和 Vuln04 差不多,需要我们修改 userMapper.xml 的内容。mybatis 为了防止这种现象的 SQL 注入,
定义接口:
1 |
|
写个 Mapper
1 | List<User> findByIdSec04(; List id) |
接着是 UserMapper.xml
1 | <select id="findByIdSec04" parameterType="String" resultMap="User"> |
如此便可以成功防护啦!
原理上,会把每一个查找的字符都进行分割,我们正确的输入可以是 id=1,2,3,4;当然,这里就涉及到其他的逻辑问题了。
使用 order by 语句
和JDBC同理,使用 #{}
方式传参会导致order by语句失效,所以使用order by语句的时候还是需要做好过滤
在项目里面说的挺明白的,加了个 Filter,我这里就不再赘述了。
关于 mybatis 中的运行原理
https://www.cnblogs.com/cxuanBlog/p/12248536.html;
cy 一下,有空再过一遍。
0x04 Mybatis-Plus 的 SQL 注入探讨
- 为了方便各位师傅们学习,我这里自己简单写了一个项目。
本来是可以直接 fork joychou 大师傅的项目的,但是 mybatis 和 mybatis_plus 不太好兼容,所以就自己写一个小项目帮助师傅们理解
https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/OWASP%20TOP10
里面的 MybatisPluSqli module 就是对应的项目。
/mybatis_plus/test
是一个关于 mybatis-plus 的测试接口,若部署成功,访问如图
我们后续讲到的这些有问题的方法,都是基于用户可控输入的情况。
使用 apply 直接拼接 SQL 语句
理想的 apply 漏洞场景
我们可以先看一种纯拼接的手法,但是讲道理,实际开发里面不可能这么写的。
1 |
|
首先实际开发中不可能会用 selectList
这个方法,这完全就是自己给自己找坑。
我拿这个例子出来给师傅们看只是简单说明一下 SQL 注入里面的 apply 漏洞
实际情况的 apply 场景
代码如下
1 |
|
这是比较接近实际的情况,首先 apply()
方法算是一个多参请求,我们需要通过 id 与 name 来确定这个数据(虽然实际开发肯定不会告诉你 id)
这里我们的 payload 要这么打
1 | ?name=drunkbaby&id=1%20and%20extractvalue(1,concat(0x7e,(select%20database()),0x7e)) |
是只有报错注入才能打通的,如果我们只是简单的 1' or '1'='1
这种方式是不可以的。
因为这个地方
1 | Employee employee = employeeMapper.selectOne(wrapper); |
我们的结果是 selectOne()
,只是搞出一个,而万能密码是导出所有的数据,明显是会报错的,所以这里只能用报错注入爆数据。
虽然表面是 500,但是有时候这种方式是可行的,因为我们看到报错的地方爆出了数据库名,比如在服务器对 500 报错,是打印错误消息的情况下,就可以成功进行 SQL 注入了!
关于 apply 场景的防护
- 用预编译来;我们修对应
/Vuln02
的洞
1 |
|
很简单,要 apply 的地方加上 {0}
即可。
防护成功了,对应的 SQL 语句其实是这样的
last 方法产生的 SQL 注入
last()
方法经过重写,有两个方法,如下
1 | last(String lastSql) |
也就是说,在 lastSql 里面我们是可以直接写 SQL 语句的,编写一个新的接口
1 |
|
从根本上来说,也是拼接产生的问题
我们用 payload 打:
1 | ?id=1%20or%201=1 |
exists/notExists 拼接产生的SQL 注入
1 | exists(String existsSql) |
一共是这四个方法
原理也比较简单,后续的内容如果没有特别,就不再赘述了
exists 的接口
1 |
|
NotExists 的接口
1 |
|
having 语句
1 | having(String sqlHaving, Object... params) |
接口如下
1 |
|
order by 语句
- orderBy
1 | orderBy(boolean condition, boolean isAsc, R... columns) |
- orderByAsc
1 | orderByAsc(R... columns) |
- orderByDesc
1 | orderByDesc(R... columns) |
这和之前说的 mybatis 里面的问题是一样的,因为 order by 的时候不能预编译,所以会出现问题
三个接口的写法大同小异,如下
1 | public List<Employee> orderby01( String id) { |
group By
1 | groupBy(R... columns) |
与 order by
的原理是一样的
接口如下
1 |
|
inSql/notInSql
1 | inSql(R column, String inValue) |
对应的两个接口
inSql 的接口
1 |
|
notInSql 的接口
1 |
|
关于 Wrapper 自定义 SQL
基本和上面的一样,就不再赘述了。
分页插件的 SQL 注入情况
漏洞点比较之前少很多,主要集中于两个地方。
一个是分页插件自带的 addOrder()
方法,另外一个是之前就有问题的 order by
方法。
- 配置分页插件
1 | package com.drunkbaby.config; |
这里我新建了一个 Person 相关的 ORM 操作,这样的话我们后续的操作就不会存在干扰,至于 pojo 那些怎么做,我这里就不赘述了。直接看存在漏洞的接口。
addOrder()
1 |
|
这里的 Page<Person> personPage = new Page<>(1,2);
的参数由自己定义
这里对应的 payload 其实,比较有讲究:
1 | ?id=1%20and%20extractvalue(1,concat(0x7e,(select%20database()),0x7e))) |
必须是通过盲注的形式,如果是普通的注入,是不会有回显的;因为这里分页查找,size 就把你的数据数量限定死了,如果超过这个数据就会报错,所以只能盲注。
pagehelper
这里的原理就和 order by 一样,不赘述了
因为Order by排序时不能进行预编译处理,所以在使用插件时需要额外注意如下function,同样会存在SQL注入风险:
- com.github.pagehelper.Page
- 主要是setOrderBy(java.lang.String)方法
- com.github.pagehelper.page.PageMethod
- 主要是startPage(int,int,java.lang.String)方法
- com.github.pagehelper.PageHelper
- 主要是startPage(int,int,java.lang.String)方法
mybatis Plus SQL 注入的修复
- 看了很多资料,基本上没有太好的防御手段,只有写 Filter 比较靠谱,我这里写了一个集成各种 Filter 的,比如 XSS。SQL 注入等漏洞的过滤器。
https://github.com/Drun1baby/JavaSecFilters
0x05 Hibernate 框架下的 SQL 注入
Hibernate 是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。
Hibernate 可以使用 hql 来执行 SQL 语句,也可以直接执行 SQL 语句,无论是哪种方式都有可能导致 SQL 注入
HQL
hql 语句就和 PHP 里面的语句非常相似,和 JDBC 的也非常相似,所以这个理解起来比较简单,主要是防护措施。
1 | String hql = "from People where username = '" + username + "' and password = '" + password + "'"; |
这种拼接方式存在 SQL 注入
正确使用以下几种 HQL 参数绑定的方式可以有效避免注入的产生:
1.命名参数(named parameter)
1 | Query<User> query = session.createQuery("from users name = ?1", User.class); |
2.位置参数(Positional parameter)
1 | String parameter = "g1ts"; |
3.命名参数列表(named parameter list)
1 | List<String> names = Arrays.asList("g1ts", "g2ts"); |
4.类实例(JavaBean)
1 | user1.setName("g1ts"); |
5.HQL拼接方法
这种方式是最常用,而且容易忽视且容易被注入的,通常做法就是对参数的特殊字符进行过滤,推荐大家使用 Spring工具包的StringEscapeUtils.escapeSql()方法对参数进行过滤:
1 | import org.apache.commons.lang.StringEscapeUtils; |
SQL
Hibernate支持使用原生SQL语句执行,所以其风险和JDBC是一致的,直接使用拼接的方法时会导致SQL注入
语句如下:
1 | Query<People> query = session.createNativeQuery("select * from user where username = '" + username + "' and password = '" + password + "'"); |
正确写法:
1 | String parameter = "g1ts"; |
0x06 参考资料
- 本文标题:Java OWASP 中的 SQL 注入代码审计
- 创建时间:2022-09-14 14:21:35
- 本文链接:2022/09/14/Java-OWASP-中的-SQL-注入代码审计/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!