Exemplo n.º 1
0
    def test_special_char(self):
        re_list = [u'\x00\x01\x02\x03']
        mre = MultiRE(re_list)

        result = to_list(mre.query('abc\x00\x01\x02\x03def'))
        self.assertEqual(1, len(result))
        self.assertEqual('\x00\x01\x02\x03', result[0][1])
Exemplo n.º 2
0
    def test_special_char(self):
        re_list = [u'\x00\x01\x02\x03']
        mre = MultiRE(re_list)

        result = to_list(mre.query('abc\x00\x01\x02\x03def'))
        self.assertEqual(1, len(result))
        self.assertEqual('\x00\x01\x02\x03', result[0][1])
Exemplo n.º 3
0
    def test_re_flags(self):
        re_list = ['12.*3456', 'ab.*cdef']
        mre = MultiRE(re_list, re.IGNORECASE)

        result = to_list(mre.query('AB3Cdef'))
        self.assertEqual(1, len(result))
        self.assertEqual('ab.*cdef', result[0][1])
Exemplo n.º 4
0
    def setup(self):
        with self._plugin_lock:
            if self._signature_re is not None:
                return

            signatures = self._read_signatures()
            self._signature_re = MultiRE(signatures, hint_len=2)
Exemplo n.º 5
0
    def test_re_flags(self):
        re_list = ['12.*3456', 'ab.*cdef']
        mre = MultiRE(re_list, re.IGNORECASE)

        result = to_list(mre.query('AB3Cdef'))
        self.assertEqual(1, len(result))
        self.assertEqual('ab.*cdef', result[0][1])
Exemplo n.º 6
0
    def test_unicode_re(self):
        re_list = [u'ñandú', u'ýandex']
        mre = MultiRE(re_list)

        result = to_list(mre.query('abcn'))
        self.assertEqual(0, len(result))
        self.assertEqual([], result)

        result = to_list(mre.query('123 ñandú 345'))
        self.assertEqual(1, len(result))
        self.assertEqual('ñandú', result[0][1])
Exemplo n.º 7
0
    def test_unicode_re(self):
        re_list = [u'ñandú', u'ýandex']
        mre = MultiRE(re_list)

        result = to_list(mre.query('abcn'))
        self.assertEqual(0, len(result))
        self.assertEqual([], result)

        result = to_list(mre.query('123 ñandú 345'))
        self.assertEqual(1, len(result))
        self.assertEqual('ñandú', result[0][1])
Exemplo n.º 8
0
    def test_simplest(self):
        re_list = ['1234', '4567', '7890']
        mre = MultiRE(re_list)

        result = to_list(mre.query('4567'))
        self.assertEqual(1, len(result))
        self.assertEqual('4567', result[0][1])

        result = to_list(mre.query('7890'))
        self.assertEqual(1, len(result))
        self.assertEqual('7890', result[0][1])
Exemplo n.º 9
0
    def test_simplest(self):
        re_list = ['1234', '4567', '7890']
        mre = MultiRE(re_list)

        result = to_list(mre.query('4567'))
        self.assertEqual(1, len(result))
        self.assertEqual('4567', result[0][1])

        result = to_list(mre.query('7890'))
        self.assertEqual(1, len(result))
        self.assertEqual('7890', result[0][1])
Exemplo n.º 10
0
    def test_unicode_query(self):
        re_list = [u'abc321', u'def123']
        mre = MultiRE(re_list)

        result = to_list(mre.query('abc321ñ'))
        self.assertEqual(1, len(result))
        self.assertEqual('abc321', result[0][1])

        result = to_list(mre.query('abc321\x00def123'))
        self.assertEqual(2, len(result))
        match_res = set(i[1] for i in result)
        self.assertEqual(set(re_list), match_res)
Exemplo n.º 11
0
    def test_unicode_query(self):
        re_list = [u'abc321', u'def123']
        mre = MultiRE(re_list)

        result = to_list(mre.query('abc321ñ'))
        self.assertEqual(1, len(result))
        self.assertEqual('abc321', result[0][1])

        result = to_list(mre.query('abc321\x00def123'))
        self.assertEqual(2, len(result))
        match_res = set(i[1] for i in result)
        self.assertEqual(set(re_list), match_res)
Exemplo n.º 12
0
    def test_re(self):
        re_list = ['1234.*56', 'ab.*cdef']
        mre = MultiRE(re_list)
        result = to_list(mre.query('456'))
        self.assertEqual(0, len(result))
        self.assertEqual([], result)

        result = to_list(mre.query('1234a56'))
        self.assertEqual(1, len(result))
        self.assertEqual('1234.*56', result[0][1])

        result = to_list(mre.query('abAAAcdef'))
        self.assertEqual(1, len(result))
        self.assertEqual('ab.*cdef', result[0][1])
