Groovy1反序列化|ysoserial学习(四)

Groovy1反序列化|ysoserial学习(四)

文章导读

今天对yso的分析选择了Groovy1这条链子, 总的来说对了解对象代理和Groovy对闭包的处理逻辑还是又不少的好处的, 快来一起学习吧

Groovy1-Gadget

/*
    Gadget chain:
        ObjectInputStream.readObject()
            PriorityQueue.readObject()
                Comparator.compare() (Proxy)
                    ConvertedClosure.invoke()
                        MethodClosure.call()
                            ...
                                Method.invoke()
                                    Runtime.exec()

    Requires:
        groovy
 */

ysoserial源码

public class Groovy1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

    public InvocationHandler getObject(final String command) throws Exception {
        final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet");

        final Map map = Gadgets.createProxy(closure, Map.class);

        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map);

        return handler;
    }

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(Groovy1.class, args);
    }
}

这里最后生产的对象是一个InvocationHandler接口类型,我们在序列化前打下断点可以看到最后生成的是一个AnnotationInvocationHandler实现类,

image-20220911125658190

至于AnnotationInvocationHandler这个类是用来干什么的呢?

如果跟过CC3的话对这个类肯定就不陌生了,其实这就是一个用于注释的动态代理实现的调用处理程序,在CC3中它的作用是通过memberTypes.get(name)触发调用 LazyMap.get,而在这里则是通过它上面的streamVals.entrySet()触发调用org.codehaus.groovy.runtime.ConversionHandler#invoke

image-20220911181539544

简单了解一下AnnotationInvocationHandler, InvocationHandler, Proxy, 被代理对象 之间一些的关系:

image-20220911125935132

从注释可知AnnotationInvocationHandler是用于注释的动态代理实现的调用处理程序

再看一下它的上层接口InvocationHandler

image-20220911125955486

从类注释可以知道InvocationHandler是一个调用处理程序是由代理实例的调用处理程序实现的接口
每个代理实例都有一个关联的调用处理程序。在代理实例上调用方法时,将对方法调用进行编码并将其调度到其调用处理程序的 invoke 方法。

这个接口只定义了一个待实现方法invoke

image-20220911130235237

可以看到有三个参数Object proxy, Method method, Object[] args,实现就是当调用proxy对象的方法的时候可以通过invoke调用method方法,(当然怎么调用的话就看实现函数的代码逻辑了)

其实总的来说就是通过以下方法将handler放入Proxy中:

 Object 被代理的对象 = new 被代理对象的类型();
 InvocationHandler handler = new InvocationHandler的实现类(被代理的对象);
 被代理对象的接口类型 proxy = (接口类型)Proxy.newProxyInstance(handler.getClass().getClassLoader(), 被代理对象的接口类.getClass().getInterfaces(), handler);

通过以上方法之后, 当我们想要调用被代理的对象.method, 可以通过proxy.method这种方式进行调用, 但是proxy.method这种方式调用会执行什么函数需要看handler的内部具体实现, 因为在执行proxy.method之后就会自动跳转到InvocationHandler的实现类.invoke中,它在InvocationHandler中的接口定义如下:

 public Object invoke(Object proxy, Method method, Object[] args)
 throws Throwable;

在这个payload中被代理的对象就是一个map, handler则是ConvertedClosure

被代理的map则是被赋值到了InvocationHandler对象的memberValues,然后在反序列化的时候就会通过调用memberValues.entrySet()完成触发handler(ConvertedClosure对象)的invoke函数

这个类既继承了闭包Closure又继承了handler,简直就是给这条链子量身打造的.

那么源码pyload构造的了解到这里就暂时结束了, 我们开始断点调试源码

ObjectInputStream.readObject

ObjectInputStream.readObjectsun.reflect.NativeMethodAccessorImpl#invoke这些中间部分都是在反序列化的过程中调用了一堆invoke进行动态调用的过程, 所以在这里就不详细展开,

image-20220911183209875

payload最终生成对象的类型为AnnotationInvocationHandler, 所以我们直接跳到sun.reflect.annotation.AnnotationInvocationHandler#readObject打下断点继续调试

sun.reflect.annotation.AnnotationInvocationHandler#readObject

image-20220911164018992

可以看到在sun.reflect.annotation.AnnotationInvocationHandler#readObject中先是从fields中读出了type之后又读出了memberValues, 而被读出之后memberValues赋值给了streamVals,又调用它的entrySet函数

那么到这里我们就需要捋清楚一些关系了:

虽然AnnotationInvocationHandler继承了InvocationHandler可以作为Proxy代理的handler, 但是在这里并没有, 在反序列化的过程中AnnotationInvocationHandler只是作为一个普通的对象而已, 我们需要对它做的利用就是它在readObject函数中的调用代码, 仅此而已

同时简单说一下下一步的流程, 就是在这个反序列化的AnnotationInvocationHandler对象中, 通过我们的构造之后它从fields读出的memberValues就是我们进行设计的一个Proxy代理, 这个代理中的代理对象是一个Map(因为AnnotationInvocationHandler的构造函数就指定memberValues必须为Map类型), handler则是ConvertedClosure

