• 漏洞分析|Adobe ColdFusion WDDX 序列化漏洞利用


    0x01 概述

    在上一篇有关 Adobe ColdFusion 序列化漏洞(CVE-2023-29300)的文章中,我们对已公开的 JNDI 利用链(CVE-2023-38204)进行了复现。JNDI 利用链受目标出网的限制,在不出网的情况下无法很好地利用。本文中我们将分享不出网的利用方式,提出 C3P0 和 JGroups 两条基于服务错误部署的新利用链。经过测试,C3P0 存在被利用的可能,JGroups 利用成功率为 0.1%。尽管能被利用成功的概率较低,我们也已经在 Goby 中实现了 C3P0 和 JGroups 利用链的完整利用,完全支持命令执行以及结果回显功能。

    0x02 Apache Felix

    ColdFusion 通过自身实现的FelixClassloader来调用BundleClassLoader#loadClass()方法,用于启动和禁用后台一个叫做 Package Manager 的服务下载的插件,动态加载 bundles 文件夹下的各种 Jar 包。
    我们可以看到这些 Jar 包的 MANIFEST 文件中多了不少字段:

    Manifest-Version: 1.0
    Bnd-LastModified: 1490514990031
    Build-Jdk: 1.8.0_111
    Built-By: uriel
    Bundle-Description: Java reflect give poor performance on getter sette
     r an constructor calls, accessors-smart use ASM to speed up those cal
     ls.
    Bundle-DocURL: http://www.minidev.net/
    Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt
    Bundle-ManifestVersion: 2
    Bundle-Name: accessors-smart
    Bundle-SymbolicName: net.minidev.accessors-smart
    Bundle-Vendor: Chemouni Uriel
    Bundle-Version: 1.2
    Created-By: Apache Maven Bundle Plugin
    Export-Package: net.minidev.asm;version="1.2.0";uses:="org.objectweb.a
     sm",net.minidev.asm.ex;version="1.2.0"
    Import-Package: net.minidev.asm.ex;version="[1.2,2)",org.objectweb.asm
     ;version="[5.0,6)"
    Tool: Bnd-3.3.0.201609221906
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    搜索可知,Apache Felix 是 OSGi(Open Service Gateway Initiative,开放服务网关协议)的一种开源实现框架,通过为 Jar 包添加 metadata 来定义哪些类暴露,哪些类隐藏,来实现依赖的模块化,其控制单元就叫做 bundle。
    其中最重要的就是Export-PackageImport-Package两个字段,分别用于告诉 OSGi 自己对外提供哪些类,引用了其它包的哪些类。如果没有这两个字段,OSGi 或者说它的实现 Felix 就无法正常工作。

    0x03 构造利用链

    3.1 Felix ClassLoader

    ColdFusion 在进行 WDDX 序列化的过程中,将调用BundleClassLoader来对目标类进行加载。实现的findClass()方法如下:

    protected Class findClass(String name) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(name);
        if (clazz == null) {
            // ...
    
            String actual = name.replace('.', '/') + ".class";
            byte[] bytes = null;
            List<Content> contentPath = this.m_wiring.m_revision.getContentPath();
            Content content = null;
    
            for(int i = 0; bytes == null && i < contentPath.size(); ++i) {
                bytes = ((Content)contentPath.get(i)).getEntryAsBytes(actual);
                content = (Content)contentPath.get(i);
            }
    
            if (bytes != null) {
                String pkgName = Util.getClassPackage(name);
                Felix felix = this.m_wiring.m_revision.getBundle().getFramework();
                Set<ServiceReference<WeavingHook>> hooks = felix.getHookRegistry().getHooks(WeavingHook.class);
                Set<ServiceReference<WovenClassListener>> wovenClassListeners = felix.getHookRegistry().getHooks(WovenClassListener.class);
                WovenClassImpl wci = null;
                // ...
    
                try {
                    clazz = this.isParallel() ? this.defineClassParallel(name, felix, wovenClassListeners, wci, bytes, content, pkgName) : this.defineClassNotParallel(name, felix, wovenClassListeners, wci, bytes, content, pkgName);
                } catch (ClassFormatError var17) {
                    // ...
                }
    
                // ...
            }
        }
    
        return clazz;
    }
    
    • 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

    此处会将全限类名转换为相对文件路径,如传入javax.sql.ConnectionPoolDataSource,则会尝试读取相对路径javax/sql/ConnectionPoolDataSource.class文件中的字节码,之后再去defineClassParallel()方法中调用defineClass()。由于这种特殊的类加载模式,即使ConnectionPoolDataSource存于rt.jar,也会因为无法定位 class 文件而抛出NoClassDefFoundError

    java.lang.NoClassDefFoundError: javax/sql/ConnectionPoolDataSource
    	at java.lang.ClassLoader.defineClass1(Native Method)
    	at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    	at org.apache.felix.framework.BundleWiringImpl$BundleClassLoader.defineClass(BundleWiringImpl.java:2338)
    	at org.apache.felix.framework.BundleWiringImpl$BundleClassLoader.defineClassParallel(BundleWiringImpl.java:2156)
    	at org.apache.felix.framework.BundleWiringImpl$BundleClassLoader.findClass(BundleWiringImpl.java:2090)
    	at org.apache.felix.framework.BundleWiringImpl.findClassOrResourceByDelegation(BundleWiringImpl.java:1556)
    	at org.apache.felix.framework.BundleWiringImpl.access$300(BundleWiringImpl.java:79)
    	at org.apache.felix.framework.BundleWiringImpl$BundleClassLoader.loadClass(BundleWiringImpl.java:1976)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    不过还有一种可能,如果目标类已提前被加载,ClassLoader#findLoadedClass()就会直接返回目标类,也不会进入抛出错误的分支。那么会不会有人部署服务时不走寻常路,将原本散落各处的依赖包全部集中到了自定义的 lib 中呢?
    本着遵循客观事实的原则,我们进行了利用测试,结果表明的确存在可以利用的目标:
    image.png
    image.png
    无一例外,这些目标都手动更改了服务的依赖包路径。相比使用 Package Manager 服务,他们更愿意自己手动管理依赖,这也将绕过BundleClassLoader的依赖加载机制,留给我们相当大的操作空间。

    3.2 C3P0

    WrapperConnectionPoolDataSourceBase#setUserOverridesAsString()方法为入口,构造的调用链如下,最后将传入的字节解码并调用原生反序列化。

    WddxDeserializer
    	-> deserialize()
    
    ...
    
    WrapperConnectionPoolDataSourceBase
    	-> setUserOverridesAsString()
    
    VetoableChangeSupport
    	-> fireVetoableChange()
    
    VetoableChangeListener
    	-> vetoableChange()
    
    C3P0ImplUtils
    	-> parseUserOverridesAsString()
    
    SerializableUtils
    	-> fromByteArray()
    	-> deserializeFromByteArray()
    
    ObjectInputStream
    	-> readObject()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    parseUserOverridesAsString()方法将截取并 Hex 解码传入的字节流,因此我们构造原生序列化流时需要相应地填写占位和进行编码。

        public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException {
            if (userOverridesAsString != null) {
                String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);
                byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);
                return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));
            }
            // ...
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    _5db20be14d804ed14721d4d90a25560c_-1996400489_图片.gif

    3.3 JGroups

    和 C3P0 类似,JGroups 的ReplicatedTree#setState()方法接受字节数组参数,并调用原生反序列化,利用链如下。

    WddxDeserializer
    	-> deserialize()
    
    ...
    
    ReplicatedTree
    	-> setState()
    
    Util
    	-> objectFromByteBuffer()
    	-> oldObjectFromByteBuffer()
    
    ObjectInputStream
    	-> readObject()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    由于setState()方法接收的参数不是基本类型,我们需要利用标签触发BinaryHandler来传入字节数组,并进行 base64 编码。

    public void onEndElement() throws WddxDeserializationException {
    	this.setValue(Base64Encoder.decode(this.m_buffer.toString()));
    	this.setType(-3, "VARBINARY");
    }
    
    • 1
    • 2
    • 3
    • 4

    objectFromByteBuffer()方法将根据传入的第一个字节,调用不同的readObject(),因此构造原生序列化流时还需要在首位添加一个0x2

    public static Object objectFromByteBuffer(byte[] buffer, int offset, int length) throws Exception {
        // ...
        ByteArrayInputStream in_stream = new ByteArrayInputStream(buffer, offset, length);
        byte b = (byte)in_stream.read();
        // ...
        try {
            ObjectInputStream ois;
            switch (b) {
                // ...
                case 2:
                    in = new ObjectInputStream(in_stream);
                    retval = ((ObjectInputStream)in).readObject();
                    break;
                // ...
            }
            // ...
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    aaa.jpg_a5b5e3b901682635665925c370e2e751_-696553035_图片(1).gif

    0x04 总结

    通过深入挖掘 CVE-2023-29300 的利用方式,我们摸清了产品更多的细节,如BundleClassLoader的类加载机制,并最终提出了两条不出网的利用链:C3P0 和 JGroups。虽然在默认部署环境中无法成功利用,我们最开始也不相信有人会手动破坏依赖管理机制,但事实是规则总有人打破,而这也让攻击者得以趁虚而入。
    希望在阅读本篇文章后,能为大家带来一些新思路的启发。

    0x05 参考链接

    https://helpx.adobe.com/security.html
    https://felix.apache.org/documentation/index.html


    Goby 欢迎表哥/表姐们加入我们的社区大家庭,一起交流技术、生活趣事、奇闻八卦,结交无数白帽好友。

    也欢迎投稿到 Goby(Goby 介绍/扫描/口令爆破/漏洞利用/插件开发/ PoC 编写/ IP 库使用场景/ Webshell /漏洞分析 等文章均可),审核通过后可奖励 Goby 红队版,快来加入微信群体验吧~~~

    文章来自Goby社区成员:M1sery@白帽汇安全研究院,转载请注明出处。
    微信群:公众号发暗号“加群”,参与积分商城、抽奖等众多有趣的活动
    获取版本:https://gobysec.net/sale

  • 相关阅读:
    线程/进程/协程的区别以及多线程状态/多线程的统一异常处理
    简单表达式的计算(两种方法)
    【C++类的继承、父子类】牛顿插值公式求近似值
    【漏洞复现】时空智友企业流程化管控系统 session泄露
    第六章:接口
    深入解读 Android Hook 技术-从原理到实践
    一篇文章带你走进cookie,session,Token的世界
    Jetson xvaier nx 安装torch1.6.0 torchvision0.7.0
    资源管理的部分
    .NET分布式Orleans - 2 - Grain的通信原理与定义
  • 原文地址:https://blog.csdn.net/m0_46699477/article/details/132734669