Persisting XSS With IFrame Traps
XSS Iframe Traps
Longer Running XSS Payloads
An issue with cross-site scripting (XSS) attacks is that our injected JavaScript might not run for an extended period of time. It may be a reflected XSS vulnerability where we've tricked our user into clicking a link, but when they land on the page where we were able to inject our malicious JavaScript it may not be a location in the application where they'll remain. They may close the browser tab or navigate to a different page in our application, and our JavaScript will stop running. Similarly with a stored XSS vulnerability, a user may only be viewing the page with our JavaScript in passing.
This is fine if our payload runs quickly, which is commonly the case. JavaScript, for all of its flaws, runs quite fast. If you have requests coded up in JavaScript for your attack, it may only take a second to run, and it may not matter if the user closes the browser tab after landing on an unexpected page. A payload to add a new Admin user, or download sensitive files from the user's account might complete fast enough.
But what if you need a longer running XSS payload? Perhaps you're exfiltrating a significant amount of data or you don't know what functionality to attack and you're crawling/exploring the application content and functionality through your injected JavaScript?
In this blog post I want to discuss a simple technique for tricking the user into staying on the page where your malicious JavaScript is running, effectively buying you time for longer running XSS payloads. We'll trap the user in an iframe and manipulate the URL address bar artificially so they think they're using the application normally. This can be a compelling ruse as long as the target application allows iframing from the same origin, a not uncommon configuration.
The idea for this attack came from discussions with my clever colleague @n00py who has used similar techniques to fake login pages on pages with XSS. This technique allows him to capture credentials from users.
Instead of a fake login page, we'll use the ability to spoof the address in the URL address bar to make it appear that the iframe the user is trapped in is the actual page they're on. As they navigate the application, our JavaScript will retrieve the URL from the iframe they're navigating and copy it into the browser URL address bar, hiding their actual location which is the page with the XSS JavaScript.
For demonstration purposes throughout the blog (until the end), we'll keep the iframe as a smaller overlay of the actual page instead of displaying it fullscreen. We'll also set the background color of the XSS landing page to pink, in order to make it easier to discern the real page with XSS from the iframe where the user is trapped.
Once we close the alert box, the trap code will start running. An iframe will be created with a different page in the application as the starting URL. I chose the homepage for example, but the appropriate page may depend on your pretext in a social engineering campaign.
In order to fake the URL address bar, our iframe trap has registered a number of events in the iframe page itself. When something in that iframe changes, the user navigates, scrolls, etc, an updateUrl() function is called. This function retrieves the URL path of the iframe, then sets it into the actual address bar using the window.history.replaceState() function call. This method is intended to allow programmers to manage the browser history but certainly has some fun side effects for attackers.
Without this code to modify the address bar, the URL will show the landing page where our XSS is running, http://192.168.78.157/index.php/bwg_gallery/dry-run/
, which could draw suspicion as the user navigates the site and the URL never changes. With our updateUrl() function, the address bar will reflect the address the user would expect, hopefully encouraging them to stay in our iframe trap.
As an example, once the user clicks the Site Admin link, the iframe will load that page, and our updateUrl() function will get called, properly faking the address bar once again.
The application and the browser will appear as though all is normal, and our XSS payload continues to run. Neat. Another fun aspect of this attack is that by using the replaceState() function to spoof the URL address, we're also managing the history list. This means that user's back/forward buttons will work as expected with their iframe responding, leading to the page they expect, and the URL address bar getting properly spoofed again properly.
There are, of course, a number of ways the user can inadvertently escape our iframe trap, and we lose execution of our XSS payload. They could close the browser tab, for example. Not much we can do there.
They could also manually type a URL in the address bar and hit enter. This will lead the browser to load the page where they typed, and they've escaped our trap. Even if they simply highlight the URL bar and hit enter to reload the page, the browser will load the spoofed URL in the address bar, and we'll lose our trap. Similarly, with a reload request by button, context menu, or keyboard shortcut, the result will be escape rom the iframe trap.
We can combat this by partially addressing the reload issue. First, we can disable the right-click context menu, making it a bit harder to find that reload control. We need to do this on each new page loaded in our iframe, so this functionality is added into the updateUrl() function, so it's run regularly.
A more likely reload event is going to come from a user utilizing the reload button in the browser bar near the back/forward buttons. We can't stop this, but we can prevent it from breaking our iframe trap. This will make our trap less stealthy however.
First, we register event handlers for the mouse leaving or entering the iframe.
Because the user pressing the reload button will load the page displayed in the URL address bar, what we'll do when the user's mouse leaves the iframe is save the URL that's currently showing in the address bar and replace it with the actual XSS landing page. Hopefully (fingers crossed), the user doesn't notice the change. The 'fake' URL is saved to session storage in the user's browser.
Now, if the user clicks the refresh button, our trap is escaped. However, the page with the XSS is reloaded, bringing up a fresh iframe trap.
The startup code for our iframe trap checks the session storage of the browser to see if there is a saved page. If there is, it sets the iframe location to that URL, making it appear to the user that they simply refreshed the page in the iframe. Everything appears to be working as expected.
That is an easy way of trying to keep your XSS payload running longer. The proof-of-concept is written fairly generically, but you might need to tweak it a bit for your client's applications. Always test to see if things are working as you would expect. The proof-of-concept also lacks any malicious code. It simply demonstrates the trap technique.
Further interesting XSS persistence/traps can be found in the BeEF browser exploitation framework. If you have any issues or ideas on how this can be improved, my DMs are always open @hoodoer.
- https://gist.github.com/hoodoer/6b005b501dc0ff90f8b00b90611bc2bb (XSS trap sourcecode)
- https://beefproject.com/