Fetching contributors…
Cannot retrieve contributors at this time
350 lines (266 sloc) 12.6 KB
Copyright 2006 Andres Riancho
This file is part of w3af, .
w3af is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 2 of the License.
w3af is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with w3af; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import w3af.core.controllers.output_manager as om
import as kb
import as severity
from w3af.core.controllers.plugins.audit_plugin import AuditPlugin
from w3af.core.controllers.csp.utils import site_protected_against_xss_by_csp
from import Vuln
from import DiskList
from import create_mutants
from import rand_alnum
from import opt_factory
from import OptionList
from import get_context_iter
class xss(AuditPlugin):
Identify cross site scripting vulnerabilities.
:author: Andres Riancho (
:author: Taras (
# TODO: Reduce the number of payloads by concatenating similar/related ones
# Start a new tag
# Escape HTML comments
# Escape JavaScript multi line and CSS comments
# Escapes for CSS
# The ":" is useful in cases where we want to add the javascript
# protocol like <a href="PAYLOAD"> --> <a href="javascript:alert()">
# Escape single line comments in JavaScript
# Escape the HTML attribute value string delimiter
# Escape HTML attribute values without string delimiters
" =",
def __init__(self):
self._xss_mutants = DiskList(table_prefix='xss')
# User configured parameters
self._check_persistent_xss = True
def audit(self, freq, orig_response):
Tests an URL for XSS vulnerabilities.
:param freq: A FuzzableRequest
fake_mutants = create_mutants(freq, [''])
# Before we run each fake mutant check in a different thread using the
# worker_pool, but this lead to a strange dead-lock
# So I simply migrated this to a slower for loop.
for fake_mutant in fake_mutants:
def _check_xss_in_parameter(self, mutant):
Tries to identify (persistent) XSS in one parameter.
if not self._identify_trivial_xss(mutant):
def _report_vuln(self, mutant, response, mod_value):
Create a Vuln object and store it in the KB.
:return: None
csp_protects = site_protected_against_xss_by_csp(response)
vuln_severity = severity.LOW if csp_protects else severity.MEDIUM
desc = 'A Cross Site Scripting vulnerability was found at: %s'
desc %= mutant.found_at()
if csp_protects:
desc += ('The risk associated with this vulnerability was lowered'
' because the site correctly implements CSP. The'
' vulnerability is still a risk for the application since'
' only the latest versions of some browsers implement CSP'
' checking.')
v = Vuln.from_mutant('Cross site scripting vulnerability', desc,
vuln_severity,, self.get_name(),
self.kb_append_uniq(self, 'xss', v)
def _identify_trivial_xss(self, mutant):
Identify trivial cases of XSS where all chars are echoed back and no
filter and/or encoding is in place.
:return: True in the case where a trivial XSS was identified.
payload = replace_randomize(''.join(self.PAYLOADS))
trivial_mutant = mutant.copy()
response = self._uri_opener.send_mutant(trivial_mutant)
# Add data for the persistent xss checking
if self._check_persistent_xss:
# This is something I've seen in as a false positive during my
# assessments and is better explained in this stackoverflow question
ct_options, _ = response.get_headers().iget('X-Content-Type-Options')
content_type, _ = response.get_headers().iget('Content-Type')
if content_type == 'application/json' and ct_options == 'nosniff':
# No luck exploiting this JSON XSS
return False
if payload in response.get_body().lower():
self._report_vuln(mutant, response, payload)
return True
return False
def _search_xss(self, mutant):
Analyze the mutant for reflected XSS.
@parameter mutant: A mutant that was used to test if the parameter
was echoed back or not
xss_strings = [replace_randomize(i) for i in self.PAYLOADS]
fuzzable_params = [mutant.get_token_name()]
mutant_list = create_mutants(mutant.get_fuzzable_request(),
def _analyze_echo_result(self, mutant, response):
Do we have a reflected XSS?
:return: None, record all the results in the kb.
# Add data for the persistent xss checking
if self._check_persistent_xss:
with self._plugin_lock:
if self._has_bug(mutant):
sent_payload = mutant.get_token_payload()
body_lower = response.get_body().lower()
sent_payload_lower = sent_payload.lower()
for context in get_context_iter(body_lower, sent_payload_lower):
if context.is_executable() or context.can_break():
self._report_vuln(mutant, response, sent_payload)
def end(self):
This method is called when the plugin wont be used anymore.
if self._check_persistent_xss:
def _identify_persistent_xss(self):
This method is called to check for persistent xss.
Many times a xss isn't on the page we get after the GET/POST of
the xss string. This method searches for the xss string on all
the pages that are known to the framework.
:return: None, Vuln (if any) are saved to the kb.
# Get all known fuzzable requests from the core
fuzzable_requests = kb.kb.get_all_known_fuzzable_requests()
grep=False, cache=False)
def _analyze_persistent_result(self, fuzzable_request, response):
After performing an HTTP request to "fuzzable_request" and getting
"response" analyze if the response contains any of the information sent
by any of the mutants.
:return: None, Vuln (if any) are saved to the kb.
body = response.get_body()
for mutant, mutant_response_id in self._xss_mutants:
sent_payload = mutant.get_token_payload()
for context in get_context_iter(body, sent_payload):
if context.is_executable() or context.can_break():
self._report_persistent_vuln(mutant, response,
def _report_persistent_vuln(self, mutant, response, mutant_response_id,
mod_value, fuzzable_request):
Report a persistent XSS vulnerability to the core.
:return: None, a vulnerability is saved in the KB.
response_ids = [, mutant_response_id]
name = 'Persistent Cross-Site Scripting vulnerability'
desc = ('A persistent Cross Site Scripting vulnerability'
' was found by sending "%s" to the "%s" parameter'
' at %s, which is echoed when browsing to %s.')
desc %= (mod_value, mutant.get_token_name(), mutant.get_url(),
csp_protects = site_protected_against_xss_by_csp(response)
vuln_severity = severity.MEDIUM if csp_protects else severity.HIGH
if csp_protects:
desc += ('The risk associated with this vulnerability was lowered'
' because the site correctly implements CSP. The'
' vulnerability is still a risk for the application since'
' only the latest versions of some browsers implement CSP'
' checking.')
v = Vuln.from_mutant(name, desc, vuln_severity,
response_ids, self.get_name(),
v['persistent'] = True
v['write_payload'] = mutant
v['read_payload'] = fuzzable_request
self.kb_append_uniq(self, 'xss', v)
def get_options(self):
:return: A list of option objects for this plugin.
ol = OptionList()
d1 = 'Identify persistent cross site scripting vulnerabilities'
h1 = ('If set to True, w3af will navigate all pages of the target one'
' more time, searching for persistent cross site scripting'
' vulnerabilities.')
o1 = opt_factory('persistent_xss', self._check_persistent_xss, d1,
'boolean', help=h1)
return ol
def set_options(self, options_list):
This method sets all the options that are configured using the user
interface generated by the framework using the result of get_options().
:param options_list: A dictionary with the options for the plugin.
:return: No value is returned.
self._check_persistent_xss = options_list['persistent_xss'].get_value()
def get_long_desc(self):
:return: A DETAILED description of the plugin functions and features.
return """
This plugin finds Cross Site Scripting (XSS) vulnerabilities.
One configurable parameters exists:
- persistent_xss
To find XSS bugs the plugin will send a set of javascript strings to
every parameter, and search for that input in the response.
The "persistent_xss" parameter makes the plugin store all data
sent to the web application and at the end, request all URLs again
searching for those specially crafted strings.
def replace_randomize(data):
rand_str = rand_alnum(5).lower()
return data.replace(RANDOMIZE, rand_str)