image-20220911191159347

org.codehaus.groovy.runtime.ConversionHandler#invoke

在前面我们有讲过当一个对象被套上了handler代理之后的函数调用会发生什么变化, 所以可以知道这里取出的memberValues代理对象被调用entrySet方法的时候, 就会转到其handler的invoke进行处理(在这里也就是ConversionHandler.invoke)

image-20220911191649012

org.codehaus.groovy.runtime.ConvertedClosure#invokeCustom

在上一步是会跳转掉到invokeCustom函数,但是在ConversionHandler并没有定义invokeCustom函数,所以调用了它的父类ConvertedClosure中的invokeCustom

image-20220911192749523

image-20220911192625490

我们往前回溯可以看到deledate.call(args)中的args参数就是我们调用entrySet时的参数(为空)

所以到这里就是可以让我们执行一个任意对象的单参数call函数

groovy.lang.Closure#call

image-20220911193404058

这没什么好说的,就是在groovy.lang.Closure里面调用了this.doCall函数

groovy.lang.MetaClassImpl#invokeMethod(java.lang.Object, java.lang.String, java.lang.Object[])

依旧还是写死的东西, 调用invokeMethod函数

image-20220911193608020

groovy.lang.MetaClassImpl#invokeMethod(java.lang.Class, java.lang.Object, java.lang.String, java.lang.Object[], boolean, boolean)

这个函数也几近是收尾的函数了, 就是从这里使用动态调用的方式调用了org.codehaus.groovy.runtime.ProcessGroovyMethods#execute(java.lang.String)然后执行Runtime.getRuntime().exec完成RCE, 在这里这个函数有几百行之长, 所以我简单捋这个函数的整体逻辑:

  1. 主要是通过sender(参数类型)和methodName(方法名)从一个寒素表中匹配查找符合要求的Method然后赋值给method变量

  2. 显示判断传入的object(method函数执行的参数)是不是一个闭包类型,
    如果是一个闭包的话会进入if判断, 在里面可能会循环使用这个闭包再次调用当前函数,
    也有可能使得上面的method变量发生变化

  3. 最后检查结果匹配还有闭包检测逻辑之后的method变量是否为空,
    如果非空的话会将object作为参数通过invoke方法调用method

    image-20220911225205663

第一次:传入一个闭包进入闭包逻辑结构

其实这个函数会执行两次,第一次传入一个闭包(也就是调用call函数的groovy.lang.Closure对象自身this), 然后根据一系列判断和对这个闭包的分析后会取出闭包中的owner并且得到它的Class对象, 然后将设置参数相关的后又再次执行这个函数

image-20220911200236131

这里为什么能够回环, 也就是registry.getMetaClass(ownerClass)刚好返回的是MetaClassImpl,起初我不知道是否默认就是如此,于是便跟进去看了一下, 发现后面是通过层层调用最后在一个org.codehaus.groovy.reflection.ClassInfo对象中获取到的, 然后又看了一下发现默认情况下是会去到org.codehaus.groovy.reflection.ClassInfo.LocalMap#cache这个LocalMap对象中获取获取到的, 从名字可以知道,这是一个缓存, 而反序列化加载到的时候cache是有5个值的,它们分别对应了5个类

image-20220911222437344

java.lang.Object
java.lang.CharSequence
java.lang.Character
java.lang.String
org.codehaus.groovy.runtime.MethodClosure

这五个类和上面闭包的owner(这里就是参数calc.exe)类型匹配. 如果找到对应结果就返回对应ClassInfo对象的MetaClass, 然后在下面调用其invokeMethod函数, 所以我们的参数可以是上面五种类型,我输出了一下它们的MetaClass,发现全部都是MetaClassImpl,所以这就没什么好说的了,下一步肯定会继续重新执行当前函数

image-20220911222240674

第二次:

到第二次的时候就开始秀起来了,

  1. 第二次的时候寻找参数类型为Stringexecute函数, 然后就在table中找到了org.codehaus.groovy.runtime.ProcessGroovyMethods#execute(java.lang.String)

  2. 返回到MetaClassImpl#invokeMethod中, 并将匹配到的方法赋值到method变量

  3. 判断传入的参数object是否为一个闭包, 当前参数为calc.exe的String类型,所以没有进入闭包,继续执行

  4. 当前methodexecute方法非空, 所以调用Method方法的doMethodInvoke执行execute函数

    ![image-20220912000613942]()

org.codehaus.groovy.runtime.ProcessGroovyMethods#execute(java.lang.String)

将传入的字符串通过调用Runtime.getRuntime().exec完成命令执行

image-20220912000654082

Runtime.getRuntime().exec

命令执行...

链子跟踪完毕

一点问题的解决&别的思考

如果想理解这个链子的话, 那么首先理解好Proxy代理中被代理对象和handler的关系是至关重要的, 如果明白这点的话那么对恶意类的构造几乎就不是问题了

问题

