DOM XSS Fuzzing strategies - Part 1


XSSes are by far still the most common vulnerability in Web applications, they are easy to introduce and easier to find in comparison with other classes of vulnerabilities. XSSes are split into 3 families, reflected, stored and DOM-based. The former are the most prevalent one and also the hardest to detect out of the three.

To hunt for DOM XSSes, it is possible to have a static approach, parsing Javascript, tainting sources and sinks, propagating taint statically, etc. This approach is hard for Javascript because of the dynamic nature of the language, which makes it false positive prone, complex and resource intensive.

Dynamic approaches may seem to be more suited for the task, they require instrumentation of the Javascript to introspect the JavaScript runtime. The possible approaches are, the list is by no means exhaustive:

Rewrite of the Javascript code on the fly to inject instrumentation code, this approach is brittle, moderately complex and resource intensive

Instrument the browser Javascript engine, which is the most resource friendly approach, but requires fiddling with a browser internals, which is difficult (because of all the JIT magic most JavaScript engines handles) and costly to maintain on the long run

Use debugger API, by setting breakpoints where appropriate, stepping through the code, etc. This has proven to be slow, not as feature complete as one would hope, good luck setting a breakpoint on all eval calls.

Use coverage API to grossly peek into what is executing coupled with monkey patching and Proxy object injection, this approach is performant, it is easier to implement but theoretically suffers from some limitations

For this blog post, we will build a simple PoC of a coverage guided XSS fuzzer. The fuzzer will use precise coverage information to identify newly executed code paths and use that information to generate new test payloads. The fuzzer will instrument sink methods, except for some limitations (see eval headache).

To make the PoC confined, we will focus on postMessage XSS, we will use the Chrome browser, the remote debugger API, we will write the PoC in Python 3 and use the Pychrome library to interact with the browser.

Enough with the introductions, lets begin.

To interact with Chrome debugger API, we will need to enable it with the following command line:

chrome --verbose --window-size=1200x600 --disable-gpu --remote-debugging-port=9222 --user-data-dir=/tmp/foo --disable-web-security

Optionally, you may want to enable headless mode, will save about 20% resources and important to use if you are building a full blown XSS fuzzer.

The important flag is remote-debugging-port, the rest you can ignore, disable-web-security is to disable the XSS auditor (we would like to find XSSes first, we can figure out how to bypass on a different day).

To access the APIs, all we have to do is instantiate it this way:

import pychrome

debug_host = '127.0.0.1'
debug_port = 9222
url = f"http://{debug_host}:{debug_port}"
browser = pychrome.Browser(url=url)

Now that we have our stage set, lets figure out what should the fuzzer do?

  1. Create a new tab
  2. Enable precise coverage collection in the page
  3. Visit our target page (might seem easy, turns out not to be the case)
  4. Inject instrumentation code
  5. Inject XSS detection methods
  6. Inject payload
  7. Detect paths executed in the code
  8. Generate new payloads from it

Go to step 6

The first step is straight forward, this is the init of our injector class, it creates a new tab, starts it and then enables a set of API in Chrome debugger to collect certain event types:

def __init__(self, browser):
 self.browser = browser
 self.debugger = browser.new_tab()
 self.debugger.start()
 self.debugger.Page.enable()
 self.debugger.Console.enable()
 self.debugger.Runtime.enable()

Enabling code coverage is easy, using its output is a bit more complex. What the following piece of code tries to do is turn the coverage information into something exploitable, it will tell us the executed piece code:

class Coverage:

 def __init__(self, debugger):
  self.debugger = debugger
  self.sources = {}
  self.coverages = []
  self.debugger.Profiler.enable()
  self.debugger.Profiler.start()
  self.debugger.Debugger.enable()
  self.debugger.Debugger.setSkipAllPauses(skip=True)
  self.debugger.set_listener('Debugger.scriptParsed', self._on_script_parsed)
  self.debugger.Profiler.startPreciseCoverage(callCount=True, detailed=True)

 def _on_script_parsed(self, scriptId, **kwargs):
  source = self.debugger.Debugger.getScriptSource(scriptId=scriptId)
  self.sources[scriptId] = source

Visiting a page is easy, it doesnt cover all cases. Should we for instance send a POST method, have certain cookies set or are add specific headers to the request, these cases require a more complex code, that in some cases, would require intercept request on the network.

For the sake of the PoC, we will assume the simple case:

self.debugger.Page.navigate(url=url)

Detecting that the page has finished loading is another mess of its own, the debugger API sends events that help with detecting loading completed, but again for the sake of the PoC we will wait:

self.debugger.wait(2)

The next step is injecting instrumentation code that MUST execute before the rest of the page has started loading. This is critical if we need to monkey patch sink methods for instance. The Chrome debugger has an API for that:

source = open('instrument.js', 'r').read()
self.debugger.Page.addScriptToEvaluateOnNewDocument(source=source)

Lets recap, we can now control the Chrome instance, we can start a new tab, we can inject instrumentation code and we can visit our target page. To avoid writing overblowing the article, we will cover the remaining steps in the next blog post, namely:

  • How to detect the XSS?
  • How to instrument objects using the Javascript Proxy API?
  • How inject payloads?
  • How to exploit the coverage data to detect new branches?
  • How to generate new payloads?