Exploiting API Framework Flexibility
TL;DR
The modern frameworks are often very flexible with what they accept, and will happily treat a POST with a JSON body as interchangeable with a URL encoded body, or even with query parameters. Due to this, an unexploitable JSON XSS vector can sometimes be made exploitable by flipping it to one of these alternative approaches.
Background
In the dim and distant past, most web apps were almost entirely bespoke creations, and they were often riddled with vulnerabilities that were unique to that particular site. These days, a lot of the generic issues have been addressed through the use of the modern app frameworks, which take care of a lot of the heavy lifting when it comes to generating HTML and providing database access etc.
However, those same app frameworks are also very flexible by default when it comes to the ways that they accept data, and they will happily take parameters, whether they are in a JSON body, URL encoded body, or a query string. Yay!
In the Red Corner
From an offensive point of view, this should be interesting as it allows you to take a vector that is otherwise unexploitable, and make it useable.
For example, a POST with a JSON body that returns an HTML response with an XSS payload. It’s a good find, but as it stands, it can’t be practically exploited. However, if you can swap the JSON body to URL encoded (or a query) then you can happily use a self-submitting form to make the XSS work without any interaction at all (no CORS, and the HTML response is rendered).
As if this wasn’t enough, you’ll often also find that the encoding/decoding sequence is different for a parameter that is received from JSON as opposed to the alternatives: this may be enough to enable an attack to work when it otherwise wouldn’t.
Simple
For a simple JSON object, the transposition is really straight forward: you just flip the attribute/value pairs between encoding styles.
{"attribute1":"value1","attribute2":"value2"}
becomes
attribute1=value1&attribute2=value2
Nested
For nested JSON objects/arrays there are a bunch of competing (and incompatible, obviously) approaches. Mostly though, this just means serialising the object into a set of paths.
The main standards for the transposition are Rack, Spring and Stripe. I’ve included a few references to get you started at the end of this blog, but beyond that, you’ll need to deploy your google-fu and tweak your approach to match the target environment.
{"object":{"attribute1":"value1","attribute2":"value2"}}
becomes
object[attribute1]=value1&object[attribute2]=value2
Weird Shit
I have also seen some weird endpoints in the wild, that ignore the content-type altogether, and will instead dynamically detect the data in the body. For these, you can try making the entire JSON request into a text/plain or URL encoded attribute and it may parse just fine.
{"object":{"attribute1":"value1","attribute2":"value2"}}
becomes for text/plain (note the JSON comment suffix to stop the equals sign tripping up the parser)
{"object":{"attribute1":"value1","attribute2":"value2"}}//=
which becomes for URL encoded
%7B%22object%22%3A%7B%22attribute1%22%3A%22value1%22%2C%22attribute2%22%3A%22value2%22%7D%7D%2F%2F=
Self-Submitting Form
And obviously, once you get the transposition working, you’ll want to be able to practically exploit it. A URL encoded POST is treated as a CORS simple request, and so it’ll happily work cross-origin, and will also include the authentication cookies too. Whoop!
<body onload=document.getElementsByTagName('form')[0].submit();>
<form method=post action=https://yo.mama>
<input type=hidden name=attribute1 value=value1>
<input type=hidden name=attribute2 value=value2>
</form>
</body>
In the Blue Corner
The thorough way to defend against this is to hunt out all your endpoints that accept JSON by default, and if they also accept URL encoded and queries (which aren’t used by your apps) then disable the functionality.
However, if you’re in a rush and need to monkey-patch an active attack, then you can always use your WAF to block content-types other than application/json. It’s not pretty, but it’ll get the job done.
References
https://brandur.org/fragments/application-x-wwww-form-urlencoded