Exemplo n.º 13
0
    def test_re(self):
        re_list = ['1234.*56', 'ab.*cdef']
        mre = MultiRE(re_list)
        result = to_list(mre.query('456'))
        self.assertEqual(0, len(result))
        self.assertEqual([], result)

        result = to_list(mre.query('1234a56'))
        self.assertEqual(1, len(result))
        self.assertEqual('1234.*56', result[0][1])

        result = to_list(mre.query('abAAAcdef'))
        self.assertEqual(1, len(result))
        self.assertEqual('ab.*cdef', result[0][1])
Exemplo n.º 14
0
    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)
Exemplo n.º 15
0
    def test_re_with_obj(self):
        re_list = [('1234.*56', None, None), ('ab.*cdef', 1, 2)]
        mre = MultiRE(re_list)

        result = to_list(mre.query('1234A56'))
        self.assertEqual(1, len(result))
        self.assertEqual('1234.*56', result[0][1])
        self.assertEqual(None, result[0][3])
        self.assertEqual(None, result[0][4])

        result = to_list(mre.query('abAAAcdef'))
        self.assertEqual(1, len(result))
        self.assertEqual('ab.*cdef', result[0][1])
        self.assertEqual(1, result[0][3])
        self.assertEqual(2, result[0][4])
Exemplo n.º 16
0
    def test_re_with_obj(self):
        re_list = [('1234.*56', None, None), ('ab.*cdef', 1, 2)]
        mre = MultiRE(re_list)

        result = to_list(mre.query('1234A56'))
        self.assertEqual(1, len(result))
        self.assertEqual('1234.*56', result[0][1])
        self.assertEqual(None, result[0][3])
        self.assertEqual(None, result[0][4])

        result = to_list(mre.query('abAAAcdef'))
        self.assertEqual(1, len(result))
        self.assertEqual('ab.*cdef', result[0][1])
        self.assertEqual(1, result[0][3])
        self.assertEqual(2, result[0][4])
Exemplo n.º 17
0
    def setup(self):
        with self._plugin_lock:
            if self._signature_re is not None:
                return

            signatures = self._read_signatures()
            self._signature_re = MultiRE(signatures, hint_len=2)
Exemplo n.º 18
0
    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)
Exemplo n.º 19
0
    def update_vulners_rules(self):
        """
        Get fresh rules from Vulners Github. The rules are regular expressions
        which are used to extract information from the HTTP response.

        Rules can be found at vulners github repository. They were not included
        into the w3af repository because of licensing incompatibilities.

        Paranoid? Check gitlog and regexes.
        """
        # w3af grep plugins shouldn't (by definition) perform HTTP requests
        # But in this case we're breaking that general rule to retrieve the
        # DB at the beginning of the scan
        try:
            http_response = self._uri_opener.GET(self._vulners_rules_url,
                                                 binary_response=True,
                                                 respect_size_limit=False)
        except Exception as e:
            msg = 'Failed to download Vulners regex rules table: "%s"'
            om.out.error(msg % e)
            return

        if http_response.get_code() != 200:
            msg = (
                'Failed to download the Vulners regex rules table, unexpected'
                ' HTTP response code %s')
            om.out.error(msg % http_response.get_code())
            return

        json_table = http_response.get_raw_body()
        self.rules_table = json.loads(json_table)

        # Adapt it for MultiRe structure [(regex,alias)] removing regex duplicated
        regex_aliases = collections.defaultdict(list)
        for software_name in self.rules_table:
            regex_aliases[self.rules_table[software_name].get('regex')] += [
                software_name
            ]

        # Now create fast RE filter
        # Using re.IGNORECASE because w3af is modifying headers when making RAW dump.
        # Why so? Raw must be raw!
        self._multi_re = MultiRE(((regex, regex_aliases.get(regex))
                                  for regex in regex_aliases), re.IGNORECASE)
Exemplo n.º 20
0
    def test_dup(self):
        re_list = ['1234', '4567']
        mre = MultiRE(re_list)

        result = to_list(mre.query('4567 4567'))
        self.assertEqual(1, len(result))
Exemplo n.º 21
0
    ('(^|\W)import java\.', {JAVA}),
    ('(^|\W)public class \w{1,60}\s?\{\s.*\Wpublic', {JAVA}),
    ('(^|\W)package\s\w+\;', {JAVA}),
    ('<!--g:render', {GROOVY}),

    # Python
    ('(^|\W)def .*?\(.*?\):(\n|\r)', {PYTHON}),

    # Ruby
    ('(^|\W)class \w{1,60}\s*<?\s*[a-zA-Z0-9_:]{0,90}.*?\W(def|validates)\s.*?\send($|\W)',
     {RUBY}),
)

