JSONP injection is a lesser known but quite widespread and dangerous vulnerability and it surfaced in the last years due to the high rate of adoption of JSON, web APIs and the urging need for cross-domain communications.
What is JSONP?
Assuming everybody knows what JSON is, let’s talk a little about JSONP. JSONP comes from JSON with Padding and it was created in order to bypass common restrictions such as Same-origin Policy which is enforced for XMLHttpRequest (AJAX requests).
Let’s take an example. Our online banking application, http://verysecurebank.ro, has implemented an API call that returns the current user’s transactions.
An HTTP request to the http://verysecurebank.ro/getAccountTransactions endpoint presents us with the transactions, JSON formatted:
If our reports application, accessible at http://reports.verysecurebank.ro wants to get the transaction details, an AJAX call to the page won’t be possible, due to Same-origin Policy being in effect (different host).
Note: even if it might be obvious, it’s worth mentioning that when including a script cross-domain, it will run in the context of the including application, not in the source’s context.
Adding a callback to the API response, wrapped around the JSON formatted data, allows us to load the API response between script tags and get its content by defining our own callback function to handle it.
These are the most common scenarios you will come across:
- the callback function is hardcoded within the response
- basic function call
- object method call
- the callback function is dynamic
- completely controllable from the URL (GET variable)
- partially controllable from the URL (GET variable), but appended with a number
- controllable from the URL (GET variable), but initially not displayed in the request
Basic function call
A very common example where the myCallback callback is hardcoded within the response, wrapped around the JSON formatted data:
We can easily exploit this by defining the myCallback function first and afterwards referencing the API call within script tags:
Note: Make sure you define your function before including the response, otherwise an undefined function will be called and you won’t get any data.
When the logged in victim visits our malicious page, we grab his data. For the sake of brevity, we display the data in the current page, neatly formatted.
Object method call
This is almost identical with the first example and you might encounter it in ASP or ASP.NET web applications. In our example, System.TransactionData.Fetch is added as a callback around the JSON formatted data:
We simply create the Fetch method for the TransactionData object which is already part of the System object.
The result is the same, so no screenshots from now on.
Completely controllable from the URL (GET variable)
This is the most common scenario you will come across. The callback function is specified in the URL and we can completely control it. The callback URL parameter allows us to change the name of the callback, so we set it to testing and see it changed within the response:
We can basically use the same exploit code, just don’t forget to add the callback parameter when including the response with script tags.
Partially controllable from the URL (GET variable), but appended with a number
In this scenario, the callback function name is appended with something, usually a number. In most cases, we get something like jQuery and a short number appended to it, like 12345, the callback becoming jQuery12345.
Logically, the exploit code remains the same, we just have to add 12345 only to our callback function name, and not when including the script.
But what if the number is not hardcoded? What if it’s dynamic and different for each session? If it’s a relatively short number we can programmatically pre-define functions for each possibility. Let’s suppose the appended number goes up to 99.999. We can programmatically create all these functions so whatever number is appended, we already have a callback function for it. Here is the sample code, I used a simpler callback just to illustrate the result:
What happens here: we have the hardcoded callback name jQuery, we set a limit for the number of functions. In the first loop we generate the callback function names in the callbackNames array. Then we loop through the array and turn each callback name into a global function. Please note that in order to shorten up the code I only alert the amount of money sent in the first transaction. Let’s see how it works:
On my machine, it took around 5 seconds to display the alert with the callback name being jQuery12345. This means it took Chrome under 5 seconds to create over 10.000 functions, so I would boldly say it’s a pretty doable exploitation.
Controllable from the URL (GET variable), but initially not displayed in the request
The last scenario involves an API call that apparently does not have a callback, so no visible JSONP. This might happen when the developers leave “hidden” backwards compatibility with other software or the code simply was not removed at refactoring. So, when seeing an API call without callback, especially if the JSON formatted data is already between parentheses, manually add the callback to the request.
If we have the following API call http://verysecurebank.ro/getAccountTransactions, we might try to guess the callback variable:
While these are the most common callback names, feel free to guess some more. If our callback is added to the response, bingo, let’s ethically grab some data.
Basic data grabbing
Since we only displayed the data until now, let’s see how we can send it back to us. This is a minimal example of JSONP data grabbing you can use as a proof of concept.
We make a GET request to our data grabber with the application response (transactions) in the data parameter.
Note: make sure you use JSON.stringify() on the data since it is an object, and we don’t want to end up with only [object Object] in our file.
Note: if the response is large, make sure you switch to POST since you may not receive the complete data due to HTTP GET size restrictions.
Here is our grabData.php code, we append the received data in the data.txt file:
While hunting web applications for JSONP vulnerabilities, we might encounter some issues. Here we try to tackle them.
Content-Type and X-Content-Type-Options
In this example, Content-Type is set to application/json and X-Content-Type-Options to nosniff.
Latest versions of Google Chrome, Microsoft Edge and Internet Explorer 11 successfully blocked the script execution. However, Firefox 50.1.0 (currently the latest version) didn’t.
Note: If the X-Content-Type-Options: nosniff header is not set, it will work on all aforementioned browsers.
Note: Older versions of browsers do not take into consideration strict MIME type checking, since X-Content-Type-Options was recently implemented. According to Mozilla, this is the browser compatibility:
Sometimes we might get some other response codes than 200, especially since we are messing with the response. I’ve run a few tests on these browsers:
- Microsoft Edge 38.14393.0.0
- Internet Explorer 11.0.38
- Google Chrome 55.0.2883.87
- Mozilla Firefox 50.1.0
These inconsistencies were found:
|Response Code||Works in|
|100 Continue||Internet Explorer, Microsoft Edge, Google Chrome|
|101 Switching Protocols||Google Chrome|
|301 Moved Permanently||Google Chrome|
|302 Found||Google Chrome|
|304 Not Modified||Microsoft Edge|
So even if we don’t get a 200 HTTP code, the vulnerability is still exploitable in other browsers.
Referrer check bypass
- using data URI scheme
If there is a HTTP Referer check, we can try not to send it in order to bypass the verification. How can we do that? Introducing Data URI.
We can abuse data URI scheme in order to make the request without a HTTP Referer. Since we are dealing with code, which includes quotes, double quotes and other syntax breaking characters, we are going to base64 encode our payload (callback definition and script inclusion).
This is the syntax: data:text/plain;base64,our_base64_encoded_code
Here are the three main HTML tags that allow us to use the data URI scheme:
- iframe (in the src attribute) – it does not work in Internet Explorer
- embed (in the src attribute) – it does not work in Internet Explorer and Microsoft Edge
- object (in the data attribute) – it does not work in Internet Explorer and Microsoft edge
We can see that no HTTP Referer was sent in the API request.
2. request from HTTPS to HTTP
We can also avoid sending a HTTP Referer by hosting our code on a HTTPS page, if our target website can be accessed via HTTP. If we issue a HTTP request from a HTTPS page, the browser is instructed not to send the Referer header in order to prevent information leakage.
All we have to do is to host our malicious code on a HTTPS enabled website.
Note: due to mixed-content security mechanism, this does not work on modern web browsers with the default settings. The victim has manually accept the security warnings of the browsers.
However, it works in older versions of the browsers, and the HTTP Referer header is not sent, as we can see:
How we can fix this
Lastly, let’s see how we can prevent this from happening. The most straightforward and modern approach is CORS (Cross-Origin Resource Sharing).
- completely remove JSONP functionality
- add the Access-Control-Allow-Origin header to your API response
- use cross-domain AJAX requests
The API response includes the Access-Control-Allow-Origin: http://reports.verysecurebank.ro:
And we get the content of http://verysecurebank.ro/getAccountTransactions:
Although JSONP usage is decreasing, there is still a large number of websites that still use or support it. As a final tip, when dealing with JSONP also don’t forget to check for Reflected File Download and Reflected Cross-Site Scripting vulnerabilities. Happy pwning!