Modify ysoserial jar serialVersionUID

简介

Java在反序列时, 会把传来的字节流中的serialVersionUID与本地对应类的serialVersionUID进行校验, 在两个SUID不同的情况下, 会抛出版本号不同的异常, 不再进行反序列。

1
2
3
4
5
6
7
8
9
if (model.serializable == osc.serializable &&
!cl.isArray() &&
suid != osc.getSerialVersionUID()) {
throw new InvalidClassException(osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
osc.getSerialVersionUID());
}

之前做JEECMS的反序列的时候, 解决C3P0 SUID不同的方法是直接通过修改Ysoeriali C3P0 JAR包的版本与目标环境的JAR包版本一致使SUID一致。
最近闲得想试试通过反射来修改Ysoeriali JAR包的SUID来使SUID一致进行反序列。

类未定义serialVersionUID属性

测试Jar包 服务端 C3P0 0.9.1.1、ysoserial C3P0 0.9.5.2,
-w927
提示本地jar包的SUID为7387108436934414104
而字节流的SUID为-2440162180985815128, SUID不一致爆出异常。

在com.mchange.v2.c3p0.PoolBackedDataSource类中,

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class PoolBackedDataSource extends AbstractPoolBackedDataSource implements PooledDataSource {
public PoolBackedDataSource(boolean autoregister) {
super(autoregister);
}

public PoolBackedDataSource() {
this(true);
}

public PoolBackedDataSource(String configName) {
super(configName);
}
}

未定义serialVersionUID属性。

如果序列化的类里没有显示定义serialVersionUID属性, 那么会通过computeDefaultSUID方法计算得出SUID。

1
2
3
4
5
6
7
8
9
10
11
12
13
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}

computeDefaultSUID的大概实现就是通过反射获取到反序列类的成员属性,方法,实现接口等以及它们的修饰符输出到流中, 最后SHA HASH生成SUID。
在这里计算SUID的时候 没有用到成员属性的值以及方法的具体实现, 所以如果修改了成员属性的值和方法的实现是不存在影响的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
private static long computeDefaultSUID(Class<?> cl) {
if (!Serializable.class.isAssignableFrom(cl) || Proxy.isProxyClass(cl))
{
return 0L;
}

try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

dout.writeUTF(cl.getName());

int classMods = cl.getModifiers() &
(Modifier.PUBLIC | Modifier.FINAL |
Modifier.INTERFACE | Modifier.ABSTRACT);

/*
* compensate for javac bug in which ABSTRACT bit was set for an
* interface only if the interface declared methods
*/
Method[] methods = cl.getDeclaredMethods();
if ((classMods & Modifier.INTERFACE) != 0) {
classMods = (methods.length > 0) ?
(classMods | Modifier.ABSTRACT) :
(classMods & ~Modifier.ABSTRACT);
}
dout.writeInt(classMods);

if (!cl.isArray()) {
/*
* compensate for change in 1.2FCS in which
* Class.getInterfaces() was modified to return Cloneable and
* Serializable for array classes.
*/
Class<?>[] interfaces = cl.getInterfaces();
String[] ifaceNames = new String[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
ifaceNames[i] = interfaces[i].getName();
}
Arrays.sort(ifaceNames);
for (int i = 0; i < ifaceNames.length; i++) {
dout.writeUTF(ifaceNames[i]);
}
}

Field[] fields = cl.getDeclaredFields();
MemberSignature[] fieldSigs = new MemberSignature[fields.length];
for (int i = 0; i < fields.length; i++) {
fieldSigs[i] = new MemberSignature(fields[i]);
}
Arrays.sort(fieldSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.name.compareTo(ms2.name);
}
});
for (int i = 0; i < fieldSigs.length; i++) {
MemberSignature sig = fieldSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE |
Modifier.TRANSIENT);
if (((mods & Modifier.PRIVATE) == 0) ||
((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0))
{
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature);
}
}

if (hasStaticInitializer(cl)) {
dout.writeUTF("<clinit>");
dout.writeInt(Modifier.STATIC);
dout.writeUTF("()V");
}

Constructor<?>[] cons = cl.getDeclaredConstructors();
MemberSignature[] consSigs = new MemberSignature[cons.length];
for (int i = 0; i < cons.length; i++) {
consSigs[i] = new MemberSignature(cons[i]);
}
Arrays.sort(consSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.signature.compareTo(ms2.signature);
}
});
for (int i = 0; i < consSigs.length; i++) {
MemberSignature sig = consSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF("<init>");
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}

