Java反序列化漏洞的危害不光在于普通gadgets能够带来的命令执行,由于Java应用的使用场景以及gadgets大多都是构造出单向代码执行,一般通过利用链构造出的单向代码链能做到的能力往往有限。而我们在多数场景比如需要回显,注入内存shell等情况下,实际上对可以直接运行整个class或者说运行一个全局上下文关联的多行代码来进行动态执行有极大的需求。它往往是需要反序列化利用链配合另一条能够动态代码执行的利用链,这里我们主要讨论的也是这种情况。下面会简要分享下部分相对比较常见同时能够直接动态代码上下文执行的方式。

单向代码利用链的缺点

我们常见的著名Java反序列化gadgets比如CommonsCollections系列的TransformerChain。还有前段时间的weblogic coherence的ReflectionExtractor虽然大多数存在于oracle的产品中,但这个链的效果是和TransformerChain可以媲美的,非常有意思。这里具体原理也不过多讲述,在分析和使用过程中我们可以发现由于它们的执行是属于能返回对象并可以再进行对象调用的链式结构。所以我们中途无法对对应执行代码做额外的事情,这也是为什么成为链的原因,它的下一步执行方法是受上一步返回的对象限制的。
举个简单的例子,我们常用这类gadgets做反射调用执行对应的方法,首先它有一个条件就是我们能写出的正常代码就是链式结构,比如:

那假如对应类里面的getRuntime方法的描述符规定的访问权限不为public,我们就没法直接调用,这时候就需要获取到类后利用反射在中间执行一步AccessibleObject的setAccessible来取消默认Java语言访问控制检查的能力,但它并没有返回值,所以无法在链中使用。因此单向代码利用链是无法执行有关联上下文代码的。这种操作常见于对象内部属性的增删改,没有返回值意味着我们无法构造出有效的执行链。
还有就是如果对应一些方法如果想要传入指定类型的参数对象,它不光意味着存在上面提到的问题,如果是自己加工过的对象需要满足的条件就是该对象要支持反序列化。
除此以外在需要回显或者做一些额外事情的场景是极受限制的,因为我们不光需要一个能够执行的代码上下文,同时还期望获取到当前执行环境中的上下文,实际的关键就成了我们需要让当前执行环境下的classLoader加载我们需要运行的代码或者通过其它脚本引擎加载代码。
单向代码利用链的缺点也就是说我们为什么需要动态代码上下文执行?它实际上核心就是利用Java动态编译加载,做安全一般会常见于jsp shell的绕过中,当然这里因为有单向执行链的前提触发条件,所以没有jsp shell的姿势多,但是存在一些额外的配合其它类库的链。

动态代码上下文执行链

这里简要分享部分可以通过单向代码执行链执行动态代码的方式和一些小原理,也欢迎补充。这里不讨论需要多次执行,和有文件落地的情况。

ClassLoader

loadClassass

ClassLoader的相关知识这里不具体说了主要是类加载的相关知识可以自行查阅,实际上主要符合的常见可直接利用的ClassLoader也不算多,因为它最终运行的核心都需要触发loadClass,正常走双亲委派流程,是需要定义好parentClassLoader的,除非是启动类加载器或者专门去打破双亲委派的情况。所以在loadClass之前需要定义好当前的classLoader的实例才可以运行(即设定好parentClassLoader)。大部分第三方实现的ClassLoader都需要用户自行获取并传入自定义或上下文的classLoader对象,比如:

同时ClassLoader类也不支持序列化因此也无法传入,所以我们要找到直接可以调用的就需要满足它运行时自己获取了上下文的classLoader而不用我们传入。
比如:

  • org.mozilla.classfile.DefiningClassLoader
  • groovy.lang.GroovyClassLoader
  • com.sun.org.apache.bcel.internal.util.ClassLoader
    ……

defineClass

