At the beginning of this year, we found an interesting exploit chain to achieve pre-auth RCE on an asset of a big Fintech company. Due to their disclosure policy, we have to redact some sensitive information related to that company and only focus on the technical details of this case. Let us refer to the domain of that company as foo.bar
.
After a quick reconnaissance, an endpoint on a subdomain caught our attention:
https://sub.foo.bar/jkstatus;
(“;”
is a well-known trick to bypass reverse-proxy in Tomcat. It was first presented at BlackHat USA 2018 by Orange Tsai).
This low-severity vulnerability discloses some sensitive information related to the internal services. So what do we need to do now? Draft a report and send it to the bug bounty program, right?
No, we're much better than that. There are a lot of defined rules (84 maps) to route requests to each internal endpoint. Luckily there’s one interesting endpoint that will route our requests to a Liferay instance:
https://sub.foo.bar/web/aviva
The game is easy now - you may think - just exploit some Liferay CVEs and get the bounty. But life doesn't always work like that. After a while, we realize that our requests must go through these layers before reaching the Liferay instance.
Now our goal is to find a payload that let us traverse back to the base context of Liferay instead of “/web/aviva”
and then reach a vulnerable endpoint like “/api/liferay”
or “/api/jsonws/”
.
But this asset is protected by Akamai WAF - it is way too good to block common bypasses like:
/o/..;/api/liferay
/o/..;/api/jsonws/invoke
/api;/liferay
/api;/jsonws/invoke
Because of “;”
and “..”
, Akamai blocked us before we could reach the Apache LoadBalancer. We almost gave up at this point but decided to review the defined rules of Apache LoadBalancer one last time.
Let focus on the rule /group/control_panel*
. This rule means it will match the wildcard if our requests starts with /group/control_panel
and then passes them to the workers. Now we have:
/api/jsonws
endpoint...;
because of Akamai WAF. But how about encoding it?Let try this payload:
/group/control_panel/%2e%2e%3b/%2e%2e%3b/api/jsonws/
What happens when /group/control_panel/%2e%2e%3b/%2e%2e%3b/api/jsonws/
goes through Akamai?
The answer is that Akamai WAF will let this payload go through because Akamai won’t do URL decoding. Next, our payload will go to Apache LoadBalancer - it will do URL decoding and then route it to the internal services according to the defined URI mappings.
Now our payload is /group/control_panel/..;/..;/api/jsonws/
.
At this stage, our payload matches the rule /group/control_panel*
so Apache LoadBalancer will route our payload to Liferay.
Liferay is based on Tomcat, and I guess you know the trick of using “;”
- Liferay treats “;”
as a path parameter, it will strip it from the URL, so now our payload is:
/group/control_panel/../../api/jsonws/
which normalized to /api/jsonws/
.
We know that this instance is vulnerable to CVE-2020-7961 (with the Last-Modified trick). Most public POC for CVE-2020-7961 has content like this:
But these payloads won't work because Akamai WAF blocks some special characters on POST body:
{ } ; :
These characters are needed to be used in the JSON payload for CVE-2020-7961. Since we can’t use it anymore, we need to find another way to exploit it. From a blog post of codewhitesec - the author of CVE-2020-7961 pointed out that we can invoke JSON Web Service in GET requests like this:
/api/jsonws/service-class-name/service-method-name/arg1/val1/arg2/val2/
Still, we have to deal with one last thing: the GET URL mustn't be too long.
After spending time looking at the documentation of Liferay, we found a way to bypass this limitation. According to this, we can call the JSON WebService in multiple ways - which means we can overcome the limitation of the JSON payload. This is good for us because we can’t use JSON anymore (Akamai WAF blocks {
and }
characters - as mentioned above).
Combine all the above techniques, the final URL of the payload is:
/group/control_panel/..%3b/..%3b/api/jsonws/expandocolumn/add-column/-p_auth/-tableId/-name/-type/-defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource/
NOTE:
-p_auth, -tableId, -name, -type, ...
: assign a null value to these parameters.-defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
: init the object defaultData with type com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
.In the POST body we use defaultData.userOverridesAsString
to trigger setter of defaultData to set our payload when C3P0ImplUtils.parseUserOverridesAsString()
was called and our serialized payload will be deserialized.
To summarize:
“-p_auth”,“-tableId”,“-name”,“-type”
on the URL of the request (Akamai WAF allows me to have the “:”
character in the URL).And the final thing we need is a handy deserialization payload to return the output of our commands in the HTTP response since the target doesn't have an outbound connection:
After a few weeks, we found another way to exploit this target with only a GET request. Our payload is short enough to execute the commands and write the output to Liferay's webroot. We put this idea into our TetCTF 2022 challenge. If you’re interested, you can try it here.