MemberSignature[] methSigs = new MemberSignature[methods.length];
for (int i = 0; i < methods.length; i++) {
methSigs[i] = new MemberSignature(methods[i]);
}
Arrays.sort(methSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
int comp = ms1.name.compareTo(ms2.name);
if (comp == 0) {
comp = ms1.signature.compareTo(ms2.signature);
}
return comp;
}
});
for (int i = 0; i < methSigs.length; i++) {
MemberSignature sig = methSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}

dout.flush();

MessageDigest md = MessageDigest.getInstance("SHA");
byte[] hashBytes = md.digest(bout.toByteArray());
long hash = 0;
for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
hash = (hash << 8) | (hashBytes[i] & 0xFF);
}
return hash;
} catch (IOException ex) {
throw new InternalError(ex);
} catch (NoSuchAlgorithmException ex) {
throw new SecurityException(ex.getMessage());
}
}

这里来对比一下两个版本C3P0的com.mchange.v2.c3p0.PoolBackedDataSource

0.9.5.2版本,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.mchange.v2.c3p0;

import com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource;

public final class PoolBackedDataSource extends AbstractPoolBackedDataSource implements PooledDataSource {
public PoolBackedDataSource(boolean autoregister) {
super(autoregister);
}

public PoolBackedDataSource() {
this(true);
}

public PoolBackedDataSource(String configName) {
this();
this.initializeNamedConfig(configName, false);
}

public String toString(boolean show_config) {
return this.toString();
}
}

0.9.1.1版本,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mchange.v2.c3p0;

import com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource;

public final class PoolBackedDataSource extends AbstractPoolBackedDataSource implements PooledDataSource {
public PoolBackedDataSource(boolean autoregister) {
super(autoregister);
}

public PoolBackedDataSource() {
this(true);
}

public PoolBackedDataSource(String configName) {
super(configName);
}
}

两个版本的com.mchange.v2.c3p0.PoolBackedDataSource类中都没有定义SUID, 所以通过computeDefaultSUID来得出SUID, 而且可以明显的看出 在高版本的C3P0当中多了一个toString方法, 必然两个版本经过computeDefaultSUID得到的SUID不同。

对于这种没有显示定义SUID的场景, 大概想了几种方法。

1 反射

尝试通过反射添加SUID属性, 然后再修改属性值为7387108436934414104。
但是翻了下文档, 没看到反射动态添加属性这个操作, 就只有放弃了。

2 Hook

Hook computeDefaultSUID方法, 如果传入的类是com.mchange.v2.c3p0.PoolBackedDataSource, 直接修改返回值为7387108436934414104。
但是找了一下 都没找到个合适的能hook class的框架,
就直接用idea来”hook”了。
在computeDefaultSUID里下个断点,

-w971
把hash修改为7387108436934414104
-w602

就不会在出现SUID异常了。
-w1075

3 修改字节码

直接使用javassist修改com.mchange.v2.c3p0.PoolBackedDataSource的字节码, 给它添加上一个值为7387108436934414104的SUID属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
ClassPool pool = ClassPool.getDefault();
try {
CtClass cls = pool.get("com.mchange.v2.c3p0.PoolBackedDataSource");
CtField field = CtField.make("private static final long serialVersionUID = 7387108436934414104;",cls);
cls.addField(field);
cls.writeFile();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

修改字节码后, 重新打包jar包 加载到ysoserial当中,
-w849
serialVersionUID已经定义上。
再次生成payload,
-w1251
不再出现SUID异常。
-w1131

类显式定义serialVersionUID属性

如果序列化的类显示定义了serialVersionUID, 只是值不同造成的异常解决起来就比较简单了, 直接通过反射修改该属性值即可。
由于每一个SUID属性的修饰符都是private static final,数据类型为long。
final修饰的属性没法通过反射直接修改属性值, 所以需要先通过反射修改SUID的修饰符 把final修饰符给去掉。
去掉final之后, 再修改SUID的属性值, 最后再把final修饰符重新添加回去即可。
假设(这是我自己改代码造的场景了)

-w1109
YSO的C3P0 Jar包 SUID为7387108436934414104, 打反序列时提示
-w869
所以此时要把yso里的SUID从7387108436934414104修改为-2440162180985815128.
因为这时yso C3P0包存在SUID只是值不同而已, 所以直接利用反射来修改。
在生成payload之前把suid改掉,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try {
Class clazz = Class.forName("com.mchange.v2.c3p0.PoolBackedDataSource");
Object obj = clazz.newInstance();

Field field = clazz.getDeclaredField("serialVersionUID");
field.setAccessible(true);

Field modifersField = Field.class.getDeclaredField("modifiers");
modifersField.setAccessible(true);
modifersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.setLong(obj,-2440162180985815128L);
modifersField.setInt(field, field.getModifiers() & Modifier.FINAL);

} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}

-w932

-w1064

总结

我TM真是闲, 还是直接修改版本方便多了。。。