构造payload没问题了, 下面我要说的一点是我在反序列化过程中遇到的一点问题:

  1. 当时我在调试MetaClassImpl#invokeMethod函数之后我就在想一个问题, 这个使用闭包进行第二次调用的原因是什么?

    因为我们完全可以在第一次调用的时候就让参数和第二次一样, 为什么还要多此一举地二次调用呢?

    之所以产生下面的问题是由于我并没有注意到第一次调用MetaClassImpl#invokeMethod的时候执行的方法名已经被指定为docall了, 但是也正是这个粗心的错误引发了我更多的一些思考, 所以我便继续记录下来了

    于是我在第一次执行MetaClassImpl#invokeMethod之前打下了断点进行调试

    image-20220912004849804

    下面在调试窗中进行代码测试

    image-20220912005114089

    确定功能正常了下面我们就修改执行的代码将参数类型指定为String.class, 执行参数为calc.exe, 执行函数为execute

    invokeMethod(String.class, "calc.exe", "execute", originalArguments, false, false)

    image-20220912005547650

    可以看到命令触发失败了, 然后我又进一步的进行调试发现问题所在,那就是在通过闭包进行第二次调用MetaClassImpl#invokeMethod之前, 我在寻找Method函数table这个Map表中是找不到execute函数的对应数据项的, 在第二次调用的时候才在table能找到execute函数对象的数据项, 下面我在第二次执行之前打下断点进行调试

    先在进入第二次调用MetaClassImpl#invokeMethod之前执行invokeMethod(String.class, "calc.exe", "execute", originalArguments, false, false),结果是失败了的

    image-20220912012007352

    在进入第二次调用的时候再来执行一次invokeMethod(String.class, "calc.exe", "execute", originalArguments, false, false)就成功了

    image-20220912012240608

  2. 详析存放method方法的table表

    通过查看table表的内容, 我们可以知道我们可以执行哪些函数

    image-20220912014620321

    image-20220912014745905

    查看一下table的值

    image-20220912014858601

    可以看到有Map大小为256,里面全部的参数类型都是String类型, 但是不管我在第一次还是第二次都没有直接看到execute函数的身影

    然后通过断点调试发现貌似这个只是表象, 实际上在很多个子项里面都有多个method方法, 可以通过nextHashEntry获取下一项

    这里execute方法就是在table的第h & (table.length - 1) = 18项中, execute方法可以通过table[18].nextHashEntry获取到

    image-20220912030413103

  3. 原因的话在后面我才反应过来, 两次调用MetaClassImpl#invokeMethodMetaClassImpl或许并不是一样的, 于是我在第一次和第二次进入org.codehaus.groovy.runtime.metaclass.MetaMethodIndex#getMethods的时候打下了断点输出table,看到它们的对象编号确实不一样,,,,,,,,而且第一次的table中method指定的参数对象都是Closure闭包类型

    image-20220912034335283

    所以到这里也就解释的通了, 在上面第一次进入MetaClassImpl的时候我们有说过可以支持五种类型的参数分别有一个org.codehaus.groovy.runtime.metaclass.MetaMethodIndex对象与之对应, 因此也就有5张table

思考

先说明一下, 本人觉得自己对Java的理解能力还是有限的, 目前也正处于学习之中, 对于将其yso系列的文章有的是之前就有所掌握的, 也有完全就是新学习的, 所以在很多地方或许会差生一些大佬们觉得不值一提的疑问那么还请见谅。 另外这里的一些想法也是个人在源码学习的过程中产生的一些思考, 但由于时间原因所以就没有去进行进一步的深挖和验证, 在此仅当个人学习的参考记录吧:

  1. table的fuzz

在这里的table中从第一个到最后一个共有256个元素, 而它们的MetaMethodIndex对象编号从第一个到最后一个更是高达上千个, 然后在下标为16的元素中我又通过多次.nextClassEntry再次拿到了execute方法,所以不知道里面到底有多少个可以被我们选用执行的函数

如果有需要的话或许我们可以对可选的mthod方法进行一次fuzz或许会有新的发现

image-20220912031137283

  1. 在这里也是留个坑吧, 那就是在看到groovy.lang.MetaClassImpl#invokeMethod里面如果传入的是一个闭包的情况会执行的代码情况

    实际上在invokeMethod函数的闭包处理流程中我们执行了的只是其中的一小部分, 也就是第一个if判断, 确定传入的闭包类型是否为CurriedClosureMethodClosure如果是这两种的其中一种就进入第一个if判断且必定会再一次根据闭包的内容再一次回调当前函数

    但是除了上面两种闭包之外其他的闭包也会根据闭包的getResolveStrategy()函数返回内容通过switch判定多种分支情况, 这里情况有很多种,但是感觉和上面大同小异, 兴许有另外的触发点

  2. 虽然payload中ConvertedClosure的构造函数指定传入的第一个参数必须是一个Closure闭包,但是其赋值给的对象org.codehaus.groovy.runtime.ConversionHandler#delegate确实Object类型, 所以我们还是或许可以通过反射的方式直接修改对象的delegate属性变为Objec类型, 然后再次通过断点进入metaMethodIndex再步入查看Obejct的table表中有没有危险函数(当然此方法可行的话除了String, Closure, Object之外剩下的两个类型的table也是可以看到的)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