BLACKLIST = {'xml', 'xpacket'}

_multi_re = MultiRE(SOURCE_CODE, re.IGNORECASE | re.DOTALL, hint_len=2)
assert _multi_re._regexes_with_no_keywords == [], 'Performance issue in MultiRE'


def contains_source_code(http_response):
    """
    :param http_response: The HTTP response object
    :return: A tuple with:
                - re.match object if the file_content matches a source code file
                - A tuple containing the programming language names
    """
    body = http_response.get_body()

    for match, _, _, lang in _multi_re.query(body):

        if is_false_positive(http_response, match, lang):
Exemplo n.º 22
0
    def test_short(self):
        re_list = ['12.?34']
        mre = MultiRE(re_list)

        result = to_list(mre.query('12X34'))
        self.assertEqual(1, len(result))
Exemplo n.º 23
0
class find_backdoors(CrawlPlugin):
    """
    Find web backdoors and web shells.

    :author: Andres Riancho ([email protected])
    """
    WEBSHELL_DB = os.path.join(CRAWL_PATH, 'find_backdoors', 'web_shells.txt')
    SIGNATURE_DB = os.path.join(CRAWL_PATH, 'find_backdoors', 'signatures.txt')

    def __init__(self):
        CrawlPlugin.__init__(self)

        # Internal variables
        self._analyzed_dirs = ScalableBloomFilter()
        self._signature_re = None

    def setup(self):
        with self._plugin_lock:
            if self._signature_re is not None:
                return

            signatures = self._read_signatures()
            self._signature_re = MultiRE(signatures, hint_len=2)

    def _read_signatures(self):
        for line in file(self.SIGNATURE_DB):
            line = line.strip()

            if not line:
                continue

            if line.startswith('#'):
                continue

            yield (line, 'Backdoor signature')

    def crawl(self, fuzzable_request, debugging_id):
        """
        For every directory, fetch a list of shell files and analyze the
        response.

        :param debugging_id: A unique identifier for this call to discover()
        :param fuzzable_request: A fuzzable_request instance that contains
                                    (among other things) the URL to test.
        """
        domain_path = fuzzable_request.get_url().get_domain_path()

        if domain_path in self._analyzed_dirs:
            return

        self._analyzed_dirs.add(domain_path)

        self.setup()

        # Read the web shell database
        web_shells = self._iter_web_shells()

        # Send the requests using threads:
        args_iter = (domain_path.url_join(fname) for fname in web_shells)
        self.worker_pool.map(self._check_if_exists, args_iter)

    def _iter_web_shells(self):
        """
        :yield: lines from the web shell DB
        """
        for line in file(self.WEBSHELL_DB):
            line = line.strip()

            if line.startswith('#'):
                continue

            if not line:
                continue

            yield line

    def _check_if_exists(self, web_shell_url):
        """
        Check if the file exists.

        :param web_shell_url: The URL to check
        """
        try:
            response = self._uri_opener.GET(web_shell_url, cache=True)
        except BaseFrameworkException:
            om.out.debug('Failed to GET webshell: %s' % web_shell_url)
            return

        signature = self._match_signature(response)
        if signature is None:
            return

        desc = (u'An HTTP response matching the web backdoor signature'
                u' "%s" was found at: "%s"; this could indicate that the'
                u' server has been compromised.')
        desc %= (signature, response.get_url())

        # It's probability is higher if we found a long signature
        _severity = severity.HIGH if len(signature) > 8 else severity.MEDIUM

        v = Vuln(u'Potential web backdoor', desc, _severity, response.id,
                 self.get_name())
        v.set_url(response.get_url())

        kb.kb.append(self, 'backdoors', v)
        om.out.vulnerability(v.get_desc(), severity=v.get_severity())

        fr = FuzzableRequest.from_http_response(response)
        self.output_queue.put(fr)

    def _match_signature(self, response):
        """
        Heuristic to infer if the content of <response> has the pattern of a
        backdoor response.

        :param response: HTTPResponse object
        :return: A bool value
        """
        body_text = response.get_body()

        for match, _, _, _ in self._signature_re.query(body_text):
            match_string = match.group(0)
            return match_string

        return None

    def get_long_desc(self):
        """
        :return: A DETAILED description of the plugin functions and features.
        """
        return """
Exemplo n.º 24
0
class http_in_body(GrepPlugin):
    """
    Search for HTTP request/response string in response body.
    :author: Andres Riancho ([email protected])
    """

    HTTP = (
        # GET / HTTP/1.0
        ('[a-zA-Z]{3,6} .*? HTTP/1.[01]', 'REQUEST'),
        # HTTP/1.1 200 OK
        ('HTTP/1.[01] [0-9][0-9][0-9] [a-zA-Z]*', 'RESPONSE')
    )
    _multi_re = MultiRE(HTTP)

    def grep(self, request, response):
        """
        Plugin entry point.

        :param request: The HTTP request object.
        :param response: The HTTP response object
        :return: None, all results are saved in the kb.
        """
        # 501 Code is "Not Implemented" which in some cases responds with
        # this in the body:
        # <body><h2>HTTP/1.1 501 Not Implemented</h2></body>
        # Which creates a false positive.
        if response.get_code() == 501:
            return

        if not response.is_text_or_html():
            return
            
        body_without_tags = response.get_clear_text_body()
        if body_without_tags is None:
            return

        uri = response.get_uri()

        for match, _, _, reqres in self._multi_re.query(body_without_tags):

            if reqres == 'REQUEST':
                desc = 'An HTTP request was found in the HTTP body of a response.'
                i = Info('HTTP Request in HTTP body', desc, response.id, self.get_name())
                i.set_uri(uri)
                i.add_to_highlight(match.group(0))
                kb.kb.append(self, 'request', i)

            if reqres == 'RESPONSE':
                desc = 'An HTTP response was found in the HTTP body of a response.'
                i = Info('HTTP Response in HTTP body', desc, response.id, self.get_name())
                i.set_uri(uri)
                i.add_to_highlight(match.group(0))
                kb.kb.append(self, 'response', i)

    def end(self):
        """
        This method is called when the plugin wont be used anymore.
        """
        item_fmt = '- %s  (id: %s)'
        msg = ('The following URLs have an HTTP %s in the HTTP'
               ' response body:')
        
        for info_type in ['request', 'response']:
            if kb.kb.get('http_in_body', info_type):
                
                om.out.information(msg % info_type)
                
                for i in kb.kb.get('http_in_body', info_type):
                    om.out.information(item_fmt % (i.get_uri(), i.get_id()))

    def get_long_desc(self):
        """
        :return: A DETAILED description of the plugin functions and features.
        """
        return """\
