class RESTAPIOutput(OutputPlugin): """ Store all log messages on a DiskList :author: Andres Riancho ([email protected]) """ def __init__(self): super(RESTAPIOutput, self).__init__() self.log = DiskList(table_prefix='RestApiScanLog') self.log_id = -1 def get_log_id(self): self.log_id += 1 return self.log_id def debug(self, msg_string, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for debug messages. """ m = Message(DEBUG, self._clean_string(msg_string), self.get_log_id()) self.log.append(m) def information(self, msg_string, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for informational messages. """ m = Message(INFORMATION, self._clean_string(msg_string), self.get_log_id()) self.log.append(m) def error(self, msg_string, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for error messages. """ m = Message(ERROR, self._clean_string(msg_string), self.get_log_id()) self.log.append(m) def vulnerability(self, msg_string, new_line=True, severity=MEDIUM): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action when a vulnerability is found. """ m = Message(VULNERABILITY, self._clean_string(msg_string), self.get_log_id()) m.set_severity(severity) self.log.append(m) def console(self, msg_string, new_line=True): """ This method is used by the w3af console to print messages to the outside """ m = Message(CONSOLE, self._clean_string(msg_string), self.get_log_id()) self.log.append(m)
def __init__(self): GrepPlugin.__init__(self) self._total_count = 0 self._vuln_count = 0 self._vulns = DiskList() self._ids = DiskList()
def test_specific_serializer_with_http_response(self): # # This test runs in 26.42 seconds on my workstation # body = '<html><a href="http://moth/abc.jsp">test</a></html>' headers = Headers([('Content-Type', 'text/html')]) url = URL('http://w3af.com') response = HTTPResponse(200, body, headers, url, url, _id=1) def dump(http_response): return msgpack.dumps(http_response.to_dict(), use_bin_type=True) def load(serialized_object): data = msgpack.loads(serialized_object, raw=False) return HTTPResponse.from_dict(data) count = 30000 dl = DiskList(dump=dump, load=load) for i in xrange(0, count): # This tests the serialization dl.append(response) # This tests the deserialization _ = dl[i]
def __init__(self): GrepPlugin.__init__(self) self._total_count = 0 self._vuln_count = 0 self._vulns = DiskList(table_prefix='cache_control') self._ids = DiskList(table_prefix='cache_control')
def test_len(self): dl = DiskList() for i in xrange(0, 100): _ = dl.append(i) self.assertEqual(len(dl), 100)
def __init__(self): GrepPlugin.__init__(self) self._total_count = 0 self._vuln_count = 0 self._vulns = DiskList(table_prefix='click_jacking') self._ids = DiskList(table_prefix='click_jacking')
def test_remove_table_then_add(self): disk_list = DiskList() disk_list.append(1) disk_list.cleanup() self.assertRaises(AssertionError, disk_list.append, 1)
def test_to_unicode(self): dl = DiskList() dl.append(1) dl.append(2) dl.append(3) self.assertEqual(unicode(dl), u'<DiskList [1, 2, 3]>')
def test_slice_all(self): disk_list = DiskList() disk_list.append('1') disk_list.append('2') dl_copy = disk_list[:] self.assertIn('1', dl_copy) self.assertIn('2', dl_copy)
def test_slice_all(self): disk_list = DiskList() disk_list.append("1") disk_list.append("2") dl_copy = disk_list[:] self.assertIn("1", dl_copy) self.assertIn("2", dl_copy)
def __init__(self): """ Class init """ GrepPlugin.__init__(self) self._total_count = 0 self._vulns = DiskList() self._urls = DiskList()
def __init__(self): """ Class init """ GrepPlugin.__init__(self) self._total_count = 0 self._vulns = DiskList(table_prefix='csp') self._urls = DiskList(table_prefix='csp')
def test_slice_greater_than_length(self): disk_list = DiskList() disk_list.append('1') disk_list.append('2') dl_copy = disk_list[:50] self.assertIn('1', dl_copy) self.assertIn('2', dl_copy) self.assertEqual(2, len(dl_copy))
def test_sorted(self): dl = DiskList() dl.append("abc") dl.append("def") dl.append("aaa") sorted_dl = sorted(dl) self.assertEqual(["aaa", "abc", "def"], sorted_dl)
def test_urlobject(self): dl = DiskList() dl.append(URL('http://w3af.org/?id=2')) dl.append(URL('http://w3af.org/?id=3')) self.assertEqual(dl[0], URL('http://w3af.org/?id=2')) self.assertEqual(dl[1], URL('http://w3af.org/?id=3')) self.assertFalse(URL('http://w3af.org/?id=4') in dl) self.assertTrue(URL('http://w3af.org/?id=2') in dl)
def test_sorted(self): dl = DiskList() dl.append('abc') dl.append('def') dl.append('aaa') sorted_dl = sorted(dl) self.assertEqual(['aaa', 'abc', 'def'], sorted_dl)
def test_slice_first_N(self): disk_list = DiskList() disk_list.append("1") disk_list.append("2") disk_list.append("3") dl_copy = disk_list[:1] self.assertIn("1", dl_copy) self.assertNotIn("2", dl_copy) self.assertNotIn("3", dl_copy)
def test_remove_table(self): disk_list = DiskList() table_name = disk_list.table_name db = get_default_temp_db_instance() self.assertTrue(db.table_exists(table_name)) disk_list.cleanup() self.assertFalse(db.table_exists(table_name))
def test_slice_first_N(self): disk_list = DiskList() disk_list.append('1') disk_list.append('2') disk_list.append('3') dl_copy = disk_list[:1] self.assertIn('1', dl_copy) self.assertNotIn('2', dl_copy) self.assertNotIn('3', dl_copy)
def test_islice(self): disk_list = DiskList() disk_list.extend("ABCDEFG") EXPECTED = "CDEFG" result = "" for c in itertools.islice(disk_list, 2, None, None): result += c self.assertEqual(EXPECTED, result)
def test_getitem_negative(self): dl = DiskList() dl.append('a') dl.append('b') dl.append('c') self.assertEqual(dl[-1], 'c') self.assertEqual(dl[-2], 'b') self.assertEqual(dl[-3], 'a') self.assertRaises(IndexError, dl.__getitem__, -4)
def test_extend(self): dl = DiskList() dl.append('a') dl.extend([1, 2, 3]) self.assertEqual(len(dl), 4) self.assertEqual(dl[0], 'a') self.assertEqual(dl[1], 1) self.assertEqual(dl[2], 2) self.assertEqual(dl[3], 3)
def test_clear(self): dl = DiskList() dl.append('a') dl.append('b') self.assertEqual(len(dl), 2) dl.clear() self.assertEqual(len(dl), 0)
def test_reverse_iteration(self): dl = DiskList() dl.append(1) dl.append(2) dl.append(3) reverse_iter_res = [] for i in reversed(dl): reverse_iter_res.append(i) self.assertEqual(reverse_iter_res, [3, 2, 1])
def test_getitem_negative(self): dl = DiskList() dl.append("a") dl.append("b") dl.append("c") self.assertEqual(dl[-1], "c") self.assertEqual(dl[-2], "b") self.assertEqual(dl[-3], "a") self.assertRaises(IndexError, dl.__getitem__, -4)
def test_islice(self): disk_list = DiskList() disk_list.extend('ABCDEFG') EXPECTED = 'CDEFG' result = '' for c in itertools.islice(disk_list, 2, None, None): result += c self.assertEqual(EXPECTED, result)
def test_table_name_with_prefix(self): _unittest = 'unittest' disk_list = DiskList(_unittest) self.assertIn(_unittest, disk_list.table_name) db = get_default_temp_db_instance() self.assertTrue(db.table_exists(disk_list.table_name)) disk_list.cleanup() self.assertFalse(db.table_exists(disk_list.table_name))
def test_int(self): dl = DiskList() for i in xrange(0, 1000): _ = dl.append(i) for i in xrange(0, 1000 / 2): r = random.randint(0, 1000 - 1) self.assertEqual(r in dl, True) for i in xrange(0, 1000 / 2): r = random.randint(1000, 1000 * 2) self.assertEqual(r in dl, False)
def test_string(self): dl = DiskList() for i in xrange(0, 1000): rnd = ''.join(random.choice(string.letters) for i in xrange(40)) _ = dl.append(rnd) self.assertEqual(rnd in dl, True) for i in string.letters: self.assertNotIn(i, dl) self.assertIn(rnd, dl)
def test_no_specific_serializer_with_string(self): # # This test runs in ~5.1 seconds on my workstation # count = 30000 dl = DiskList() for i in xrange(0, count): i_str = str(i) # This tests the serialization dl.append(i_str) # This tests the deserialization _ = dl[i]
def __init__(self): OutputPlugin.__init__(self) # These attributes hold the file pointers self._file = None # User configured parameters self._file_name = '~/report.xml' self._timestamp = str(int(time.time())) self._long_timestamp = str(time.strftime(TIME_FORMAT, time.localtime())) # Set defaults for scan metadata self._plugins_dict = {} self._options_dict = {} # List with additional xml elements self._errors = DiskList() # xml document that helps with the creation of new elements # this is an empty document until we want to write to the # output file, where we populate it, serialize it to the file, # and empty it again self._xml = None
class error_pages(GrepPlugin): """ Grep every page for error pages. :author: Andres Riancho ([email protected]) """ ERROR_PAGES = ( '<H1>Error page exception</H1>', # This signature fires up also in default 404 pages of aspx which # generates a lot of noise, so ... disabling it # '<span><H1>Server Error in ', '<h2> <i>Runtime Error</i> </h2></span>', '<h2> <i>Access is denied</i> </h2></span>', '<H3>Original Exception: </H3>', 'Server object error', 'invalid literal for int()', 'exceptions.ValueError', '<font face="Arial" size=2>Type mismatch: ', '[an error occurred while processing this directive]', '<HTML><HEAD><TITLE>Error Occurred While Processing Request</TITLE>' '</HEAD><BODY><HR><H3>Error Occurred While Processing Request</H3><P>', # VBScript '<p>Microsoft VBScript runtime </font>', "<font face=\"Arial\" size=2>error '800a000d'</font>", # nwwcgi errors '<TITLE>nwwcgi Error', # ASP error I found during a pentest, the ASP used a foxpro db, not a # SQL injection '<font face="Arial" size=2>error \'800a0005\'</font>', '<h2> <i>Runtime Error</i> </h2></span>', # Some error in ASP when using COM objects. 'Operation is not allowed when the object is closed.', # An error when ASP tries to include something and it fails '<p>Active Server Pages</font> <font face="Arial" size=2>error \'ASP 0126\'</font>', # ASPX '<b> Description: </b>An unhandled exception occurred during the execution of the' ' current web request', # Struts '] does not contain handler parameter named', # PHP '<b>Warning</b>: ', 'No row with the given identifier', 'open_basedir restriction in effect', "eval()'d code</b> on line <b>", "Cannot execute a blank command in", "Fatal error</b>: preg_replace", "thrown in <b>", "#0 {main}", "Stack trace:", "</b> on line <b>", # python "PythonHandler django.core.handlers.modpython", "t = loader.get_template(template_name) # You need to create a 404.html template.", '<h2>Traceback <span>(innermost last)</span></h2>', # Java '[java.lang.', 'class java.lang.', 'java.lang.NullPointerException', 'java.rmi.ServerException', 'at java.lang.', 'onclick="toggle(\'full exception chain stacktrace\')"', 'at org.apache.catalina', 'at org.apache.coyote.', 'at org.apache.tomcat.', 'at org.apache.jasper.', # https://github.com/andresriancho/w3af/issues/4001 '<html><head><title>Application Exception</title>', # ruby '<h1 class="error_title">Ruby on Rails application could not be started</h1>', # Coldfusion '<title>Error Occurred While Processing Request</title></head><body><p></p>', '<HTML><HEAD><TITLE>Error Occurred While Processing Request</TITLE></HEAD><BODY><HR><H3>', '<TR><TD><H4>Error Diagnostic Information</H4><P><P>', '<li>Search the <a href="http://www.macromedia.com/support/coldfusion/" ' 'target="new">Knowledge Base</a> to find a solution to your problem.</li>', # http://www.programacion.net/asp/articulo/kbr_execute/ 'Server.Execute Error', # IIS '<h2 style="font:8pt/11pt verdana; color:000000">HTTP 403.6 - Forbidden: IP address rejected<br>', '<TITLE>500 Internal Server Error</TITLE>', ) _multi_in = multi_in(ERROR_PAGES) VERSION_REGEX = ( ('<address>(.*?)</address>', 'Apache'), ('<HR size="1" noshade="noshade"><h3>(.*?)</h3></body>', 'Apache Tomcat'), ('<a href="http://www.microsoft.com/ContentRedirect.asp\?prd=iis&sbp=&pver=(.*?)&pid=&ID', 'IIS'), # <b>Version Information:</b> Microsoft .NET Framework Version:1.1.4322.2300; ASP.NET Version:1.1.4322.2300 ('<b>Version Information:</b> (.*?)\n', 'ASP .NET')) _multi_re = multi_re(VERSION_REGEX) MAX_REPORTED_PER_MSG = 10 def __init__(self): GrepPlugin.__init__(self) # Internal variables self._potential_vulns = DiskList(table_prefix='error_pages') self._already_reported_max_msg_exceeded = [] self._already_reported_versions = [] self._compiled_regex = [] def grep(self, request, response): """ Plugin entry point, find the error pages and report them. :param request: The HTTP request object. :param response: The HTTP response object :return: None """ if not response.is_text_or_html(): return self.find_error_page(request, response) self.find_version_numbers(request, response) def find_error_page(self, request, response): # There is no need to report more than one info for the # same result, the user will read the info object and # analyze it even if we report it only once. If we report # it twice, he'll get mad ;) for _, _, _, url, _ in self._potential_vulns: if url == response.get_url(): return for msg in self._multi_in.query(response.body): if self._avoid_report(request, response, msg): continue # We found a new error in a response! desc = 'The URL: "%s" contains the descriptive error: "%s".' desc %= (response.get_url(), msg) title = 'Descriptive error page' data = (title, desc, response.id, response.get_url(), msg) self._potential_vulns.append(data) # Just report one instance for each HTTP response, no # matter if multiple strings match break def _avoid_report(self, request, response, msg): # We should avoid multiple reports for the same error message # the idea here is that the root cause for the same error # message might be the same, and fixing one will fix all. # # So the user receives the first report with MAX_REPORTED_PER_MSG # vulnerabilities, fixes the root cause, scans again and then # all those instances go away. # # Without this code, the scanner will potentially report # thousands of issues for the same error message. Which will # overwhelm the user. count = 0 for title, desc, _id, url, highlight in self._potential_vulns: if highlight == msg: count += 1 if count < self.MAX_REPORTED_PER_MSG: return False if msg not in self._already_reported_max_msg_exceeded: self._already_reported_max_msg_exceeded.append(msg) desc = ('The application returned multiple HTTP responses' ' containing detailed error pages containing exceptions' ' and internal information. The maximum number of' ' vulnerabilities for this issue type was reached' ' and no more issues will be reported.') i = Info('Multiple descriptive error pages', desc, [], self.get_name()) self.kb_append_uniq(self, 'error_page', i) return True def end(self): """ This method is called when the plugin wont be used anymore. """ all_findings = kb.kb.get_all_findings() for title, desc, _id, url, highlight in self._potential_vulns: for info in all_findings: # This makes sure that if the sqli plugin found a vulnerability # in the same URL as we found a detailed error, we won't report # the detailed error. # # If the user fixes the sqli vulnerability and runs the scan again # most likely the detailed error will disappear too. If the sqli # vulnerability disappears and this one remains, it will appear # as a new vulnerability in the second scan. if info.get_url() == url: break else: i = Info(title, desc, _id, self.get_name()) i.set_url(url) i.add_to_highlight(highlight) self.kb_append_uniq(self, 'error_page', i) self._potential_vulns.cleanup() def find_version_numbers(self, request, response): """ Now i'll check if I can get a version number from the error page This is common in apache, tomcat, etc... """ if 400 < response.get_code() < 600: for match, _, _, server in self._multi_re.query(response.body): match_string = match.group(0) if match_string not in self._already_reported_versions: # Save the info obj desc = 'An error page sent this %s version: "%s".' desc %= (server, match_string) i = Info('Error page with information disclosure', desc, response.id, self.get_name()) i.set_url(response.get_url()) i.add_to_highlight(server) i.add_to_highlight(match_string) kb.kb.append(self, 'server', i) kb.kb.raw_write(self, 'server', match_string) self._already_reported_versions.append(match_string) def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class LogGraph(gtk.DrawingArea, MessageConsumer): """Defines a log visualization widget that shows an XY plot :author: Facundo Batista <facundobatista =at= taniquetil.com.ar> """ def __init__(self, w3af): gtk.DrawingArea.__init__(self) MessageConsumer.__init__(self) self.w3af = w3af self.pangolayout = self.create_pango_layout("") # store all messages to be able to redraw self.all_messages = DiskList(table_prefix='gui_graph') self._need_redraw = 0 # control variables self.alreadyStopped = False self.timeGrouping = 2 self.timeBase = int(time.time() * 1000) self.realLeftMargin = MIZQ self.gc = None self._redraw_gen = None # Go live! self.connect("expose-event", self.area_expose_cb) gobject.timeout_add(500, self.draw_handler) self.show() def draw_handler(self): """ Draws the graph. """ # gtk.MAPPED: the widget can be displayed on the screen. # flags: http://pygtk.org/docs/pygtk/class-gtkobject.html#method-gtkobject--flags if self.flags() & gtk.MAPPED: if self._redraw_gen is None: self._redraw_gen = self._redraw_all() reset = self._redraw_gen.next() if reset: self._redraw_gen = None return True def handle_message(self, msg): """Adds a message to the all_messages DiskList which is then used as a source for drawing the graph. @returns: True to keep calling it, and False when all it's done. """ yield super(LogGraph, self).handle_message(msg) mmseg = int(msg.get_real_time() * 1000) mtype = msg.get_type() if mtype == "vulnerability": sever = msg.get_severity() else: sever = None self.all_messages.append((mmseg, mtype, sever)) def _redraw_all(self): """ Redraws all the graph. """ if self.gc is None: # sorry, not exposed yet... yield True # do we have enough data to start? if len(self.all_messages) < 2: yield True try: # size helpers pan = self.all_messages[-1][0] - self.all_messages[0][0] except IndexError: # We should rarely get here, note that in the bug report the # IndexError is raised in the DiskList.__getitem__ , where we're # getting the -1 and 0 indexes. According to len(self.all_messages) # those indexes exist... so... we get here on rare race conditions # # https://github.com/andresriancho/w3af/issues/4211 yield True self.window.clear() (w, h) = self.window.get_size() tspan = pan / self.timeGrouping usableWidth = w - MDER - self.realLeftMargin if tspan > usableWidth: # # Note that this line was changed from the previous (buggy line): # self.timeGrouping *= int(tspan / usableWidth) + 1 # # Which triggers https://github.com/andresriancho/w3af/issues/488 # The new line makes it impossible for self.timeGrouping to be zero # self.timeGrouping = self.timeGrouping * int(tspan / usableWidth) + 1 tspan = pan / self.timeGrouping elif tspan < usableWidth // 2 and self.timeGrouping > 2: self.timeGrouping //= 2 tspan = pan / self.timeGrouping # real left margin txts = ["", "Vulns", "Info", "", "Debug"] maxw = 0 for txt in txts: self.pangolayout.set_text(txt) (tw, th) = self.pangolayout.get_pixel_size() if tw > maxw: maxw = tw # 5 for the tick, 3 separating lm = self.realLeftMargin = int(maxw) + MIZQ + 8 # the axis self.gc.set_rgb_fg_color(colors.whitesmoke) self.window.draw_rectangle(self.gc, True, lm, MSUP, w-MDER-lm, h-MINF-MSUP) self.gc.set_rgb_fg_color(colors.black) self.window.draw_line(self.gc, lm, MSUP, lm, h-MINF+10) self.window.draw_line(self.gc, lm, h-MINF, w-MDER, h-MINF) # small horizontal ticks for x,timepoint in self._calculateXTicks(w-lm-MDER): posx = x + lm self.window.draw_line(self.gc, posx, h-MINF+5, posx, h-MINF) self.pangolayout.set_text(timepoint) (tw, th) = self.pangolayout.get_pixel_size() self.window.draw_layout(self.gc, posx-tw//2, h-MINF+10, self.pangolayout) self.pangolayout.set_text("[s]") (tw, th) = self.pangolayout.get_pixel_size() self.window.draw_layout(self.gc, w-MDER+5, h-MINF-th // 2, self.pangolayout) # small vertical ticks and texts sep = (h-MSUP-MINF) / 4 self.posHorizItems = {} self.maxItemHeight = {} posyant = MSUP for i,txt in enumerate(txts): if not txt: continue posy = int(MSUP + i*sep) self.posHorizItems[txt] = posy self.maxItemHeight[txt] = posy - posyant - 1 posyant = posy self.window.draw_line(self.gc, lm-5, posy, lm, posy) self.pangolayout.set_text(txt) (tw,th) = self.pangolayout.get_pixel_size() self.window.draw_layout(self.gc, lm-tw-8, posy-th//2, self.pangolayout) # draw the info countingPixel = 0 pixelQuant = 0 mesind = 0 while True: for (mmseg, mtype, sever) in itertools.islice(self.all_messages, mesind, None, None): mesind += 1 pixel = (mmseg - self.timeBase) // self.timeGrouping posx = self.realLeftMargin + pixel # if out of bound, restart draw if posx > (w-MDER): yield True if mtype == "debug": if pixel == countingPixel: pixelQuant += 1 else: countingPixel = pixel self._drawItem_debug(posx, pixelQuant) pixelQuant = 1 elif mtype == "information": self._drawItem_info(posx) elif mtype == "vulnerability": self._drawItem_vuln(posx, sever) yield False def _drawItem_debug(self, posx, quant): posy = self.posHorizItems["Debug"] - 1 quant = min(quant, self.maxItemHeight["Debug"]) self.gc.set_rgb_fg_color(colors.grey) self.window.draw_line(self.gc, posx, posy, posx, posy - quant) self.gc.set_rgb_fg_color(colors.black) def _drawItem_info(self, posx): posy = self.posHorizItems["Info"] self.gc.set_rgb_fg_color(colors.blue) self.window.draw_rectangle(self.gc, True, posx - 1, posy - 1, 2, 2) self.gc.set_rgb_fg_color(colors.black) def _drawItem_vuln(self, posx, sever): posy = self.posHorizItems["Vulns"] self.gc.set_rgb_fg_color(colors.red) if sever == severity.LOW: sever = 4 elif sever == severity.MEDIUM: sever = 10 else: sever = 20 self.window.draw_rectangle( self.gc, True, posx - 1, posy - sever, 2, sever) self.gc.set_rgb_fg_color(colors.black) def area_expose_cb(self, area, event): style = self.get_style() self.gc = style.fg_gc[gtk.STATE_NORMAL] self._redraw_gen = self._redraw_all() return True def _calculateXTicks(self, width): """Returns the ticks X position and time.""" step = width / 10 for i in range(10): punto = int(step * i) label = "%.2f" % (punto * self.timeGrouping / 1000) yield punto, label
class xss(AuditPlugin): """ Identify cross site scripting vulnerabilities. :author: Andres Riancho ([email protected]) :author: Taras ([email protected]) """ # TODO: Reduce the number of payloads by concatenating similar/related ones PAYLOADS = [ # 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 "\n", # Escape the HTML attribute value string delimiter '"', "'", "`", # Escape HTML attribute values without string delimiters " =", ] PAYLOADS = ['%s%s%s' % (RANDOMIZE, p, RANDOMIZE) for p in PAYLOADS] IGNORE_EXTENSIONS_FOR_PERSISTENT_XSS = set() IGNORE_EXTENSIONS_FOR_PERSISTENT_XSS.update(JAVASCRIPT) IGNORE_EXTENSIONS_FOR_PERSISTENT_XSS.update(CSS) IGNORE_EXTENSIONS_FOR_PERSISTENT_XSS.update(FLASH) IGNORE_EXTENSIONS_FOR_PERSISTENT_XSS.update(IMAGES) def __init__(self): AuditPlugin.__init__(self) self._xss_mutants = DiskList(table_prefix='xss') # User configured parameters self._check_persistent_xss = True def audit(self, freq, orig_response, debugging_id): """ Tests an URL for XSS vulnerabilities. :param freq: A FuzzableRequest :param orig_response: The HTTP response associated with the fuzzable request :param debugging_id: A unique identifier for this call to audit() """ 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 # # https://github.com/andresriancho/w3af/issues/4068 # # So I simply migrated this to a slower for loop. for fake_mutant in fake_mutants: self._check_xss_in_parameter(fake_mutant, debugging_id) def _check_xss_in_parameter(self, mutant, debugging_id): """ Tries to identify (persistent) XSS in one parameter. """ if not self._identify_trivial_xss(mutant): self._search_xss(mutant, debugging_id) 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, response.id, self.get_name(), mutant) v.add_to_highlight(mod_value) 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() trivial_mutant.set_token_value(payload) response = self._uri_opener.send_mutant(trivial_mutant, grep=True) # Add data for the persistent xss checking if self._check_persistent_xss: self._xss_mutants.append((trivial_mutant, response.id)) if self._is_json_response(response): return False if payload in response.get_body().lower(): self._report_vuln(mutant, response, payload) return True return False def _is_json_response(self, response): """ This is something I've seen in as a false positive during my assessments and is better explained in this stackoverflow question https://goo.gl/BgXVJY """ ct_options, _ = response.get_headers().iget('X-Content-Type-Options', '') content_type, _ = response.get_headers().iget('Content-Type', '') if 'application/json' in content_type and 'nosniff' in ct_options: # No luck exploiting this JSON XSS return True return False def _search_xss(self, mutant, debugging_id): """ 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(), xss_strings, fuzzable_param_list=fuzzable_params) self._send_mutants_in_threads(self._uri_opener.send_mutant, mutant_list, self._analyze_echo_result, debugging_id=debugging_id, grep=False) 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: self._xss_mutants.append((mutant, response.id)) with self._plugin_lock: if self._has_bug(mutant): return sent_payload = mutant.get_token_payload() # TODO: https://github.com/andresriancho/w3af/issues/12305 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) return def end(self): """ This method is called when the plugin wont be used anymore. """ if self._check_persistent_xss: self._identify_persistent_xss() self._xss_mutants.cleanup() 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() debugging_id = rand_alnum(8) om.out.debug('Starting stored XSS search (did=%s)' % debugging_id) self._send_mutants_in_threads( self._uri_opener.send_mutant, self._filter_out_images(fuzzable_requests), self._analyze_persistent_result, grep=False, cache=False, debugging_id=debugging_id) def _filter_out_images(self, fuzzable_requests): """ The fuzzable requests that have image extensions have a very low chance (near to none) of having a XSS payload. Filter them out of the test to reduce the number of HTTP requests sent during _identify_persistent_xss. :param fuzzable_requests: List of fuzzable requests :return: yield fuzzable requests that don't have image extensions """ for fuzzable_request in fuzzable_requests: ext = fuzzable_request.get_url().get_extension() if ext in self.IGNORE_EXTENSIONS_FOR_PERSISTENT_XSS: continue yield fuzzable_request 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. """ msg = 'Analyzing HTTP response %s to verify if XSS token was persisted' om.out.debug(msg % response.get_uri()) if self._is_json_response(response): return 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, mutant_response_id, sent_payload, fuzzable_request) break 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 = [response.id, 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(), response.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(), mutant) v['persistent'] = True v['write_payload'] = mutant v['read_payload'] = fuzzable_request v.add_to_highlight(mutant.get_token_payload()) om.out.vulnerability(v.get_desc()) 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) ol.add(o1) 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 """
class csp(GrepPlugin): """ This plugin identifies incorrect or too permissive CSP (Content Security Policy) headers returned by the web application under analysis. """ def __init__(self): """ Class init """ GrepPlugin.__init__(self) self._total_count = 0 self._vulns = DiskList() self._urls = DiskList() def get_long_desc(self): return """ This plugin identifies incorrect or too permissive CSP (Content Security Policy) headers returned by the web application under analysis. Additional information: https://www.owasp.org/index.php/Content_Security_Policy http://www.w3.org/TR/CSP """ def grep(self, request, response): """ Perform search on current HTTP request/response exchange. Store informations about vulns for further global processing. @param request: HTTP request @param response: HTTP response """ #Check that current URL has not been already analyzed response_url = str(response.get_url().uri2url()) if response_url in self._urls: return else: self._urls.append(response_url) #Search issues using dedicated module csp_vulns = find_vulns(response) #Analyze issue list if len(csp_vulns) > 0: vuln_store_item = DiskCSPVulnStoreItem(response_url, response.id, csp_vulns) self._vulns.append(vuln_store_item) #Increment the vulnerabilities counter for csp_directive_name in csp_vulns: self._total_count += len(csp_vulns[csp_directive_name]) def end(self): """ Perform global analysis for all vulnerabilities found. """ #Check if vulns has been found if self._total_count == 0: return #Parse vulns collection vuln_already_reported = [] total_url_processed_count = len(self._urls) for vuln_store_item in self._vulns: for csp_directive_name, csp_vulns_list in vuln_store_item.csp_vulns.iteritems( ): for csp_vuln in csp_vulns_list: #Check if the current vuln is common (shared) to several url processed #and has been already reported if csp_vuln.desc in vuln_already_reported: continue #Search for current vuln occurences in order to know if #the vuln is common (shared) to several url processed occurences = self._find_occurences(csp_vuln.desc) v = None if len(occurences) > 1: #Shared vuln case v = Vuln('CSP vulnerability', csp_vuln.desc, csp_vuln.severity, occurences, self.get_name()) vuln_already_reported.append(csp_vuln.desc) else: #Isolated vuln case v = Vuln('CSP vulnerability', csp_vuln.desc, csp_vuln.severity, vuln_store_item.resp_id, self.get_name()) #Report vuln self.kb_append(self, 'csp', v) #Cleanup self._urls.cleanup() self._vulns.cleanup() def _find_occurences(self, vuln_desc): """ Internal utility function to find all occurences of a vuln into the global collection of vulns found by the plugin. @param vuln_desc: Vulnerability description. @return: List of response ID for which the vuln is found. """ list_resp_id = [] #Check input for quick exit if vuln_desc is None or vuln_desc.strip() == "": return list_resp_id #Parse vulns collection ref = vuln_desc.lower().strip() for vuln_store_item in self._vulns: for csp_directive_name, csp_vulns_list in vuln_store_item.csp_vulns.iteritems( ): for csp_vuln in csp_vulns_list: if csp_vuln.desc.strip().lower() == ref: if vuln_store_item.resp_id not in list_resp_id: list_resp_id.append(vuln_store_item.resp_id) return list_resp_id
def __init__(self): GrepPlugin.__init__(self) # Internal variables self._reported = DiskList(table_prefix='path_disclosure') self._signature_re = None
class ssi(AuditPlugin): """ Find server side inclusion vulnerabilities. :author: Andres Riancho ([email protected]) """ def __init__(self): AuditPlugin.__init__(self) # Internal variables self._expected_res_mutant = DiskDict() self._freq_list = DiskList() re_str = '<!--#exec cmd="echo -n (.*?);echo -n (.*?)" -->' self._extract_results_re = re.compile(re_str) def audit(self, freq, orig_response): """ Tests an URL for server side inclusion vulnerabilities. :param freq: A FuzzableRequest """ # Create the mutants to send right now, ssi_strings = self._get_ssi_strings() mutants = create_mutants(freq, ssi_strings, orig_resp=orig_response) # Used in end() to detect "persistent SSI" for mut in mutants: expected_result = self._extract_result_from_payload( mut.get_mod_value()) self._expected_res_mutant[expected_result] = mut self._freq_list.append(freq) # End of persistent SSI setup self._send_mutants_in_threads(self._uri_opener.send_mutant, mutants, self._analyze_result) def _get_ssi_strings(self): """ This method returns a list of server sides to try to include. :return: A string, see above. """ yield '<!--#exec cmd="echo -n %s;echo -n %s" -->' % (rand_alpha(5), rand_alpha(5)) # TODO: Add mod_perl ssi injection support # http://www.sens.buffalo.edu/services/webhosting/advanced/perlssi.shtml #yield <!--#perl sub="sub {print qq/If you see this, mod_perl is working!/;}" --> def _extract_result_from_payload(self, payload): """ Extract the expected result from the payload we're sending. """ match = self._extract_results_re.search(payload) return match.group(1) + match.group(2) def _analyze_result(self, mutant, response): """ Analyze the result of the previously sent request. :return: None, save the vuln to the kb. """ if self._has_no_bug(mutant): e_res = self._extract_result_from_payload(mutant.get_mod_value()) if e_res in response and not e_res in mutant.get_original_response_body( ): desc = 'Server side include (SSI) was found at: %s' desc = desc % mutant.found_at() v = Vuln.from_mutant('Server side include vulnerability', desc, severity.HIGH, response.id, self.get_name(), mutant) v.add_to_highlight(e_res) self.kb_append_uniq(self, 'ssi', v) def end(self): """ This method is called when the plugin wont be used anymore and is used to find persistent SSI vulnerabilities. Example where a persistent SSI can be found: Say you have a "guestbook" (a CGI application that allows visitors to leave messages for everyone to see) on a server that has SSI enabled. Most such guestbooks around the Net actually allow visitors to enter HTML code as part of their comments. Now, what happens if a malicious visitor decides to do some damage by entering the following: <!--#exec cmd="ls" --> If the guestbook CGI program was designed carefully, to strip SSI commands from the input, then there is no problem. But, if it was not, there exists the potential for a major headache! For a working example please see moth VM. """ multi_in_inst = multi_in(self._expected_res_mutant.keys()) def filtered_freq_generator(freq_list): already_tested = ScalableBloomFilter() for freq in freq_list: if freq not in already_tested: already_tested.add(freq) yield freq def analyze_persistent(freq, response): for matched_expected_result in multi_in_inst.query( response.get_body()): # We found one of the expected results, now we search the # self._persistent_data to find which of the mutants sent it # and create the vulnerability mutant = self._expected_res_mutant[matched_expected_result] desc = 'Server side include (SSI) was found at: %s' \ ' The result of that injection is shown by browsing'\ ' to "%s".' desc = desc % (mutant.found_at(), freq.get_url()) v = Vuln.from_mutant( 'Persistent server side include vulnerability', desc, severity.HIGH, response.id, self.get_name(), mutant) v.add_to_highlight(matched_expected_result) self.kb_append(self, 'ssi', v) self._send_mutants_in_threads(self._uri_opener.send_mutant, filtered_freq_generator(self._freq_list), analyze_persistent, cache=False) self._expected_res_mutant.cleanup() self._freq_list.cleanup() def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class generic(AuditPlugin): """ Find all kind of bugs without using a fixed error database. :author: Andres Riancho ([email protected]) """ def __init__(self): AuditPlugin.__init__(self) # Internal variables self._potential_vulns = DiskList(table_prefix='generic') # User configured variables self._diff_ratio = 0.30 self._extensive = False def audit(self, freq, original_response): """ Find all kind of "generic" bugs without using a fixed error database :param freq: A FuzzableRequest """ # Prevent some false positives for cases where the original response # is already triggering an error if original_response.get_code() == 500: return # Get the original response and create the mutants mutants = create_mutants(freq, [ '', ], orig_resp=original_response) original_response_repeat = repeat(original_response) args_iterator = izip(original_response_repeat, mutants) check_mutant = one_to_many(self._check_mutant) self.worker_pool.imap_unordered(check_mutant, args_iterator) def _check_mutant(self, original_response, mutant): # First I check that the current modified parameter in the mutant # doesn't have an already reported vulnerability. I don't want to # report vulnerabilities more than once. if self._has_potential_vuln(mutant): return # Now, we request the limit (something that doesn't exist) # If http://localhost/a.php?b=1 # * Then I should request b=12938795 (random number) # # If http://localhost/a.php?b=abc # * Then I should request b=hnv98yks (random alnum) limit_response = self._get_limit_response(mutant) # Now I request something that could generate an error # If http://localhost/a.php?b=1 # * Then I should request b=<payload> for payload_string in self._get_payloads(): mutant.set_token_value(payload_string) error_response = self._uri_opener.send_mutant(mutant) self._analyze_responses(original_response, limit_response, error_response, mutant) def _get_payloads(self): """ :return: A payload list, size depends on the "extensive" user configured parameter. Most payloads came from [0]! [0] https://github.com/minimaxir/big-list-of-naughty-strings/ """ # This is the reduced payload set which is effective in triggering # most of the errors you'll find payloads = [ u'1/0', u'Ω≈ç√∫˜µ≤≥÷', u'<>?:"{}|_+\',./;\'[]\\-=', u'%*.*s', u'' ] # Add more payloads if the user wants to perform a detailed scan if self._extensive: payloads += [ u'undefined', u'undef', u'null', u'NULL', u'nil', u'NIL', u'true', u'false', u'True', u'False', u'None', u'-1', u'0.0/0', u'NaN', u'Infinity', u"$ENV{'HOME'}", u'00˙Ɩ$-', ] return set(payloads) def _add_potential_vuln(self, mutant, id_list): """ Stores the information about the potential vulnerability :param mutant: The mutant, containing the payload which triggered the HTTP response with the error. :param id_list: The HTTP response ids associated with the error :return: None """ self._potential_vulns.append( (mutant.get_url(), mutant.get_token_name(), mutant, id_list)) def _has_potential_vuln(self, mutant): """ :param mutant: The mutant to verify :return: True if the mutant is already tagged as a potential vuln """ for url, token_name, stored_mutant, id_list in self._potential_vulns: if mutant.get_url() != url: continue if mutant.get_token_name() != token_name: continue return True return False def _analyze_responses(self, orig_resp, limit_response, error_response, mutant): """ Analyze responses using various methods. :return: None """ for analyzer in {self._analyze_code, self._analyze_body}: is_vuln = analyzer(orig_resp, limit_response, error_response, mutant) if is_vuln: break def _analyze_code(self, orig_resp, limit_response, error_response, mutant): """ :return: True if we found a bug using the response code """ if error_response.get_code() == 500 and \ limit_response.get_code() != 500: id_list = [orig_resp.id, limit_response.id, error_response.id] self._add_potential_vuln(mutant, id_list) return True return False def _analyze_body(self, orig_resp, limit_response, error_response, mutant): """ :return: True if we found a bug by comparing the response bodies """ original_to_error = relative_distance(orig_resp.get_body(), error_response.get_body()) limit_to_error = relative_distance(limit_response.get_body(), error_response.get_body()) original_to_limit = relative_distance(limit_response.get_body(), orig_resp.get_body()) ratio = self._diff_ratio + (1 - original_to_limit) if original_to_error < ratio and limit_to_error < ratio: # Maybe the limit I requested wasn't really a non-existent one # (and the error page really found the limit), # let's request a new limit (one that hopefully doesn't exist) # in order to remove some false positives limit_response_2 = self._get_limit_response(mutant) limit_to_limit = relative_distance(limit_response_2.get_body(), limit_response.get_body()) if limit_to_limit > 1 - self._diff_ratio: # The two limits are "equal"; It's safe to suppose that we have # found the limit here and that the error string really produced # an error id_list = [orig_resp.id, limit_response.id, error_response.id] self._add_potential_vuln(mutant, id_list) def _get_limit_response(self, mutant): """ We request the limit (something that doesn't exist) - If http://localhost/a.php?b=1 then I should request b=12938795 (random number) - If http://localhost/a.php?b=abc then I should request b=hnv98yks (random alnum) :return: The limit response object """ mutant_copy = mutant.copy() is_digit = mutant.get_token_original_value().isdigit() value = rand_number(length=8) if is_digit else rand_alnum(length=8) mutant_copy.set_token_value(value) limit_response = self._uri_opener.send_mutant(mutant_copy) return limit_response def end(self): """ This method is called when the plugin wont be used anymore. """ all_findings = kb.kb.get_all_findings() for url, variable, mutant, id_list in self._potential_vulns: for info in all_findings: if info.get_token_name() == variable and info.get_url() == url: break else: desc = ('An unhandled error, which could potentially translate' ' to a vulnerability, was found at: %s') desc %= mutant.found_at() v = Vuln.from_mutant('Unhandled error in web application', desc, severity.LOW, id_list, self.get_name(), mutant) self.kb_append_uniq(self, 'generic', v) self._potential_vulns.cleanup() def get_options(self): """ :return: A list of option objects for this plugin. """ ol = OptionList() d = ('Ratio to use when comparing two HTTP response bodies, if two' ' strings have a ratio less than diff_ratio, then they are' ' really different.') o = opt_factory('diff_ratio', self._diff_ratio, d, FLOAT) ol.add(o) d = ('When enabled this plugin will send an extended payload set which' ' might trigger bugs and vulnerabilities which are not found by' ' the default (reduced, fast) payload set.') o = opt_factory('extensive', self._extensive, d, BOOL) ol.add(o) 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._diff_ratio = options_list['diff_ratio'].get_value() self._extensive = options_list['extensive'].get_value() def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class xml_file(OutputPlugin): """ Print all messages to a xml file. :author: Andres Riancho ([email protected]) """ XML_OUTPUT_VERSION = '2.4' def __init__(self): OutputPlugin.__init__(self) # User configured parameters self._file_name = '~/report.xml' self._timestamp = str(int(time.time())) self._long_timestamp = str(time.strftime(TIME_FORMAT, time.localtime())) # Set defaults for scan metadata self._plugins_dict = {} self._options_dict = {} self._scan_targets = None # Keep internal state self._is_working = False self._jinja2_env = self._get_jinja2_env() # List with additional xml elements self._errors = DiskList() def do_nothing(self, *args, **kwds): pass debug = information = vulnerability = console = log_http = do_nothing def error(self, message, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for error messages. """ # # Note that while the call to "get_caller()" is costly, it only happens # when an error occurs, so it shouldn't impact performance # error_data = (message, self.get_caller()) self._errors.append(error_data) def set_options(self, option_list): """ Sets the Options given on the OptionList to self. The options are the result of a user entering some data on a window that was constructed using the XML Options that was retrieved from the plugin using get_options() This method MUST be implemented on every plugin. :return: No value is returned. """ self._file_name = option_list['output_file'].get_value() def get_options(self): """ :return: A list of option objects for this plugin. """ ol = OptionList() d = 'Output file name where to write the XML data' o = opt_factory('output_file', self._file_name, d, OUTPUT_FILE) ol.add(o) return ol def log_enabled_plugins(self, plugins_dict, options_dict): """ This method is called from the output manager object. This method should take an action for the enabled plugins and their configuration. Usually, write the info to a file or print it somewhere. :param plugins_dict: A dict with all the plugin types and the enabled plugins for that type of plugin. :param options_dict: A dict with the options for every plugin. """ # See doc for _log_enabled_plugins_to_xml to understand why we don't write # to the XML just now. self._plugins_dict = plugins_dict self._options_dict = options_dict def end(self): """ This method is called when the scan has finished. """ self.flush() # Free some memory and disk space self._plugins_dict = {} self._options_dict = {} self._scan_targets = None self._errors.cleanup() self._jinja2_env = None def flush(self): """ Write the XML to the output file :return: None """ # Create the cache path CachedXMLNode.create_cache_path() FindingsCache.create_cache_path() # Create the context context = dotdict({}) self._add_root_info_to_context(context) self._add_scan_info_to_context(context) self._add_scan_status_to_context(context) self._add_findings_to_context(context) self._add_errors_to_context(context) # Write to file self._write_context_to_file(context) @took def _add_root_info_to_context(self, context): context.start_timestamp = self._timestamp context.start_time_long = self._long_timestamp context.xml_version = self.XML_OUTPUT_VERSION context.w3af_version = get_w3af_version.get_w3af_version() @took def _add_scan_info_to_context(self, context): if self._scan_targets is None: self._scan_targets = ','.join( [t.url_string for t in cf.cf.get('targets')]) scan_info = ScanInfo(self._jinja2_env, self._scan_targets, self._plugins_dict, self._options_dict) context.scan_info = scan_info.to_string() @took def _add_scan_status_to_context(self, context): status = self.get_w3af_core().status.get_status_as_dict() total_urls = len(kb.kb.get_all_known_fuzzable_requests()) scan_status = ScanStatus(self._jinja2_env, status, total_urls) context.scan_status = scan_status.to_string() @took def _add_errors_to_context(self, context): context.errors = self._errors def findings(self): """ A small generator that queries the findings cache and yields all the findings so they get written to the XML. :yield: Strings representing the findings as XML """ cache = FindingsCache() cached_nodes = cache.list() processed_uniq_ids = [] om.out.debug('[xml_file.flush()] Starting findings()') start = time.time() # # This for loop is a performance improvement which should yield # really good results, taking into account that get_all_uniq_ids_iter # will only query the DB and yield IDs, without doing any of the # CPU-intensive cPickle.loads() done in get_all_findings_iter() # which we do below. # # Ideally, we're only doing a cPickle.loads() once for each finding # the rest of the calls to flush() will load the finding from the # cache in this loop, and use the exclude_ids to prevent cached # entries from being queried # # What this for loop also guarantees is that we're not simply # reading all the items from the cache and putting them into the XML, # which would be incorrect because some items are modified in the # KB (which changes their uniq id) # for uniq_id in kb.kb.get_all_uniq_ids_iter(include_ids=cached_nodes): yield cache.get_node_from_cache(uniq_id) processed_uniq_ids.append(uniq_id) msg = '[xml_file.flush()] findings() processed %s cached nodes in %.2f seconds' spent = time.time() - start args = (len(processed_uniq_ids), spent) om.out.debug(msg % args) start = time.time() # # This for loop is getting all the new findings that w3af has found # In this context "new" means that the findings are not in the cache # new_findings = 0 for finding in kb.kb.get_all_findings_iter(exclude_ids=cached_nodes): uniq_id = finding.get_uniq_id() processed_uniq_ids.append(uniq_id) node = Finding(self._jinja2_env, finding).to_string() cache.save_finding_to_cache(uniq_id, node) new_findings += 1 yield node msg = '[xml_file.flush()] findings() processed %s new findings in %.2f seconds' spent = time.time() - start args = (new_findings, spent) om.out.debug(msg % args) start = time.time() # # Now that we've finished processing all the new findings we can # evict the findings that were removed from the KB from the cache # evicted_findings = 0 for cached_finding in cached_nodes: if cached_finding not in processed_uniq_ids: cache.evict_from_cache(cached_finding) evicted_findings += 1 msg = '[xml_file.flush()] findings() evicted %s findings from cache in %.2f seconds' spent = time.time() - start args = (evicted_findings, spent) om.out.debug(msg % args) @took def _add_findings_to_context(self, context): context.findings = (f for f in self.findings()) def _get_jinja2_env(self): """ Creates the jinja2 environment which will be used to render all templates The same environment is used in order to take advantage of jinja's template cache. :return: A jinja2 environment """ env_config = { 'undefined': StrictUndefined, 'trim_blocks': True, 'autoescape': True, 'lstrip_blocks': True } jinja2_env = Environment(**env_config) jinja2_env.loader = FileSystemLoader(TEMPLATE_ROOT) jinja2_env.filters['escape_attr'] = jinja2_attr_value_escape_filter jinja2_env.filters['escape_text'] = jinja2_text_value_escape_filter return jinja2_env @took def _write_context_to_file(self, context): """ Write xml report to the file by rendering the context :return: None """ template = self._jinja2_env.get_template('root.tpl') # We use streaming as explained here: # # http://flask.pocoo.org/docs/0.12/patterns/streaming/ # # To prevent having the whole XML in memory # pylint: disable=E1101 report_stream = template.stream(context) report_stream.enable_buffering(3) # pylint: enable=E1101 # Write everything to a temp file, this is useful in two cases: # # * An external tool will always see a valid XML in the output, # and not just a partially written XML document. # # * If w3af is killed in the middle of writing the XML report, # the report file will still be valid -- if xml_file.flush() was # run successfully at least once tempfh = NamedTemporaryFile(delete=False, prefix='w3af-xml-output', suffix='xml') try: # Write each report section to the temp file for report_section in report_stream: tempfh.write(report_section.encode(DEFAULT_ENCODING)) except Exception: # No exception handling is done here, we just raise the exception # so that the core can handle it properly raise else: # Close the temp file so all the content is flushed tempfh.close() # Copy to the real output file report_file_name = os.path.expanduser(self._file_name) shutil.copy(tempfh.name, report_file_name) finally: os.remove(tempfh.name) def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class cache_control(GrepPlugin): """ Grep every page for Pragma and Cache-Control headers. :author: Andres Riancho ([email protected]) """ SAFE_CONFIG = {'pragma': 'no-cache', 'cache-control': 'no-store'} def __init__(self): GrepPlugin.__init__(self) self._total_count = 0 self._vuln_count = 0 self._vulns = DiskList() self._ids = DiskList() def grep(self, request, response): if response.is_image() or response.is_swf(): return elif response.get_url().get_protocol() == 'http': return elif response.get_code() > 300\ and response.get_code() < 310: return elif response.body == '': return else: self._total_count += 1 cache_control_settings = self._get_cache_control(response) self._analyze_cache_control(cache_control_settings, response) def _get_cache_control(self, response): """ :param response: The http response we want to extract the information from. :return: A list with the headers and meta tag information used to configure the browser cache control. """ res = [] cache_control_headers = self.SAFE_CONFIG.keys() headers = response.get_headers() for _type in cache_control_headers: header_value, _ = headers.iget(_type, None) if header_value is not None: res.append(CacheSettings(_type, header_value.lower())) try: doc_parser = parser_cache.dpc.get_document_parser_for(response) except BaseFrameworkException: pass else: for meta_tag in doc_parser.get_meta_tags(): header_name = meta_tag.get('http-equiv', None) header_value = meta_tag.get('content', None) if header_name is not None and header_value is not None: header_name = header_name.lower() header_value = header_value.lower() if header_name in cache_control_headers: res.append(CacheSettings(header_name, header_value)) return res def _analyze_cache_control(self, cache_control_settings, response): """ Analyze the cache control settings set in headers and meta tags, store the information to report the vulnerabilities. """ received_headers = set() for cache_setting in cache_control_settings: expected_header = self.SAFE_CONFIG[cache_setting.type] received_header = cache_setting.value.lower() received_headers.add(cache_setting.type) if expected_header not in received_header: # The header has an incorrect value self.is_vuln(response) return if len(received_headers) != len(self.SAFE_CONFIG): # No cache control header found self.is_vuln(response) def is_vuln(self, response): self._vuln_count += 1 if response.get_url() not in self._vulns: self._vulns.append(response.get_url()) self._ids.append(response.id) def end(self): # If all URLs implement protection, don't report anything. if not self._vuln_count: return # If none of the URLs implement protection, simply report # ONE vulnerability that says that. if self._total_count == self._vuln_count: desc = 'The whole target web application has no protection (Pragma'\ ' and Cache-Control headers) against sensitive content'\ ' caching.' # If most of the URLs implement the protection but some # don't, report ONE vulnerability saying: "Most are protected, but x, y # are not. if self._total_count > self._vuln_count: desc = 'Some URLs have no protection (Pragma and Cache-Control'\ ' headers) against sensitive content caching. Among them:\n' desc += ' '.join([str(url) + '\n' for url in self._vulns]) response_ids = [_id for _id in self._ids] v = Vuln('Missing cache control for HTTPS content', desc, severity.LOW, response_ids, self.get_name()) self.kb_append_uniq(self, 'cache_control', v, 'URL') self._vulns.cleanup() self._ids.cleanup() def get_long_desc(self): return """\
class html_file(OutputPlugin): """ Generate HTML report with identified vulnerabilities and log messages. :author: Andres Riancho (([email protected])) """ def __init__(self): OutputPlugin.__init__(self) # Internal variables self._initialized = False self._additional_info = DiskList(table_prefix='html_file') self._enabled_plugins = {} self.template_root = os.path.join(ROOT_PATH, 'plugins', 'output', 'html_file', 'templates') # User configured parameters self._verbose = False self._output_file_name = './report.html' self._template = os.path.join(self.template_root, 'complete.html') def debug(self, message, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for debug messages. """ if self._verbose: to_print = self._clean_string(message) self._append_additional_info(to_print, 'debug') def do_nothing(self, *args, **kwargs): pass information = vulnerability = do_nothing def error(self, message, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for error messages. """ to_print = self._clean_string(message) self._append_additional_info(to_print, 'error') def console(self, message, new_line=True): """ This method is used by the w3af console to print messages to the outside. """ to_print = self._clean_string(message) self._append_additional_info(to_print, 'console') def _append_additional_info(self, message, msg_type): """ Add a message to the debug table. :param message: The message to add to the table. It's in HTML. :param msg_type: The type of message """ now = time.localtime(time.time()) the_time = time.strftime("%c", now) self._additional_info.append((the_time, msg_type, message)) def set_options(self, option_list): """ Sets the Options given on the OptionList to self. The options are the result of a user entering some data on a window that was constructed using the XML Options that was retrieved from the plugin using get_options() This method MUST be implemented on every plugin. :return: No value is returned. """ self._output_file_name = option_list['output_file'].get_value() self._verbose = option_list['verbose'].get_value() def get_options(self): """ :return: A list of option objects for this plugin. """ ol = OptionList() d = 'The path to the HTML template used to render the report.' o = opt_factory('template', self._template, d, INPUT_FILE) ol.add(o) d = 'File name where this plugin will write to' o = opt_factory('output_file', self._output_file_name, d, OUTPUT_FILE) ol.add(o) d = 'True if debug information will be appended to the report.' o = opt_factory('verbose', self._verbose, d, 'boolean') ol.add(o) return ol def log_enabled_plugins(self, plugins_dict, options_dict): """ This method is called from the output manager object. This method should take an action for the enabled plugins and their configuration. Usually, write the info to a file or print it somewhere. :param plugins_dict: A dict with all the plugin types and the enabled plugins for that type of plugin. :param options_dict: A dict with the options for every plugin. """ self._enabled_plugins = {} # TODO: Improve so it contains the plugin configuration too for plugin_type, enabled in plugins_dict.iteritems(): self._enabled_plugins[plugin_type] = enabled def end(self): """ This method is called when the scan has finished, we perform these main tasks: * Get the target URLs * Get the enabled plugins * Get the vulnerabilities and infos from the KB * Get the debug data * Send all the data to jinja2 for rendering the template """ target_urls = [t.url_string for t in cf.cf.get('targets')] target_domain = cf.cf.get('target_domains')[0] enabled_plugins = self._enabled_plugins findings = kb.kb.get_all_findings() debug_log = ((t, l, smart_unicode(m)) for (t, l, m) in self._additional_info) known_urls = kb.kb.get_all_known_urls() context = { 'target_urls': target_urls, 'target_domain': target_domain, 'enabled_plugins': enabled_plugins, 'findings': findings, 'debug_log': debug_log, 'known_urls': known_urls } # The file was verified to exist when setting the plugin configuration template_fh = file(os.path.expanduser(self._template), 'r') output_fh = file(os.path.expanduser(self._output_file_name), 'w') self._render_html_file(template_fh, context, output_fh) def _render_html_file(self, template_fh, context, output_fh): """ Renders the HTML file using the configured template. Separated as a method to be able to easily test. :param context: A dict containing target urls, enabled plugins, etc. :return: True on successful rendering """ severity_icon = functools.partial(get_severity_icon, self.template_root) env_config = { 'undefined': StrictUndefined, 'trim_blocks': True, 'autoescape': True, 'lstrip_blocks': True } try: jinja2_env = Environment(**env_config) except TypeError: # Kali uses a different jinja2 version, which doesn't have the same # Environment kwargs, so we first try with the version we expect # to have available, and then if it doesn't work apply this # workaround for Kali # # https://github.com/andresriancho/w3af/issues/9552 env_config.pop('lstrip_blocks') jinja2_env = Environment(**env_config) jinja2_env.filters['render_markdown'] = render_markdown jinja2_env.filters['request'] = request_dump jinja2_env.filters['response'] = response_dump jinja2_env.filters['severity_icon'] = severity_icon jinja2_env.filters['severity_text'] = get_severity_text jinja2_env.globals['get_current_date'] = get_current_date jinja2_env.loader = FileSystemLoader(self.template_root) template = jinja2_env.from_string(template_fh.read()) try: rendered_output = template.render(context) except Exception, e: msg = u'Failed to render html report template. Exception: "%s"' om.out.error(msg % e) return False finally:
class xss(AuditPlugin): """ Identify cross site scripting vulnerabilities. :author: Andres Riancho ( [email protected] ) :author: Taras ( [email protected] ) """ PAYLOADS = [ 'RANDOMIZE</->', 'RANDOMIZE/*', 'RANDOMIZE"RANDOMIZE', "RANDOMIZE'RANDOMIZE", "RANDOMIZE`", "RANDOMIZE =" ] def __init__(self): AuditPlugin.__init__(self) self._xss_mutants = DiskList() # 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, [ '', ]) # Run this in the worker pool in order to get different # parameters tested at the same time. self.worker_pool.map(self._check_xss_in_parameter, fake_mutants) def _check_xss_in_parameter(self, mutant): """ Tries to identify (persistent) XSS in one parameter. """ if not self._identify_trivial_xss(mutant): self._search_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 = 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, response.id, self.get_name(), mutant) v.add_to_highlight(mod_value) 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() trivial_mutant.set_mod_value(payload) response = self._uri_opener.send_mutant(trivial_mutant) # Add data for the persistent xss checking if self._check_persistent_xss: self._xss_mutants.append((trivial_mutant, response.id)) if payload in response.get_body(): 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] mutant_list = create_mutants(mutant.get_fuzzable_req(), xss_strings, fuzzable_param_list=[mutant.get_var()]) self._send_mutants_in_threads(self._uri_opener.send_mutant, mutant_list, self._analyze_echo_result) 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: self._xss_mutants.append((mutant, response.id)) with self._plugin_lock: if self._has_bug(mutant): return mod_value = mutant.get_mod_value() for context in get_context_iter(response.get_body(), mod_value): if context.is_executable() or context.can_break(mod_value): self._report_vuln(mutant, response, mod_value) return def end(self): """ This method is called when the plugin wont be used anymore. """ if self._check_persistent_xss: self._identify_persistent_xss() self._xss_mutants.cleanup() 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() self._send_mutants_in_threads(self._uri_opener.send_mutant, fuzzable_requests, self._analyze_persistent_result, 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. """ response_body = response.get_body() for mutant, mutant_response_id in self._xss_mutants: mod_value = mutant.get_mod_value() for context in get_context_iter(response_body, mod_value): if context.is_executable() or context.can_break(mod_value): self._report_persistent_vuln(mutant, response, mutant_response_id, mod_value, fuzzable_request) break 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 = [response.id, 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 = desc % (mod_value, mutant.get_var(), mutant.get_url(), response.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(), mutant) v['persistent'] = True v['write_payload'] = mutant v['read_payload'] = fuzzable_request v.add_to_highlight(mutant.get_mod_value()) om.out.vulnerability(v.get_desc()) 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) ol.add(o1) 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(). @parameter 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 """
class xml_file(OutputPlugin): """ Print all messages to a xml file. :author: Andres Riancho ([email protected]) """ XML_OUTPUT_VERSION = '2.8' def __init__(self): OutputPlugin.__init__(self) # User configured parameters self._file_name = '~/report.xml' self._timestamp = str(int(time.time())) self._long_timestamp = str(time.strftime(TIME_FORMAT, time.localtime())) # Set defaults for scan metadata self._plugins_dict = {} self._options_dict = {} self._scan_targets = None # Keep internal state self._is_working = False self._jinja2_env = self._get_jinja2_env() # List with additional xml elements self._errors = DiskList() def do_nothing(self, *args, **kwds): pass debug = information = vulnerability = console = log_http = do_nothing def error(self, message, new_line=True): """ This method is called from the output object. The output object was called from a plugin or from the framework. This method should take an action for error messages. """ # # Note that while the call to "get_caller()" is costly, it only happens # when an error occurs, so it shouldn't impact performance # error_data = (message, self.get_caller()) self._errors.append(error_data) def set_options(self, option_list): """ Sets the Options given on the OptionList to self. The options are the result of a user entering some data on a window that was constructed using the XML Options that was retrieved from the plugin using get_options() This method MUST be implemented on every plugin. :return: No value is returned. """ self._file_name = option_list['output_file'].get_value() def get_options(self): """ :return: A list of option objects for this plugin. """ ol = OptionList() d = 'Output file name where to write the XML data' o = opt_factory('output_file', self._file_name, d, OUTPUT_FILE) ol.add(o) return ol def log_enabled_plugins(self, plugins_dict, options_dict): """ This method is called from the output manager object. This method should take an action for the enabled plugins and their configuration. Usually, write the info to a file or print it somewhere. :param plugins_dict: A dict with all the plugin types and the enabled plugins for that type of plugin. :param options_dict: A dict with the options for every plugin. """ # See doc for _log_enabled_plugins_to_xml to understand why we don't write # to the XML just now. self._plugins_dict = plugins_dict self._options_dict = options_dict def end(self): """ This method is called when the scan has finished. """ self.flush() # Free some memory and disk space self._plugins_dict = {} self._options_dict = {} self._scan_targets = None self._errors.cleanup() self._jinja2_env = None def flush(self): """ Write the XML to the output file :return: None """ # Create the cache path CachedXMLNode.create_cache_path() FindingsCache.create_cache_path() # Create the context context = dotdict({}) try: self._add_scan_status_to_context(context) except RuntimeError, rte: # In some very strange scenarios we get this error: # # Can NOT call get_run_time before start() # # Just "ignore" this call to flush and write the XML in the next call msg = 'xml_file.flush() failed to add scan status to context: "%s"' om.out.debug(msg % rte) return self._add_root_info_to_context(context) self._add_scan_info_to_context(context) self._add_findings_to_context(context) self._add_errors_to_context(context) # Write to file self._write_context_to_file(context)
class path_disclosure(GrepPlugin): """ Grep every page for traces of path disclosure vulnerabilities. :author: Andres Riancho ([email protected]) """ def __init__(self): GrepPlugin.__init__(self) # Internal variables self._reported = DiskList(table_prefix='path_disclosure') # Compile all regular expressions and store information to avoid # multiple queries to the same function self._common_directories = get_common_directories() self._compiled_regexes = {} self._compile_regex() def _compile_regex(self): """ :return: None, the result is saved in self._path_disc_regex_list """ # # I tried to enhance the performance of this plugin by putting # all the regular expressions in one (1|2|3|4...|N) # That gave no visible result. # for path_disclosure_string in self._common_directories: regex_string = '(%s.*?)[^A-Za-z0-9\._\-\\/\+~]' regex_string = regex_string % path_disclosure_string regex = re.compile(regex_string, re.IGNORECASE) self._compiled_regexes[path_disclosure_string] = regex def _potential_disclosures(self, html_string): """ Taking into account that regular expressions are slow, we first apply this function to check if the HTML string has potential path disclosures. With this performance enhancement we reduce the plugin run time to 1/8 of the time in cases where no potential disclosures are found, and around 1/3 when potential disclosures *are* found. :return: Potential path disclosures """ for path_disclosure_string in self._common_directories: if path_disclosure_string in html_string: yield path_disclosure_string def grep(self, request, response): """ Identify the path disclosure vulnerabilities. :param request: The HTTP request object. :param response: The HTTP response object :return: None, the result is saved in the kb. """ if not response.is_text_or_html(): return vuln = self.find_path_disclosure(request, response) if vuln: self._update_kb_path_list() def find_path_disclosure(self, request, response): """ Actually find the path disclosure vulnerabilities """ html_string = response.get_body() for potential_disclosure in self._potential_disclosures(html_string): path_disc_regex = self._compiled_regexes[potential_disclosure] match_list = path_disc_regex.findall(html_string) # Sort by the longest match, this is needed for filtering out # some false positives please read the note below. match_list.sort(longest_cmp) real_url = response.get_url().url_decode() for match in match_list: # Avoid duplicated reports if (real_url, match) in self._reported: continue # Remove false positives if not self._is_false_positive(match, request, response): self._reported.append((real_url, match)) desc = 'The URL: "%s" has a path disclosure'\ ' vulnerability which discloses "%s".' desc = desc % (response.get_url(), match) v = Vuln('Path disclosure vulnerability', desc, severity.LOW, response.id, self.get_name()) v.set_url(real_url) v['path'] = match v.add_to_highlight(match) self.kb_append(self, 'path_disclosure', v) return v def _is_false_positive(self, match, request, response): """ :return: True if the match is a false positive """ # This if is to avoid false positives if request.sent(match): return True if self._is_attr_value(match, response): return True # https://github.com/andresriancho/w3af/issues/6640 url_list = kb.kb.get_all_known_urls() for url in url_list: path_and_file = url.get_path() if match == path_and_file: return True # There is a rare bug also, which is triggered in cases like this one: # # >>> import re # >>> re.findall('/var/www/.*','/var/www/foobar/htdocs/article.php') # ['/var/www/foobar/htdocs/article.php'] # >>> re.findall('/htdocs/.*','/var/www/foobar/htdocs/article.php') # ['/htdocs/article.php'] # >>> # # What I need to do here, is to keep the longest match. for real_url_reported, match_reported in self._reported: if match_reported.endswith(match): break else: # Note to self: I get here when "break" is NOT executed. # It's a new one, report! return False return True def _is_attr_value(self, path_disclosure_string, response): """ This method was created to remove some false positives. :return: True if path_disclosure_string is the value of an attribute inside a tag. Examples: path_disclosure_string = '/home/image.png' response_body = '....<img src="/home/image.png">...' return: True path_disclosure_string = '/home/image.png' response_body = '...<b>Error while checking /home/image.png</b>...' return: False """ dom = response.get_dom() if dom is None: return False for elem in dom.iterdescendants(): for key, value in elem.items(): if path_disclosure_string in value: return True return False def _update_kb_path_list(self): """ If a path disclosure was found, I can create a list of full paths to all URLs ever visited. This method updates that list. """ path_disc_vulns = kb.kb.get('path_disclosure', 'path_disclosure') url_list = kb.kb.get_all_known_urls() # Now I find the longest match between one of the URLs that w3af has # discovered, and one of the path disclosure strings that this plugin # has found. I use the longest match because with small match_list I # have more probability of making a mistake. longest_match = '' longest_path_disc_vuln = None for path_disc_vuln in path_disc_vulns: for url in url_list: path_and_file = url.get_path() if path_disc_vuln['path'].endswith(path_and_file): if len(longest_match) < len(path_and_file): longest_match = path_and_file longest_path_disc_vuln = path_disc_vuln # Now I recalculate the place where all the resources are in disk, all # this is done taking the longest_match as a reference, so... if we # don't have a longest_match, then nothing is actually done if not longest_match: return # Get the webroot webroot = longest_path_disc_vuln['path'].replace(longest_match, '') # # This if fixes a strange case reported by Olle # if webroot[0] == '/': # IndexError: string index out of range # That seems to be because the webroot == '' # if not webroot: return # Check what path separator we should use (linux / windows) path_sep = '/' if webroot.startswith('/') else '\\' # Create the remote locations remote_locations = [] for url in url_list: remote_path = url.get_path().replace('/', path_sep) remote_locations.append(webroot + remote_path) remote_locations = list(set(remote_locations)) kb.kb.raw_write(self, 'list_files', remote_locations) kb.kb.raw_write(self, 'webroot', webroot) def end(self): self._reported.cleanup() def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class generic(AuditPlugin): """ Find all kind of bugs without using a fixed database of errors. :author: Andres Riancho ([email protected]) """ ERROR_STRINGS = ['d\'kc"z\'gj\'\"**5*(((;-*`)', ''] def __init__(self): AuditPlugin.__init__(self) # Internal variables self._potential_vulns = DiskList(table_prefix='generic') # User configured variables self._diff_ratio = 0.30 def audit(self, freq, orig_response): """ Find all kind of bugs without using a fixed database of errors. :param freq: A FuzzableRequest """ # First, get the original response and create the mutants mutants = create_mutants(freq, [ '', ], orig_resp=orig_response) for m in mutants: # First I check that the current modified parameter in the mutant # doesn't have an already reported vulnerability. I don't want to # report vulnerabilities more than once. if (m.get_url(), m.get_token_name()) in self._potential_vulns: continue # Now, we request the limit (something that doesn't exist) # If http://localhost/a.php?b=1 # * Then I should request b=12938795 (random number) # # If http://localhost/a.php?b=abc # * Then I should request b=hnv98yks (random alnum) limit_response = self._get_limit_response(m) # Now I request something that could generate an error # If http://localhost/a.php?b=1 # * Then I should request b=d'kcz'gj'"**5*(((*) # # If http://localhost/a.php?b=abc # * Then I should request b=d'kcz'gj'"**5*(((*) # # I also try to trigger errors by sending empty strings # If http://localhost/a.php?b=1 ; then I should request b= # If http://localhost/a.php?b=abc ; then I should request b= for error_string in self.ERROR_STRINGS: m.set_token_value(error_string) error_response = self._uri_opener.send_mutant(m) # Now I compare responses self._analyze_responses(orig_response, limit_response, error_response, m) def _analyze_responses(self, orig_resp, limit_response, error_response, mutant): """ Analyze responses; if error_response doesn't look like orig_resp nor limit_response, then we have a vuln. :return: None """ original_to_error = relative_distance(orig_resp.get_body(), error_response.get_body()) limit_to_error = relative_distance(limit_response.get_body(), error_response.get_body()) original_to_limit = relative_distance(limit_response.get_body(), orig_resp.get_body()) ratio = self._diff_ratio + (1 - original_to_limit) if original_to_error < ratio and limit_to_error < ratio: # Maybe the limit I requested wasn't really a non-existent one # (and the error page really found the limit), # let's request a new limit (one that hopefully doesn't exist) # in order to remove some false positives limit_response2 = self._get_limit_response(mutant) id_list = [orig_resp.id, limit_response.id, error_response.id] if relative_distance( limit_response2.get_body(), limit_response.get_body()) > 1 - self._diff_ratio: # The two limits are "equal"; It's safe to suppose that we have # found the limit here and that the error string really produced # an error self._potential_vulns.append( (mutant.get_url(), mutant.get_token_name(), mutant, id_list)) def _get_limit_response(self, mutant): """ We request the limit (something that doesn't exist) - If http://localhost/a.php?b=1 then I should request b=12938795 (random number) - If http://localhost/a.php?b=abc then I should request b=hnv98yks (random alnum) :return: The limit response object """ mutant_copy = mutant.copy() is_digit = mutant.get_token_original_value().isdigit() value = rand_number(length=8) if is_digit else rand_alnum(length=8) mutant_copy.set_token_value(value) limit_response = self._uri_opener.send_mutant(mutant_copy) return limit_response def end(self): """ This method is called when the plugin wont be used anymore. """ all_findings = kb.kb.get_all_findings() for url, variable, mutant, id_list in self._potential_vulns: for info in all_findings: if info.get_token_name() == variable and info.get_url() == url: break else: desc = 'An unhandled error, which could potentially translate' \ ' to a vulnerability, was found at: %s' desc = desc % mutant.found_at() v = Vuln.from_mutant('Unhandled error in web application', desc, severity.LOW, id_list, self.get_name(), mutant) self.kb_append_uniq(self, 'generic', v) self._potential_vulns.cleanup() def get_options(self): """ :return: A list of option objects for this plugin. """ ol = OptionList() d = 'If two strings have a diff ratio less than diff_ratio, then they'\ ' are really different.' o = opt_factory('diff_ratio', self._diff_ratio, d, 'float') ol.add(o) 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._diff_ratio = options_list['diff_ratio'].get_value() def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class _LineScroller(gtk.TextView, MessageConsumer): """The text view of the Messages window. :author: Facundo Batista <facundobatista =at= taniquetil.com.ar> """ def __init__(self, scroll_bar, active_filter, possible): """ :param scroll_bar: Gtk Vertical Scrollbar object :param active_filter: the filter active at startup. :param possible: all filter keys """ gtk.TextView.__init__(self) MessageConsumer.__init__(self) self.set_editable(False) self.set_cursor_visible(False) self.set_wrap_mode(gtk.WRAP_WORD) self.textbuffer = self.get_buffer() self.show() self.possible = set(possible) self.active_filter = active_filter self.text_position = 0 self.all_messages = DiskList() # scroll bar self.freeze_scrollbar = False scroll_bar.connect("value-changed", self.scroll_changed) # colors self.textbuffer.create_tag("red-fg", foreground="red") self.textbuffer.create_tag("blue-fg", foreground="blue") self.textbuffer.create_tag("brown-fg", foreground="brown") self.bg_colors = { "vulnerability": "red-fg", "information": "blue-fg", "error": "brown-fg", } def filter(self, filtinfo): """Applies a different filter to the textview. :param filtinfo: the new filter """ self.active_filter = filtinfo textbuff = self.textbuffer textbuff.set_text("") for (mtype, text) in self.all_messages: if mtype in filtinfo: colortag = self.bg_colors[mtype] iterl = textbuff.get_end_iter() textbuff.insert_with_tags_by_name(iterl, text, colortag) self.scroll_to_end() def handle_message(self, msg): """Adds a message to the textview. :param msg: The message to add to the textview @returns: None """ yield super(_LineScroller, self).handle_message(msg) textbuff = self.textbuffer text = "[%s] %s\n" % (msg.get_time(), msg.get_msg()) mtype = msg.get_type() # only store it if it's of one of the possible filtered if mtype in self.possible: # store it self.all_messages.append((mtype, text)) antpos = self.text_position self.text_position += len(text) if mtype in self.active_filter: iterl = textbuff.get_end_iter() colortag = self.bg_colors[mtype] textbuff.insert_with_tags_by_name(iterl, text, colortag) self.scroll_to_end() def scroll_to_end(self): if not self.freeze_scrollbar: self.scroll_to_mark(self.textbuffer.get_insert(), 0) def scroll_changed(self, vscrollbar): """Handle scrollbar's "value-changed" signal. Figure out if the scroll should be frozen. If the adjustment's value is not in the last page's range => means it was moved up => the scroll bar should be stopped. """ adj = vscrollbar.get_adjustment() self.freeze_scrollbar = \ False if adj.value >= (adj.upper - adj.page_size) else True
class path_disclosure(GrepPlugin): """ Grep every page for traces of path disclosure vulnerabilities. :author: Andres Riancho ([email protected]) """ def __init__(self): GrepPlugin.__init__(self) # Internal variables self._reported = DiskList(table_prefix='path_disclosure') self._signature_re = None def setup(self): """ :return: None, the result is saved in self._path_disc_regex_list """ if self._signature_re is not None: return all_signatures = [] for path_disclosure_string in get_common_directories(): regex_string = '(%s.*?)[^A-Za-z0-9\._\-\\/\+~]' regex_string = regex_string % path_disclosure_string all_signatures.append(regex_string) self._signature_re = MultiRE(all_signatures, hint_len=1) def grep(self, request, response): """ Identify the path disclosure vulnerabilities. :param request: The HTTP request object. :param response: The HTTP response object :return: None, the result is saved in the kb. """ if not response.is_text_or_html(): return self.setup() if self.find_path_disclosure(request, response): self._update_kb_path_list() def find_path_disclosure(self, request, response): """ Actually find the path disclosure vulnerabilities """ match_list = [] body_text = response.get_body() real_url = response.get_url().url_decode() for match, _, _ in self._signature_re.query(body_text): match_list.append(match.group(1)) # Sort by the longest match, this is needed for filtering out # some false positives please read the note below. match_list.sort(longest_cmp) for match in match_list: # Avoid duplicated reports if (real_url, match) in self._reported: continue # Remove false positives if self._is_false_positive(match, request, response): continue # Found! self._reported.append((real_url, match)) desc = ('The URL: "%s" has a path disclosure vulnerability which' ' discloses "%s".') desc %= (response.get_url(), match) v = Vuln('Path disclosure vulnerability', desc, severity.LOW, response.id, self.get_name()) v.add_to_highlight(match) v.set_url(real_url) v['path'] = match self.kb_append(self, 'path_disclosure', v) return v def _is_false_positive(self, match, request, response): """ :return: True if the match is a false positive """ # This if is to avoid false positives if request.sent(match): return True # https://github.com/andresriancho/w3af/issues/6640 url_list = kb.kb.get_all_known_urls() for url in url_list: path_and_file = url.get_path() if match == path_and_file: return True # There is a rare bug also, which is triggered in cases like this one: # # >>> import re # # >>> re.findall('/var/www/.*','/var/www/foobar/htdocs/article.php') # ['/var/www/foobar/htdocs/article.php'] # # >>> re.findall('/htdocs/.*','/var/www/foobar/htdocs/article.php') # ['/htdocs/article.php'] # # What I need to do here, is to keep the longest match. for real_url_reported, match_reported in self._reported: if match_reported.endswith(match): return True # Check if the match we got is part of a tag attribute value # # This part of the function is the one that consumes the most CPU usage # thus we run it last, hoping that at least one of the methods we # implemented above tags this match as a false positive and we don't # have to run the expensive method if self._is_attr_value(match, response): return True return False def _is_attr_value(self, path_disclosure_string, response): """ This method was created to remove some false positives. This method consumes 99% of the CPU usage of the plugin, but there are only a few improvements that come to mind: * Run the code that checks if the value is in the attributes in the subprocess. The performance of this plugin will be slightly improved. * Before calling the document parser check at least it looks like the path_disclosure_string is part of an attribute value using a regular expression such as: </?\w+((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[\^'">\s]+))?)+\s*|\s*)/?> (I just need to add the path_disclosure_string somewhere there) At some point I was using a similar approach [0] but it seems that it was slow? (I doubt that it will be slower than parsing the response with lxml). Something that could be done, and given that we know that this is an HTML string is: - Find all places in the response where path_disclosure_string appears - Create 'HTTP response snippets' with the locations of path_disclosure_string +/- 500 strings. - Apply the regular expression over those strings only, avoiding the cost of applying the regex to the whole HTML response [0] https://github.com/andresriancho/w3af/commit/f1029328fcaf7e790cc317701b63954c55a3f4c8 [1] https://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx/ :return: True if path_disclosure_string is the value of an attribute inside a tag. Examples: path_disclosure_string = '/home/image.png' response_body = '....<img src="/home/image.png">...' return: True path_disclosure_string = '/home/image.png' response_body = '...<b>Error while checking /home/image.png</b>...' return: False """ for tag in mp_doc_parser.get_tags_by_filter(response, None): for value in tag.attrib.itervalues(): if path_disclosure_string in value: return True return False def _update_kb_path_list(self): """ If a path disclosure was found, I can create a list of full paths to all URLs ever visited. This method updates that list. """ path_disc_vulns = kb.kb.get('path_disclosure', 'path_disclosure') url_list = kb.kb.get_all_known_urls() # Now I find the longest match between one of the URLs that w3af has # discovered, and one of the path disclosure strings that this plugin # has found. I use the longest match because with small match_list I # have more probability of making a mistake. longest_match = '' longest_path_disc_vuln = None for path_disc_vuln in path_disc_vulns: for url in url_list: path_and_file = url.get_path() if path_disc_vuln['path'].endswith(path_and_file): if len(longest_match) < len(path_and_file): longest_match = path_and_file longest_path_disc_vuln = path_disc_vuln # Now I recalculate the place where all the resources are in disk, all # this is done taking the longest_match as a reference, so... if we # don't have a longest_match, then nothing is actually done if not longest_match: return # Get the webroot webroot = longest_path_disc_vuln['path'].replace(longest_match, '') # # This if fixes a strange case reported by Olle # if webroot[0] == '/': # IndexError: string index out of range # That seems to be because the webroot == '' # if not webroot: return # Check what path separator we should use (linux / windows) path_sep = '/' if webroot.startswith('/') else '\\' # Create the remote locations remote_locations = [] for url in url_list: remote_path = url.get_path().replace('/', path_sep) remote_locations.append(webroot + remote_path) remote_locations = list(set(remote_locations)) kb.kb.raw_write(self, 'list_files', remote_locations) kb.kb.raw_write(self, 'webroot', webroot) def end(self): self._reported.cleanup() def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """
class click_jacking(GrepPlugin): """ Grep every page for X-Frame-Options header. :author: Taras ([email protected]) """ def __init__(self): GrepPlugin.__init__(self) self._total_count = 0 self._vuln_count = 0 self._vulns = DiskList() self._ids = DiskList() def grep(self, request, response): """ TODO: need to check here for auth cookie?! """ if not response.is_text_or_html(): return self._total_count += 1 headers = response.get_lower_case_headers() x_frame_options = headers.get('x-frame-options', '') if not x_frame_options.lower() in ('deny', 'sameorigin'): self._vuln_count += 1 if response.get_url() not in self._vulns: self._vulns.append(response.get_url()) self._ids.append(response.id) def end(self): # If all URLs implement protection, don't report anything. if not self._vuln_count: return response_ids = [_id for _id in self._ids] # If none of the URLs implement protection, simply report # ONE vulnerability that says that. if self._total_count == self._vuln_count: desc = 'The whole target has no protection (X-Frame-Options'\ ' header) against Click-Jacking attacks' # If most of the URLs implement the protection but some # don't, report ONE vulnerability saying: "Most are protected, # but x, y are not. if self._total_count > self._vuln_count: desc = 'Some URLs have no protection (X-Frame-Options header) '\ 'against Click-Jacking attacks. Among them:\n '\ ' '.join([str(url) + '\n' for url in self._vulns]) v = Vuln('Click-Jacking vulnerability', desc, severity.MEDIUM, response_ids, self.get_name()) self.kb_append(self, 'click_jacking', v) self._vulns.cleanup() self._ids.cleanup() def get_long_desc(self): return """
def __init__(self): super(content_sniffing, self).__init__() self._vuln_count = 0 self._vulns = DiskList(table_prefix='content_sniffing') self._ids = DiskList(table_prefix='content_sniffing')
class xss(AuditPlugin): """ Identify cross site scripting vulnerabilities. :author: Andres Riancho ([email protected]) :author: Taras ([email protected]) """ PAYLOADS = ['<ScRipT>alert("RANDOMIZE")</ScRipT>', '<ScRipT>prompt("RANDOMIZE")</ScRipT>', '<iMg src=1 OneRRor=alert("RANDOMIZE")>', '<video><source onerror=\'javascript:prompt("RANDOMIZE")\'>', '<input onmouseover=alert("RANDOMIZE")>', '<input onmouseclick=prompt("RANDOMIZE")>', '<input onmousedown=prompt("RANDOMIZE")>', '<input onmouseup=alert("RANDOMIZE")>', '<input onload=alert("RANDOMIZE")>', '<body onload=alert("RANDOMIZE")>', '<img src="javascript:alert(\'RANDOMIZE\');">', '<body background="javascript:alert(\'RANDOMIZE\');">', '<div style="height:expression(alert(\'RANDOMIZE\'),1)"/>', '<img dynsrc="javascript:alert(\'RANDOMIZE\');">', '<img lowsrc="javascript:alert(\'RANDOMIZE\');">', '<br size="&{alert("RANDOMIZE")}">', '<link rel="stylesheet" herf="javascript:alert(\'RANDOMIZE\');>', '<iframe src="javascript:alert(\'RANDOMIZE")\'>', '<table background="javascript:alert(\'RANDOMIZE\')">', '><ScRipT>alert("RANDOMIZE")</ScRipT>', '><ScRipT>prompt("RANDOMIZE")</ScRipT>', '><iMg src=1 OneRRor=alert("RANDOMIZE")>', '></iframe><iMg src=1 OneRRor=alert("RANDOMIZE")>', '><video><source onerror=\'javascript:prompt("RANDOMIZE")\'>', '><input onmouseover=alert("RANDOMIZE")>', '><input onmouseclick=prompt("RANDOMIZE")>', '><input onmousedown=prompt("RANDOMIZE")>', '><input onmouseup=alert("RANDOMIZE")>', '><input onload=alert("RANDOMIZE")>', '><body onload=alert("RANDOMIZE")>', '><img src="javascript:alert(\'RANDOMIZE\');">', '><body background="javascript:alert(\'RANDOMIZE\');">', '><div style="height:expression(alert(\'RANDOMIZE\'),1)"/>', '><img dynsrc="javascript:alert(\'RANDOMIZE\');">', '><img lowsrc="javascript:alert(\'RANDOMIZE\');">', '><br size="&{alert("RANDOMIZE")}">', '><link rel="stylesheet" herf="javascript:alert(\'RANDOMIZE\');>', '><iframe src="javascript:alert(\'RANDOMIZE")\'>', '><table background="javascript:alert(\'RANDOMIZE\')">', '\'><ScRipT>alert("RANDOMIZE")</ScRipT>', '\'><ScRipT>prompt("RANDOMIZE")</ScRipT>', '\'><iMg src=1 OneRRor=alert("RANDOMIZE")>', '\'><video><source onerror=\'javascript:prompt("RANDOMIZE")\'>', '\'><input onmouseover=alert("RANDOMIZE")>', '\'><input onmouseclick=prompt("RANDOMIZE")>', '\'><input onmousedown=prompt("RANDOMIZE")>', '\'><input onmouseup=alert("RANDOMIZE")>', '\'><input onload=alert("RANDOMIZE")>' '\'><body onload=alert("RANDOMIZE")>', '\'><img src="javascript:alert(\'RANDOMIZE\');">', '\'><body background="javascript:alert(\'RANDOMIZE\');">', '\'><div style="height:expression(alert(\'RANDOMIZE\'),1)"/>', '\'><img dynsrc="javascript:alert(\'RANDOMIZE\');">', '\'><img lowsrc="javascript:alert(\'RANDOMIZE\');">', '\'><br size="&{alert("RANDOMIZE")}">', '\'><link rel="stylesheet" herf="javascript:alert(\'RANDOMIZE\');>', '\'><iframe src="javascript:alert(\'RANDOMIZE")\'>', '\'><table background="javascript:alert(\'RANDOMIZE\')">', '"><ScRipT>alert("RANDOMIZE")</ScRipT>', '"><ScRipT>prompt("RANDOMIZE")</ScRipT>', '"><iMg src=1 OneRRor=alert("RANDOMIZE")>', '"><video><source onerror=\'javascript:prompt("RANDOMIZE")\'>', '"><input onmouseover=alert("RANDOMIZE")>', '"><input onmouseclick=prompt("RANDOMIZE")>', '"><input onmousedown=prompt("RANDOMIZE")>', '"><input onmouseup=alert("RANDOMIZE")>', '"><input onload=alert("RANDOMIZE")>', '"><body onload=alert("RANDOMIZE")>', '"><img src="javascript:alert(\'RANDOMIZE\');">', '"><body background="javascript:alert(\'RANDOMIZE\');">', '"><div style="height:expression(alert(\'RANDOMIZE\'),1)"/>', '"><img dynsrc="javascript:alert(\'RANDOMIZE\');">', '"><img lowsrc="javascript:alert(\'RANDOMIZE\');">', '"><br size="&{alert("RANDOMIZE")}">', '"><link rel="stylesheet" herf="javascript:alert(\'RANDOMIZE\');>', '"><iframe src="javascript:alert(\'RANDOMIZE")\'>', '"><table background="javascript:alert(\'RANDOMIZE\')">' # '+/v8\r\n+ADw-ScRipT+AD4-alert(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n+ADw-ScRipT+AD4-prompt(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n+ADw-iMg src+AD0-1 OneRRor+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-video+AD4APA-source onerror+AD0\'javascript:prompt(+ACI-RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n+ADw-input onmouseover+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-input onmouseclick+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-input onmousedown+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-input onmouseup+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-input onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-body onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ADw-img src+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ADw-body background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ADw-div style+AD0AIg-height:expression(alert(\'RANDOMIZE\'),1)+ACI-/+AD4-', # '+/v8\r\n+ADw-img dynsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ADw-img lowsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ADw-br size+AD0AIgAmAHs-alert(+ACI-RANDOMIZE+ACI)+AH0AIgA+-', # '+/v8\r\n+ADw-link rel+AD0AIg-stylesheet+ACI herf+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAPg-', # '+/v8\r\n+ADw-iframe src+AD0AIg-javascript:alert(\'RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n+ADw-table background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ACIAPg-', # '+/v8\r\n+AD4APA-ScRipT+AD4-alert(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n+AD4APA-ScRipT+AD4-prompt(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n+AD4APA-iMg src+AD0-1 OneRRor+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+AD4APA-video+AD4APA-source onerror+AD0\'javascript:prompt(+ACI-RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n+AD4APA-input onmouseover+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+AD4APA-input onmouseclick+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+AD4APA-input onmousedown+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+AD4APA-input onmouseup+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+AD4APA-input onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4', # '+/v8\r\n+AD4APA-body onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+AD4APA-img src+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+AD4APA-body background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+AD4APA-div style+AD0AIg-height:expression(alert(\'RANDOMIZE\'),1)+ACI-/+AD4-', # '+/v8\r\n+AD4APA-img dynsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+AD4APA-img lowsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+AD4APA-br size+AD0AIgAmAHs-alert(+ACI-RANDOMIZE+ACI)+AH0AIgA+-', # '+/v8\r\n+AD4APA-link rel+AD0AIg-stylesheet+ACI herf+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAPg-', # '+/v8\r\n+AD4APA-iframe src+AD0AIg-javascript:alert(\'RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n+AD4APA-table background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ACIAPg-', # '+/v8\r\n\'+AD4APA-ScRipT+AD4-alert(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n\'+AD4APA-ScRipT+AD4-prompt(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n\'+AD4APA-iMg src+AD0-1 OneRRor+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n\'+AD4APA-video+AD4APA-source onerror+AD0\'javascript:prompt(+ACI-RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n\'+AD4APA-input onmouseover+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n\'+AD4APA-input onmouseclick+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n\'+AD4APA-input onmousedown+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n\'+AD4APA-input onmouseup+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n\'+AD4APA-input onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4', # '+/v8\r\n\'+AD4APA-body onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n\'+AD4APA-img src+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n\'+AD4APA-body background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n\'+AD4APA-div style+AD0AIg-height:expression(alert(\'RANDOMIZE\'),1)+ACI-/+AD4-', # '+/v8\r\n\'+AD4APA-img dynsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n\'+AD4APA-img lowsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n\'+AD4APA-br size+AD0AIgAmAHs-alert(+ACI-RANDOMIZE+ACI)+AH0AIgA+-', # '+/v8\r\n\'+AD4APA-link rel+AD0AIg-stylesheet+ACI herf+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAPg-', # '+/v8\r\n\'+AD4APA-iframe src+AD0AIg-javascript:alert(\'RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n\'+AD4APA-table background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ACIAPg-', # '+/v8\r\n+ACIAPgA8-ScRipT+AD4-alert(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n+ACIAPgA8-ScRipT+AD4-prompt(+ACI-RANDOMIZE+ACI)+ADw-/ScRipT+AD4-', # '+/v8\r\n+ACIAPgA8-iMg src+AD0-1 OneRRor+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ACIAPgA8-video+AD4APA-source onerror+AD0\'javascript:prompt(+ACI-RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n+ACIAPgA8-input onmouseover+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ACIAPgA8-input onmouseclick+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ACIAPgA8-input onmousedown+AD0-prompt(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ACIAPgA8-input onmouseup+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ACIAPgA8-input onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4', # '+/v8\r\n+ACIAPgA8-body onload+AD0-alert(+ACI-RANDOMIZE+ACI)+AD4-', # '+/v8\r\n+ACIAPgA8-img src+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ACIAPgA8-body background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ACIAPgA8-div style+AD0AIg-height:expression(alert(\'RANDOMIZE\'),1)+ACI-/+AD4-', # '+/v8\r\n+ACIAPgA8-img dynsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ACIAPgA8-img lowsrc+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAIgA+-', # '+/v8\r\n+ACIAPgA8-br size+AD0AIgAmAHs-alert(+ACI-RANDOMIZE+ACI)+AH0AIgA+-', # '+/v8\r\n+ACIAPgA8-link rel+AD0AIg-stylesheet+ACI herf+AD0AIg-javascript:alert(\'RANDOMIZE\')+ADsAPg-', # '+/v8\r\n+ACIAPgA8-iframe src+AD0AIg-javascript:alert(\'RANDOMIZE+ACI)\'+AD4-', # '+/v8\r\n+ACIAPgA8-table background+AD0AIg-javascript:alert(\'RANDOMIZE\')+ACIAPg-' ] def __init__(self): AuditPlugin.__init__(self) self._xss_mutants = DiskList(table_prefix='xss') # User configured parameters self._check_persistent_xss = False 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 # # https://github.com/andresriancho/w3af/issues/4068 # # So I simply migrated this to a slower for loop. for fake_mutant in fake_mutants: self._check_xss_in_parameter(fake_mutant) def _check_xss_in_parameter(self, mutant): """ Tries to identify (persistent) XSS in one parameter. """ if not self._identify_trivial_xss(mutant): self._search_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, response.id, self.get_name(), mutant) v.add_to_highlight(mod_value) 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() trivial_mutant.set_token_value(payload) response = self._uri_opener.send_mutant(trivial_mutant) # Add data for the persistent xss checking if self._check_persistent_xss: self._xss_mutants.append((trivial_mutant, response.id)) # This is something I've seen in as a false positive during my # assessments and is better explained in this stackoverflow question # https://goo.gl/BgXVJY 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(), xss_strings, fuzzable_param_list=fuzzable_params) self._send_mutants_in_threads(self._uri_opener.send_mutant, mutant_list, self._analyze_echo_result) 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: self._xss_mutants.append((mutant, response.id)) with self._plugin_lock: if self._has_bug(mutant): return sent_payload = mutant.get_token_payload() # TODO: https://github.com/andresriancho/w3af/issues/12305 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) return def end(self): """ This method is called when the plugin wont be used anymore. """ if self._check_persistent_xss: self._identify_persistent_xss() self._xss_mutants.cleanup() 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() self._send_mutants_in_threads(self._uri_opener.send_mutant, fuzzable_requests, self._analyze_persistent_result, 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, mutant_response_id, sent_payload, fuzzable_request) break 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 = [response.id, 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(), response.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(), mutant) v['persistent'] = True v['write_payload'] = mutant v['read_payload'] = fuzzable_request v.add_to_highlight(mutant.get_token_payload()) om.out.vulnerability(v.get_desc()) 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) ol.add(o1) 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 """
def test_not(self): dl = DiskList() self.assertFalse(dl)
class path_disclosure(GrepPlugin): """ Grep every page for traces of path disclosure vulnerabilities. :author: Andres Riancho ([email protected]) """ def __init__(self): GrepPlugin.__init__(self) # Internal variables self._reported = DiskList(table_prefix='path_disclosure') self._signature_re = None def setup(self): """ :return: None, the result is saved in self._path_disc_regex_list """ if self._signature_re is not None: return all_signatures = [] for path_disclosure_string in get_common_directories(): regex_string = '(%s.*?)[^A-Za-z0-9\._\-\\/\+~]' regex_string = regex_string % path_disclosure_string all_signatures.append(regex_string) self._signature_re = multi_re(all_signatures, hint_len=1) def grep(self, request, response): """ Identify the path disclosure vulnerabilities. :param request: The HTTP request object. :param response: The HTTP response object :return: None, the result is saved in the kb. """ if not response.is_text_or_html(): return self.setup() if self.find_path_disclosure(request, response): self._update_kb_path_list() def find_path_disclosure(self, request, response): """ Actually find the path disclosure vulnerabilities """ body_text = response.get_body() match_list = [] for match, _, _ in self._signature_re.query(body_text): match_list.append(match.group(1)) # Sort by the longest match, this is needed for filtering out # some false positives please read the note below. match_list.sort(longest_cmp) real_url = response.get_url().url_decode() for match in match_list: # Avoid duplicated reports if (real_url, match) in self._reported: continue # Remove false positives if self._is_false_positive(match, request, response): continue # Found! self._reported.append((real_url, match)) desc = ('The URL: "%s" has a path disclosure vulnerability which' ' discloses "%s".') desc %= (response.get_url(), match) v = Vuln('Path disclosure vulnerability', desc, severity.LOW, response.id, self.get_name()) v.add_to_highlight(match) v.set_url(real_url) v['path'] = match self.kb_append(self, 'path_disclosure', v) return v def _is_false_positive(self, match, request, response): """ :return: True if the match is a false positive """ # This if is to avoid false positives if request.sent(match): return True if self._is_attr_value(match, response): return True # https://github.com/andresriancho/w3af/issues/6640 url_list = kb.kb.get_all_known_urls() for url in url_list: path_and_file = url.get_path() if match == path_and_file: return True # There is a rare bug also, which is triggered in cases like this one: # # >>> import re # >>> re.findall('/var/www/.*','/var/www/foobar/htdocs/article.php') # ['/var/www/foobar/htdocs/article.php'] # >>> re.findall('/htdocs/.*','/var/www/foobar/htdocs/article.php') # ['/htdocs/article.php'] # >>> # # What I need to do here, is to keep the longest match. for real_url_reported, match_reported in self._reported: if match_reported.endswith(match): break else: # Note to self: I get here when "break" is NOT executed. # It's a new one, report! return False return True def _is_attr_value(self, path_disclosure_string, response): """ This method was created to remove some false positives. :return: True if path_disclosure_string is the value of an attribute inside a tag. Examples: path_disclosure_string = '/home/image.png' response_body = '....<img src="/home/image.png">...' return: True path_disclosure_string = '/home/image.png' response_body = '...<b>Error while checking /home/image.png</b>...' return: False """ for tag in mp_doc_parser.get_tags_by_filter(response, None): for value in tag.attrib.itervalues(): if path_disclosure_string in value: return True return False def _update_kb_path_list(self): """ If a path disclosure was found, I can create a list of full paths to all URLs ever visited. This method updates that list. """ path_disc_vulns = kb.kb.get('path_disclosure', 'path_disclosure') url_list = kb.kb.get_all_known_urls() # Now I find the longest match between one of the URLs that w3af has # discovered, and one of the path disclosure strings that this plugin # has found. I use the longest match because with small match_list I # have more probability of making a mistake. longest_match = '' longest_path_disc_vuln = None for path_disc_vuln in path_disc_vulns: for url in url_list: path_and_file = url.get_path() if path_disc_vuln['path'].endswith(path_and_file): if len(longest_match) < len(path_and_file): longest_match = path_and_file longest_path_disc_vuln = path_disc_vuln # Now I recalculate the place where all the resources are in disk, all # this is done taking the longest_match as a reference, so... if we # don't have a longest_match, then nothing is actually done if not longest_match: return # Get the webroot webroot = longest_path_disc_vuln['path'].replace(longest_match, '') # # This if fixes a strange case reported by Olle # if webroot[0] == '/': # IndexError: string index out of range # That seems to be because the webroot == '' # if not webroot: return # Check what path separator we should use (linux / windows) path_sep = '/' if webroot.startswith('/') else '\\' # Create the remote locations remote_locations = [] for url in url_list: remote_path = url.get_path().replace('/', path_sep) remote_locations.append(webroot + remote_path) remote_locations = list(set(remote_locations)) kb.kb.raw_write(self, 'list_files', remote_locations) kb.kb.raw_write(self, 'webroot', webroot) def end(self): self._reported.cleanup() def get_long_desc(self): """ :return: A DETAILED description of the plugin functions and features. """ return """