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”.
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. ***
It works but it’s an easy target for a filter. Let’s split the string creating some variables and make some changes.
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.
The same splitting used for location can be performed here:
Using a property-based payload with the splitting technique, a bypass in a known WAF vendor (Imperva) was discovered little time ago:
— 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:
Double slashes were added just in case there’s any native text content after injection.
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:
Here’s the DOM view:
So our next vector is straightforward:
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:
Now for our inner/outerHTML property-based payloads we have something:
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:
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:
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:
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.
— Rodolfo Assis (@rodoassis) April 21, 2021