Exemplo n.º 25
0
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 """
Exemplo n.º 26
0
class sqli(AuditPlugin):
    """
    Find SQL injection bugs.
    :author: Andres Riancho ([email protected])
    """
    SQL_ERRORS_STR = (
        # ASP / MSSQL
        (r'System.Data.OleDb.OleDbException', dbms.MSSQL),
        (r'[SQL Server]', dbms.MSSQL),
        (r'[Microsoft][ODBC SQL Server Driver]', dbms.MSSQL),
        (r'[SQLServer JDBC Driver]', dbms.MSSQL),
        (r'[SqlException', dbms.MSSQL),
        (r'System.Data.SqlClient.SqlException', dbms.MSSQL),
        (r'Unclosed quotation mark after the character string', dbms.MSSQL),
        (r"'80040e14'", dbms.MSSQL),
        (r'mssql_query()', dbms.MSSQL),
        (r'odbc_exec()', dbms.MSSQL),
        (r'Microsoft OLE DB Provider for ODBC Drivers', dbms.MSSQL),
        (r'Microsoft OLE DB Provider for SQL Server', dbms.MSSQL),
        (r'Incorrect syntax near', dbms.MSSQL),
        (r'Sintaxis incorrecta cerca de', dbms.MSSQL),
        (r'Syntax error in string in query expression', dbms.MSSQL),
        (r'ADODB.Field (0x800A0BCD)<br>', dbms.MSSQL),
        (r"ADODB.Recordset'", dbms.MSSQL),
        (r"Unclosed quotation mark before the character string", dbms.MSSQL),
        (r"'80040e07'", dbms.MSSQL),
        (r'Microsoft SQL Native Client error', dbms.MSSQL),
        (r'SQL Server Native Client', dbms.MSSQL),
        (r'Invalid SQL statement', dbms.MSSQL),

        # DB2
        (r'SQLCODE', dbms.DB2),
        (r'DB2 SQL error:', dbms.DB2),
        (r'SQLSTATE', dbms.DB2),
        (r'[CLI Driver]', dbms.DB2),
        (r'[DB2/6000]', dbms.DB2),

        # Sybase
        (r"Sybase message:", dbms.SYBASE),
        (r"Sybase Driver", dbms.SYBASE),
        (r"[SYBASE]", dbms.SYBASE),

        # Access
        (r'Syntax error in query expression', dbms.ACCESS),
        (r'Data type mismatch in criteria expression.', dbms.ACCESS),
        (r'Microsoft JET Database Engine', dbms.ACCESS),
        (r'[Microsoft][ODBC Microsoft Access Driver]', dbms.ACCESS),

        # ORACLE
        (r'Microsoft OLE DB Provider for Oracle', dbms.ORACLE),
        (r'wrong number or types', dbms.ORACLE),

        # POSTGRE
        (r'PostgreSQL query failed:', dbms.POSTGRE),
        (r'supplied argument is not a valid PostgreSQL result', dbms.POSTGRE),
        (r'unterminated quoted string at or near', dbms.POSTGRE),
        (r'pg_query() [:', dbms.POSTGRE),
        (r'pg_exec() [:', dbms.POSTGRE),

        # MYSQL
        (r'supplied argument is not a valid MySQL', dbms.MYSQL),
        (r'Column count doesn\'t match value count at row', dbms.MYSQL),
        (r'mysql_fetch_array()', dbms.MYSQL),
        (r'mysql_', dbms.MYSQL),
        (r'on MySQL result index', dbms.MYSQL),
        (r'You have an error in your SQL syntax;', dbms.MYSQL),
        (r'You have an error in your SQL syntax near', dbms.MYSQL),
        (r'MySQL server version for the right syntax to use', dbms.MYSQL),
        (r'Division by zero in', dbms.MYSQL),
        (r'not a valid MySQL result', dbms.MYSQL),
        (r'[MySQL][ODBC', dbms.MYSQL),
        (r"Column count doesn't match", dbms.MYSQL),
        (r"the used select statements have different number of columns",
         dbms.MYSQL),
        (r"DBD::mysql::st execute failed", dbms.MYSQL),
        (r"DBD::mysql::db do failed:", dbms.MYSQL),

        # Informix
        (r'com.informix.jdbc', dbms.INFORMIX),
        (r'Dynamic Page Generation Error:', dbms.INFORMIX),
        (r'An illegal character has been found in the statement',
         dbms.INFORMIX),
        (r'[Informix]', dbms.INFORMIX),
        (r'<b>Warning</b>:  ibase_', dbms.INTERBASE),
        (r'Dynamic SQL Error', dbms.INTERBASE),

        # DML
        (r'[DM_QUERY_E_SYNTAX]', dbms.DMLDATABASE),
        (r'has occurred in the vicinity of:', dbms.DMLDATABASE),
        (r'A Parser Error (syntax error)', dbms.DMLDATABASE),

        # Java
        (r'java.sql.SQLException', dbms.JAVA),
        (r'Unexpected end of command in statement', dbms.JAVA),

        # Coldfusion
        (r'[Macromedia][SQLServer JDBC Driver]', dbms.MSSQL),

        # SQLite
        (r'could not prepare statement', dbms.SQLITE),

        # Generic errors..
        (r'Unknown column', dbms.UNKNOWN),
        (r'where clause', dbms.UNKNOWN),
        (r'SqlServer', dbms.UNKNOWN),
        (r'syntax error', dbms.UNKNOWN),
        (r'Microsoft OLE DB Provider', dbms.UNKNOWN),
    )
    _multi_in = MultiIn(x[0] for x in SQL_ERRORS_STR)

    SQL_ERRORS_RE = (
        # ASP / MSSQL
        (r"Procedure '[^']+' requires parameter '[^']+'", dbms.MSSQL),
        # ORACLE
        (r'PLS-[0-9][0-9][0-9][0-9]', dbms.ORACLE),
        (r'ORA-[0-9][0-9][0-9][0-9]', dbms.ORACLE),
        # MYSQL
        (r"Table '[^']+' doesn't exist", dbms.MYSQL),
        # Generic errors..
        (r'SELECT .*? FROM .*?', dbms.UNKNOWN),
        (r'UPDATE .*? SET .*?', dbms.UNKNOWN),
        (r'INSERT INTO .*?', dbms.UNKNOWN),
    )
    _multi_re = MultiRE(SQL_ERRORS_RE)

    # Note that these payloads are similar but they do generate different errors
    # depending on the SQL query context they are used. Removing one or the
    # other will lower our SQLMap testenv coverage
    SQLI_STRINGS = (u"a'b\"c'd\"", u"1'2\"3")

    SQLI_MESSAGE = (u'A SQL error was found in the response supplied by '
                    u'the web application, the error is (only a fragment is '
                    u'shown): "%s". The error was found on response with id'
                    u' %s.')

    def __init__(self):
        AuditPlugin.__init__(self)

    def audit(self, freq, orig_response, debugging_id):
        """
        Tests an URL for SQL injection 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()
        """
        mutants = create_mutants(freq,
                                 self.SQLI_STRINGS,
                                 orig_resp=orig_response)

        self._send_mutants_in_threads(self._uri_opener.send_mutant,
                                      mutants,
                                      self._analyze_result,
                                      debugging_id=debugging_id)

    def _analyze_result(self, mutant, response):
        """
        Analyze results of the _send_mutant method.
        """
        sql_error_list = self._findsql_error(response)
        orig_resp_body = mutant.get_original_response_body()

        for sql_error_string, dbms_type in sql_error_list:
            if sql_error_string not in orig_resp_body:
                if self._has_no_bug(mutant):
                    # Create the vuln,
                    desc = 'SQL injection in a %s was found at: %s'
                    desc %= dbms_type, mutant.found_at()

                    v = Vuln.from_mutant('SQL injection', desc, severity.HIGH,
                                         response.id, self.get_name(), mutant)

                    v.add_to_highlight(sql_error_string)
                    v['error'] = sql_error_string
                    v['db'] = dbms_type

                    self.kb_append_uniq(self, 'sqli', v)
                    break

    def _findsql_error(self, response):
        """
        This method searches for SQL errors in html's.

        :param response: The HTTP response object
        :return: A list of errors found on the page
        """
        res = []

        for match in self._multi_in.query(response.body):
            om.out.information(self.SQLI_MESSAGE % (match, response.id))
            dbms_type = [x[1] for x in self.SQL_ERRORS_STR if x[0] == match][0]
            res.append((match, dbms_type))

        for match, _, regex_comp, dbms_type in self._multi_re.query(
                response.body):
            om.out.information(self.SQLI_MESSAGE %
                               (match.group(0), response.id))
            res.append((match.group(0), dbms_type))

        return res

    def get_long_desc(self):
        """
        :return: A DETAILED description of the plugin functions and features.
        """
        return """
