Testing for Cross-Site Scripting (XSS) might seem easy at first sight, with several hacking tools automating this process. But regardless of how tests to find a XSS are performed, automated or manually, here we will see a step-by-step procedure to try to find most of the XSS cases out there.

For that we will use the same approach employed by KNOXSS, our online XSS PoC tool. Although without details of its own implementation and intermediary steps needed to make its decisions (which will be done by ourselves if we follow the tests manually), this will cover pretty much what is done by this unique tool.

Test Procedure

1. First of all, we need to look for a reflection of our input in SOURCE CODE or in DOM.

(It must be clear here the difference between those 2: source code is what we see by pressing Ctrl+U keys in browser for a given page while DOM is what we see in the Elements Tab of Browser Developer Tools by pressing F12 key).

Some XSS vulnerabilities are a Stored type and those usually also reflect right in the response so we can treat them in the same way regarding testing. If not, they can be a Blind type (we will see later how to proceed) or simply trigger in another page. This last one makes a little harder to check the results of our attempts and although manually it can be done, in an automated way it greatly increases the complexity of a tool.

To look for a reflection of our input we use any simple string like “test123” (without quotes) as our input in all of the following places (all test points are in bold):

  • Values of every URL parameter and/or every POST body parameter.
    Example: param1=value1&param2=value2&param3=value3
  • In path of the URL replacing each folder, subfolder and page, one at a time.
    Example: https://domain/folder/subfolder1/subfolder2/page?param=value
  • URL fragment like in https://domain/path/page?param=value#data

2. For those entry points above which reflects we proceed with what we call a probe, a string with some needed special char designed to test if there will be some filtering or sanitizing by application.

It’s just our test string with a special char attached like less than sign (<), double quote (“), single quote (‘), etc. Those are the main ones, enough for a manual test and we recommend testing one at a time because by using them all you might trigger some filter or different logic from when you use just one of them.

3. If step 2 succeed (our probe must reflect exactly as it is or it’s sanitized) it’s time to check where it got reflected, in source code or DOM. For each one of them there are different proceedings:

3.1. Reflection in SOURCE CODE:

We check if it’s outside or inside a script block.

  • Outside script block: we use simple or inline HTML injection (cases #2, #3 and #4 here), closing the respective tag where injection lands if it’s  one of the following tags: <title>, <textarea>, <style>, <iframe>, <noscript> and <xmp>. If injection reflects in a comment section like “<!– COMMENT –>”, we break out from it with “–>” or “–!>” (2 dashes, without quotes). We can also use a “javascript:alert(1)” payload (without quotes) if our input reflects inside an anchor element (<a href=HERE>) or in any other case where a source (src) or HTTP reference (href) is expected like in <iframe src=HERE>, <object src=HERE> and <embed src=HERE>. For the anchor, it will need a click to fire the XSS.
  • Inside script block: we use </script> to break out from native script block and inject some HTML (case #5 here) or we inject JavaScript code straight to the script block (cases #6 and #7 here) including Quoteless Injection if applicable (check requirements in the  respective post).

3.2. Reflection in DOM:

Here we will deal with our 3 sinks approach which covers the majority of DOM-based XSS cases.

  • If it gets inserted into DOM elements (Document Sink), we use HTML injection like we do above with reflection in source code but with extra attention: some HTML vectors might not work like “<script>” (usually) due to the fact that scripts are the first things to load in a given page so the injected one “gets late” and does not execute. A vector like <img src onerror=alert(1)> works fine, with some other alternatives.
  • If it redirects to some place with our input (Location Sink) we use  the pseudo-protocol with a payload “javascript:alert(1)” (without quotes). It’s also possible to use “data:text/html,<img/src/onerror=alert(1)>” (again without quotes) if the goal is just to control the browser (it does not run in the context of the website) or if it’s somehow used to return some data to include in DOM. For this latter possibility, we need to check if there’s a request fired with our input without necessarily performing a redirect (usually AJAX). We will fall into a variation of our Document Sink case explained above. Check “CORS Enabled XSS” for more, using the payload above with “document.domain” instead of “1” in the test page of that post to see it working.
  • If there’s some string evaluation (eval function or similar) or template engine in place we have the Execution Sink case. We need to dig into script blocks and JS library calls to try to figure out how it can be vulnerable. It’s by far the most difficult one. Going blackbox to save time, we usually try some payloads like “${alert(1)}” or “alert(1)” alone, both without quotes.

4. Next we drop a Blind XSS payload for every input we test. It’s just a request, regardless if it will reflect or not. We never know where this might going to end, maybe in logs displayed in SIEM-like web applications or in customer service ones (Help Desk for example). It can be a simple <script src=//domain/script.js></script> or any other XSS vector using what is explained here. To know how to create a Blind XSS script check here.

5. Finally we try to guess parameter names while also checking for reflection in the parameter name itself. Some pages come with an incomplete set of parameters, there are hidden functionalities or even forgotten parameters from a previous version. We use a list with the most common parameter names and concatenate them to save time. That list can be appended to the current set of parameters of the page or not (some might be mandatory while others can mess with your probing). Example:

s=test123&search=test123&q=test123&query=test123&test123=0

Notice the last “test123” is not a value of a parameter but a parameter itself whose value is 0. If we hit the source or DOM with one of those guessed parameters we proceed isolating the one which reflects and following the same steps above, starting from step 2.

Important Considerations

  • Inputs in Base64 format, double encoded or with strict length also have to be handled accordingly for proper testing.
  • Reflections which come from error messages must be handled in a certain way to allow the correct exploitation. Those originated from Error-Based SQL Injection, for example, demand single quote and/or backslash characters in specific ways to trigger such situations.
  • Some inputs are validated but not filtered. PHP’s “filter_var” function with “FILTER_VALIDATE_EMAIL” flag, for example, must be exploited using a different way to test for it like shown here.
  • Some nasty JavaScript injections need syntax fixing only possible with the help of a JavaScript feature named HOISTING. Check this for more info.
  • A reflected XSS triggered with POST method must have its PoC built with a HTML form. If it’s not possible, it’s just SELF XSS and it’s useless 99% of the time.
  • XML-based scenarios require strict syntax to work, so we usually try full vectors and payloads instead of testing using these steps.

There are also several tricks that can be used in countless XSS situations and some few specific XSS cases which were not possible to cover here. Our previous post is a good example.

Final Words

All tests and information provided in this post are features or procedures employed by KNOXSS (except the anchor one) and all vectors and payloads to prove a XSS vulnerability manually (or not) can be found in the latest edition of our XSS Cheat Sheet.

#hack2learn