前面讲了利用ClassLoader的首个条件,对于ClassLoader来获取class,它必经之路就是loadClass方法。即使很多人说使用defineClass也可以构造一个class,但它实际上也会走次loadClass,我们可以简单过下原理。
首先加载类的话它会先尝试加载父类。当我们调用defineClass的时候,它会走到Native方法defineClass1
ClassLoader.c#Java_java_lang_ClassLoader_defineClass1() -> jvm.cpp#JVM_DefineClassWithSource() -> jvm.cpp#jvm_define_class_common()

这里主要是最后调用SystemDictionary::resolve_from_stream()将Class文件加载成内存中的Klass。

在其中会走进一次ClassFileParser::parseClassFile函数

我们可以看到它在其中调用了SystemDictionary::resolve_super_or_fail,这个函数会对父类进行解析,因为我们defineClass的类是继承java.lang.Object的,所以会根据多态用我们使用的类加载器调用loadClass来加载java.lang.Object。
也就是说肯定会出现父类(这里是java.lang.Object)进入到loadClass方法,而这时如果没有像前面说的设定上下文的classloader,直接去调用parentClassloader的loadClass就会报空指针,若继承的是当前classpath下的类则会NoClassDefFoundError。

DefiningClassLoader

继续回归主题,在loadClass之前我们一般需要先直接调用defineclass方法去加载byte[]来构造一个Class对象返回,而实际情况很少有public的defineClass,也即我们无法直接调用它的defineClass方法。
所以主要符合的类有两个条件,首先需要找到一个它运行初静态块或构造方法里,或defineClass这种会触发的方法,它们自己完成了获取了上下文的classLoader而不用我们传入,其次就是defineClass的访问权限为public,同时可以接收byte或可序列化的类型在后续进行构造。
最常见的就是org.mozilla.classfile.DefiningClassLoader

传入的byte数组即由class转换,在class中我们可以写入任意代码来达到目的

GroovyClassLoader

同上

也可以使用Groovy语法来执行上下文的多条代码,比如使用GroovyShell

BCEL ClassLoader

字节码操作相关类,这里举例BCEL(Byte Code Engineering Library),是Java classworking最广泛使用的一种框架。它在实际的JVM指令层次上进行操作(BCEL拥有丰富的JVM 指令级支持)而Javassist所强调的源代码级别的工作。
JDK1.6开始引入自带的BCEL,BCEL的ClassLoader对于高版本的JDK也是在实际中非常好利用的一种。需要注意的是BCEL loadClass时候会有classpath的限制。

加载一个报错class。

如果有继承非"java.", "javax.", "sun."包中的类需要额外设置,因为调用BCEL无参的构造方法之后loadclass会走SyntheticRepository的,所以会有classpath限制。直接传classloader对象也可以不过它不支持序列化,所以可以通过添加ignored_packages来绕过限制。

这样就可以支持加载继承了cn包开头的类。

URLClassLoader

比较常见了,主要的问题是需要出网,不出网的话需要先在本地写入文件,这里不讨论这种情况。
ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,实际上主要还原class还是通过defineClass。

这里可以嵌套URL对象的原因就是它符合了我们之前提到的支持序列化的条件,所以用gadget构造上面这条链是可以的。同时java.net.URLClassLoader#URLClassLoader(java.net.URL[])一直super调到了java.lang.ClassLoader里面的初始化所以拿到了systemClassloader。


对于不可直接利用的ClassLoader这里先不提,需要后面的其它方式配合。

TemplatesImpl

这个属于实际中用到的最多的了,因为它存在于rt.jar里,正常情况下都可以用到,大家应该也都很熟悉。具体可以参考ysoserial的createTemplatesImpl做简单的修改

将对应的执行代码编写到TestTranslet类的静态块中,初始化即可调用。
触发点可以调用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer,它会触发getTransletInstance()然后根据_bytecodes去defineClass并且newInstance初始化。

可以使用TrAXFilter来配合

JNDI