Exemplo n.º 27
0
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 = MultiIn(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>&nbsp;Microsoft .NET Framework Version:1.1.4322.2300; ASP.NET Version:1.1.4322.2300
        ('<b>Version Information:</b>&nbsp;(.*?)\n', 'ASP .NET'))
    _multi_re = MultiRE(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 """
Exemplo n.º 28
0
class lfi(AuditPlugin):
    """
    Find local file inclusion vulnerabilities.
    :author: Andres Riancho ([email protected])
    """

    file_pattern_multi_in = MultiIn(FILE_PATTERNS)
    file_read_error_multi_re = MultiRE(FILE_OPEN_ERRORS)

    def audit(self, freq, orig_response, debugging_id):
        """
        Tests an URL for local file inclusion 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()
        """
        mutants = create_mutants(freq,
                                 self.get_lfi_tests(freq),
                                 orig_resp=orig_response)

        self._send_mutants_in_threads(self._uri_opener.send_mutant,
                                      mutants,
                                      self._analyze_result,
                                      grep=False,
                                      debugging_id=debugging_id)

    def get_lfi_tests(self, freq):
        """
        :param freq: The fuzzable request we're analyzing
        :return: The paths to test
        """
        #
        #   Add some tests which try to read "self"
        #   http://host.tld/show_user.php?id=show_user.php
        #
        lfi_tests = [
            freq.get_url().get_file_name(),
            '/%s' % freq.get_url().get_file_name()
        ]

        #
        #   Add some tests which try to read common/known files
        #
        lfi_tests.extend(self._get_common_file_list(freq.get_url()))

        return lfi_tests

    def _get_common_file_list(self, orig_url):
        """
        This method returns a list of local files to try to include.

        :return: A string list, see above.
        """
        local_files = []

        extension = orig_url.get_extension()

        # I will only try to open these files, they are easy to identify of they
        # echoed by a vulnerable web app and they are on all unix or windows
        # default installs. Feel free to mail me (Andres Riancho) if you know
        # about other default files that could be installed on AIX ? Solaris ?
        # and are not /etc/passwd
        if cf.cf.get('target_os') in {'unix', 'unknown'}:
            local_files.append('/../' * 15 + 'etc/passwd')
            local_files.append('../' * 15 + 'etc/passwd')

            local_files.append('/../' * 15 + 'etc/passwd\0')
            local_files.append('/../' * 15 + 'etc/passwd\0.html')
            local_files.append('/etc/passwd')

            # This test adds support for finding vulnerabilities like this one
            # http://website/zen-cart/extras/curltest.php?url=file:///etc/passwd
            local_files.append('file:///etc/passwd')

            local_files.append('/etc/passwd\0')
            local_files.append('/etc/passwd\0.html')

            if extension != '':
                local_files.append('/etc/passwd%00.' + extension)
                local_files.append('/../' * 15 + 'etc/passwd%00.' + extension)

        if cf.cf.get('target_os') in {'windows', 'unknown'}:
            local_files.append('/../' * 15 + 'boot.ini')
            local_files.append('../' * 15 + 'boot.ini')

            local_files.append('/../' * 15 + 'boot.ini\0')
            local_files.append('/../' * 15 + 'boot.ini\0.html')

            local_files.append('C:\\boot.ini')
            local_files.append('C:\\boot.ini\0')
            local_files.append('C:\\boot.ini\0.html')

            local_files.append('%SYSTEMROOT%\\win.ini')
            local_files.append('%SYSTEMROOT%\\win.ini\0')
            local_files.append('%SYSTEMROOT%\\win.ini\0.html')

            # file:// URIs for windows , docs here: http://goo.gl/A9Mvux
            local_files.append('file:///C:/boot.ini')
            local_files.append('file:///C:/win.ini')

            if extension != '':
                local_files.append('C:\\boot.ini%00.' + extension)
                local_files.append('%SYSTEMROOT%\\win.ini%00.' + extension)

        return local_files

    def _analyze_result(self, mutant, response):
        """
        Analyze results of the _send_mutant method.
        Try to find the local file inclusions.
        """
        #
        #   I will only report the vulnerability once.
        #
        if self._has_bug(mutant):
            return

        #
        #   Identify the vulnerability
        #
        for file_pattern_match in self._find_common_file_fragments(response):
            if file_pattern_match not in mutant.get_original_response_body():

                desc = 'Local File Inclusion was found at: %s'
                desc %= mutant.found_at()

                v = Vuln.from_mutant('Local file inclusion vulnerability',
                                     desc, severity.MEDIUM, response.id,
                                     self.get_name(), mutant)

                v['file_pattern'] = file_pattern_match

                v.add_to_highlight(file_pattern_match)
                self.kb_append_uniq(self, 'lfi', v)
                return

        #
        # If the vulnerability could not be identified by matching strings that
        # commonly appear in "/etc/passwd", then I'll check one more thing...
        # (note that this is run if no vulns were identified)
        #
        # http://host.tld/show_user.php?id=show_user.php
        #
        # The calls to smart_str_ignore fix a UnicodeDecoreError which appears when
        # the token value is a binary string which can't be converted to unicode.
        # This happens, for example, when trying to upload JPG files to a multipart form
        #
        # >>> u'' in '\x80'
        # ...
        # UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 0: ordinal not in range(128)
        #
        filename = smart_str_ignore(mutant.get_url().get_file_name())
        token_value = smart_str_ignore(mutant.get_token_value())

        if filename in token_value:
            match, lang = contains_source_code(response)
            if match:
                # We were able to read the source code of the file that is
                # vulnerable to local file read
                desc = ('An arbitrary local file read vulnerability was'
                        ' found at: %s')
                desc %= mutant.found_at()

                v = Vuln.from_mutant('Local file inclusion vulnerability',
                                     desc, severity.MEDIUM, response.id,
                                     self.get_name(), mutant)

                #
                #    Set which part of the source code to match
                #
                match_source_code = match.group(0)
                v['file_pattern'] = match_source_code

                self.kb_append_uniq(self, 'lfi', v)
                return

        #
        #   Check for interesting errors (note that this is run if no vulns were
        #   identified)
        #
        body = response.get_body()
        for _, error_str, _ in self.file_read_error_multi_re.query(body):
            if error_str not in mutant.get_original_response_body():
                desc = 'A file read error was found at: %s'
                desc %= mutant.found_at()

                i = Info.from_mutant('File read error', desc, response.id,
                                     self.get_name(), mutant)
                i.add_to_highlight(error_str)

                self.kb_append_uniq(self, 'error', i)

    def _find_common_file_fragments(self, response):
        """
        This method finds out if the local file has been successfully included
        in the resulting HTML.

        :param response: The HTTP response object
        :return: A list of errors found on the page
        """
        res = set()
        body = response.get_body()

        for file_pattern_match in self.file_pattern_multi_in.query(body):
            res.add(file_pattern_match)

        if len(res) == 1:
            msg = (
                'A file fragment was found. The section where the file is'
                ' included is (only a fragment is shown): "%s". This is'
                ' just an informational message, which might be related'
                '  to a vulnerability and was found on response with id %s.')
            om.out.debug(msg % (list(res)[0], response.id))

        if len(res) > 1:
            msg = ('File fragments have been found. The following is a list'
                   ' of file fragments that were returned by the web'
                   ' application while testing for local file inclusion: \n')

            for file_pattern_match in res:
                msg += '- "%s" \n' % file_pattern_match

            msg += ('This is just an informational message, which might be'
                    ' related to a vulnerability and was found in response'
                    ' with id %s.' % response.id)

            om.out.debug(msg)

        return res

    def get_long_desc(self):
        """
        :return: A DETAILED description of the plugin functions and features.
        """
        return """
Exemplo n.º 29
0
class find_backdoors(CrawlPlugin):
    """
    Find web backdoors and web shells.

    :author: Andres Riancho ([email protected])
    """
    WEBSHELL_DB = os.path.join(CRAWL_PATH, 'find_backdoors', 'web_shells.txt')
    SIGNATURE_DB = os.path.join(CRAWL_PATH, 'find_backdoors', 'signatures.txt')

    def __init__(self):
        CrawlPlugin.__init__(self)

        # Internal variables
        self._analyzed_dirs = ScalableBloomFilter()
        self._signature_re = None

    def setup(self):
        with self._plugin_lock:
            if self._signature_re is not None:
                return

            signatures = self._read_signatures()
            self._signature_re = MultiRE(signatures, hint_len=2)

    def _read_signatures(self):
        for line in file(self.SIGNATURE_DB):
            line = line.strip()

            if not line:
                continue

            if line.startswith('#'):
                continue

            yield (line, 'Backdoor signature')

    def crawl(self, fuzzable_request):
        """
        For every directory, fetch a list of shell files and analyze the
        response.

        :param fuzzable_request: A fuzzable_request instance that contains
                                    (among other things) the URL to test.
        """
        domain_path = fuzzable_request.get_url().get_domain_path()

        if domain_path not in self._analyzed_dirs:
            self._analyzed_dirs.add(domain_path)

            self.setup()

            # Read the web shell database
            web_shells = self._iter_web_shells()

            # Send the requests using threads:
            args_iter = (domain_path.url_join(fname) for fname in web_shells)
            self.worker_pool.map(self._check_if_exists, args_iter)

    def _iter_web_shells(self):
        """
        :yield: lines from the web shell DB
        """
        for line in file(self.WEBSHELL_DB):
            line = line.strip()

            if line.startswith('#'):
                continue

            if not line:
                continue

            yield line

    def _check_if_exists(self, web_shell_url):
        """
        Check if the file exists.

        :param web_shell_url: The URL to check
        """
        try:
            response = self._uri_opener.GET(web_shell_url, cache=True)
        except BaseFrameworkException:
            om.out.debug('Failed to GET webshell:' + web_shell_url)
        else:
            signature = self._match_signature(response)
            if signature is None:
                return

            desc = (u'An HTTP response matching the web backdoor signature'
                    u' "%s" was found at: "%s"; this could indicate that the'
                    u' server has been compromised.')
            desc %= (signature, response.get_url())

            # It's probability is higher if we found a long signature
            _severity = severity.HIGH if len(signature) > 8 else severity.MEDIUM

            v = Vuln(u'Potential web backdoor', desc, _severity,
                     response.id, self.get_name())
            v.set_url(response.get_url())

            kb.kb.append(self, 'backdoors', v)
            om.out.vulnerability(v.get_desc(), severity=v.get_severity())

            fr = FuzzableRequest.from_http_response(response)
            self.output_queue.put(fr)

    def _match_signature(self, response):
        """
        Heuristic to infer if the content of <response> has the pattern of a
        backdoor response.

        :param response: HTTPResponse object
        :return: A bool value
        """
        body_text = response.get_body()
        
        for match, _, _, _ in self._signature_re.query(body_text):
            match_string = match.group(0)
            return match_string

        return None

    def get_long_desc(self):
        """
        :return: A DETAILED description of the plugin functions and features.
        """
        return """
Exemplo n.º 30
0
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
        """
        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 """
Exemplo n.º 31
0
    def test_dup(self):
        re_list = ['1234', '4567']
        mre = MultiRE(re_list)

        result = to_list(mre.query('4567 4567'))
        self.assertEqual(1, len(result))
Exemplo n.º 32
0
    def test_short(self):
        re_list = ['12.?34']
        mre = MultiRE(re_list)

        result = to_list(mre.query('12X34'))
        self.assertEqual(1, len(result))