Liferay revisited: A tale of 20k$


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

Discovery of jkstatus bypass

After a quick reconnaissance, an endpoint on a subdomain caught our attention:;

(“;” is a well-known trick to bypass reverse-proxy in Tomcat. It was first presented at BlackHat USA 2018 by Orange Tsai).

JK Status Manager

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?

From jkstatus to a Liferay instance

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:

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.

Flow of a HTTP request to 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:


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.

URI Mappings of Apache LoadBalancer

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:

  • We have a way to reach the Liferay instance with an arbitrary suffix payload.
  • We need to traverse back to the Liferay context and then reach /api/jsonws endpoint.
  • We can’t input ..; because of Akamai WAF. But how about encoding it?

Let try this payload:


Bypass payload process by Akamai WAF

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.

Bypass payload process by Apache LoadBalancer

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

Bypassing Akamai and pwning the Liferay instance

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:

Common public POC for CVE-2020-7961

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:


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

Bypass the limitation

Combine all the above techniques, the final URL of the payload is:



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

  • We found a way to set parameters like “-p_auth”,“-tableId”,“-name”,“-type” on the URL of the request (Akamai WAF allows me to have the “:” character in the URL).
  • Luckily our exploit chain is hex-encoded so we can put it in the POST body.

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:

Final payload to achieve pre-auth RCE on the target

From a real case to an interesting CTF challenge

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.

Credit: k0nv0y@VSRC