16年的BlackHat上@pwntester分享了通过JNDI注入进行RCE利用,现在也是比较常见的方法。需出网,有jdk版本限制,都懂不多说了。

ScriptEngine

jdk1.6开始就提供了动态脚本语言诸如JavaScript动态的支持,1.8开始使用新的Nashorn JavaScript引擎。Nashorn通过在JVM上,以原生方式运行动态的JavaScript代码来扩展Java的功能。实际使用过程中需要考虑下版本的问题,payload有些地方要改动。
它可以用js语法来执行操作,当然它可以使用一些方法来引入java类,由此来通过Nashorn来操作java运行情况。

因此可以来执行任意java上下文代的代码,如果想要直接加载java代码可以利用js语法调用java的classloader去defineClass加载,这时候就没什么前面说的限制了,因为我们可以使用反射了(没有使用SecurityManager或授权的情况下)。

Jython

Jython是用Java编写的,由于继承了Java和Python二者的特性而显得很独特,它是一种完整的语言,而不是一个Java翻译器或仅仅是一个Python编译器。Jython是一个Python语言在Java中的完全实现。在Java中实现Python可以看到有趣的Java反射API的作用。反射使Jython能无缝地使用任何Java类。

JRuby

JRuby是一个纯Java实现的Ruby解释器。通过JRuby,你可以在JVM上直接运行Ruby程序,调用Java的类库。

因为支持JSR223,所以我们也可以用ScriptEngineManager来调用,或BSF。当然Java还支持其它的脚本语言,这里不再举例。

EL

实际上就是我们常见的EL注入的位置,选择一些支持多行执行的EL表达式进行操作,这里举例MVEL,语法更趋近于Java。当然并不是所有类型EL都支持都上下文的代码段,有部分只支持一个完整的代码片段即一个表达式。

MLet

JMX的远程命令执行攻击手法,利用MLet加载并实例化我们指定的远程jar,需要出网。

案例

实际场景中大多数是用自带的BCEL ClassLoader或者TemplatesImpl。
比如说Weblogic过滤了TemplatesImpl

那需要绑定rmi实例回显的方式就使用了DefiningClassLoader,后来高版本官方去掉了这个包,大家又开始使用URLClassLoader,不过有个条件就是需要出网,当然也可以配合一条写文件的gadget,只不过需要多次请求,还会留下文件痕迹。
上面总结了这些,不出网加载的话Weblogic还是可以使用BCEL ClassLoader的方式加载字节码,BCEL使用加载的classpath有限制,可以通过上面提到的添加ignored_packages来绕过,添加一个weblogic即可,不过不支持<jdk1.6版本。也可以用ScriptEngine不过需要区别jdk版本,同时如果版本太低也不支持。同时又翻了下Weblogic的包,发现也可以用Jython。
所以用Jython的方式配合Coherence的gadget会比较通用,这里来写一下。
因为全用Jython语法调Java还是有些大坑的,适度避免一下,所以首先翻了下Weblogic使用的Jython版本的包内容,发现存在org.python.core.BytecodeLoader

其中makeClass比较舒服可以直接拿来用,不过存在的问题就是之前最开始提到的没有放入当前上下文的classLoader

所以用直接调用BytecodeLoader是不行的。
看了下调用流程发现使用PythonInterpreter实例化的时候会自动生成,所以正好配合之前Jython的调用上下文代码方法。大致流程就成了Coherence gadget -> PythonInterpreter exec -> BytecodeLoader makeClass

总结

不是什么新东西,算一个利用思路吧,好久没写东西了顺手总结一下。当然提到的并不是全部,也欢迎补充。还有很多单语句的执行方式比如JShell,BeanShell这里不在讨论范围。最开始也说了这种方式是配合存在单向代码执行链拓展威力的一种方法,它的核心其实就是利用Java提供的动态编译,其中也有一部分是用自己的脚本解析引擎。事实上不光是反序列化,这类动态加载方法还可以配合单行执行的各种问题,比如只支持一个表达式的EL注入等。