HTML Injection to Stored XSS and Account Takeover This bug was really a nice bug that had many parts and took a lot of effort to build the payload so I would like to share the journey with you. I will refer to it as target domain, I have been hunting on the application a quite long time and i knew about most of the parts. Application is a shop and item selling website in your shop bio there is a very limited HTML injection which is sanitized by a servers-side sanitizer like DOMPurify, looking at the JS files of the application I found this code snippet:
$(document).on('click', '.remote-pagination-container .pagination a', function (e) {
e.preventDefault(); if (isDisabled(e.target)) {
return;
}
const containerSelector = '.remote-pagination-container';
let $container = $(containerSelector);
const updatingContainerClass = $container.data('updating-container-class');
const url = $(this).prop('href');
// ensure that requests go to the current origin
const u = new URL(url, window.origin) // use the current host if relative
if (u.origin !== window.origin) {
return;
}
if (updatingContainerClass) {
$container.spin();
$container.addClass(updatingContainerClass);
}
$('html, body').animate({ scrollTop: 0 }, 400);
APIRequest.get(url).done((html) => {
$container.replaceWith(html);
Let’s look what this code does
remote-pagination-container .pagination a
remote-pagination-container
then looks for the child to have pagination
class finally it will pick the element that is child of this elements and it's <a>
tag const url = $(this).prop('href');
it will get the href of the <a>
tagtarget.com
and send an ajax request to it using APIRequest.get(url).done((html)
$container.replaceWith(html);
Most developers don't know jQuery .replaceWith()
is same as .html()
now we have a potential attack vector. Let's start exploring how we can abuse this, all we need is a response on the site with our controlled data, the content type doesn't matter. First thing came to mind was using API endpoints, the app used REST style API to get the data and showed in application/json format. I used /api/shops/renwashop
which will return a JSON data with all my shop details without sanitization, sadly it didn't work because the app or jQuery will check response content-type if it's JSON it will parse it then in my cause it will be .html([Object object])
Second thing I tried was using graphql
which returns an array but again the app will parse the data to array and we can't get the XSS. Next thing I remembered about an endpoint which the response is text/plain and if we send a new line to it will through an error with our data like this:
Using it and it didn’t work because we made the app throw an error the response code was 400
and the app will only consider 200 OK
as a successful request, all other response types will fail. Next thing I tried was the app had a feature to export your items as a CSV file, you will request a file and it will generate a /exports/file/<token>
which will redirect to an AWS S3 bucket to your CSV file, tried it and didn't work because the S3 bucket was a cross origin and didn't have Access-Control-Allow-Origin
header which blocks the app from reading the response.
All these things failed but I didn’t stop exploring the app for potentially anything we can use, I looked at how images are uploaded in the app. It used a third party company called Cloudinary
and a separate domain images.target.com
but I found something Interesting. In the app there was some type of images which was loaded locally like this /cloudinary/images/image_id?options[something]=something
then it will redirect to images.target.com/image/upload/s--zuNenGNo--/image_id
but there was a problem the image was converted to a smaller one in dimension , looking at cloudinary documentation https://cloudinary.com/documentation/image_transformations#delivery_types
I found out if you supply options[delivery_type]=upload
it will load the original image without compressing and conversion.
Then I grabbed exiftools
and tried to add my XSS payload to a comment or artist header in the image but sadly all those headers were removed, I had to go the hard way. Looked at jpeg
file type structure I found a location which you can insert your payload inside it without harming the image, I used online tool https://hexed.it/
and put my payload inside it like this
Then putting it all together:
<div class="remote-pagination-container"><br><div class="pagination"><br><a href="/cloudinary/images/image_id?options%5Bdelivery_type%5D=upload">Click Here<br><br><br><br></a></div></div>
The app used a token and communication with api.target.com
so using the XSS I stole the token inside local storage and redirected to my domain to steal all the user info and showing it, video POC: