Ejemplo n.º 1
0
    async def _async_try_login_post(
            self, username: str, password: str,
            auth_url: str) -> Tuple[bool, dict, List[str]]:
        # Fetch the login page and try to extract the login form
        try:
            page = await self.async_get(web.Request(auth_url),
                                        follow_redirects=True)
            form = {}
            disconnect_urls = []

            login_form, username_field_idx, password_field_idx = page.find_login_form(
            )
            if login_form:
                post_params = login_form.post_params
                get_params = login_form.get_params

                if login_form.method == "POST":
                    post_params[username_field_idx][1] = username
                    post_params[password_field_idx][1] = password
                    form["login_field"] = post_params[username_field_idx][0]
                    form["password_field"] = post_params[password_field_idx][0]
                else:
                    get_params[username_field_idx][1] = username
                    get_params[password_field_idx][1] = password
                    form["login_field"] = get_params[username_field_idx][0]
                    form["password_field"] = get_params[password_field_idx][0]

                login_request = web.Request(path=login_form.url,
                                            method=login_form.method,
                                            post_params=post_params,
                                            get_params=get_params,
                                            referer=login_form.referer,
                                            link_depth=login_form.link_depth)

                login_response = await self.async_send(login_request,
                                                       follow_redirects=True)

                # ensure logged in
                if login_response.soup.find_all(
                        text=re.compile(DISCONNECT_REGEX)):
                    self.is_logged_in = True
                    logging.success(_("Login success"))
                    disconnect_urls = self._extract_disconnect_urls(
                        login_response)
                else:
                    logging.warning(
                        _("Login failed") + " : " +
                        _("Credentials might be invalid"))
            else:
                logging.warning(
                    _("Login failed") + " : " + _("No login form detected"))
            return self.is_logged_in, form, disconnect_urls

        except ConnectionError:
            logging.error(_("[!] Connection error with URL"), auth_url)
        except httpx.RequestError as error:
            logging.error(
                _("[!] {} with url {}").format(error.__class__.__name__,
                                               auth_url))
Ejemplo n.º 2
0
    async def async_try_login(self, auth_url: str):
        """Try to authenticate with the provided url and credentials."""
        if len(self._auth_credentials) != 2:
            logging.error(
                _("Login failed") + " : " + _("Invalid credentials format"))
            return

        username, password = self._auth_credentials

        # Fetch the login page and try to extract the login form
        try:
            page = await self.async_get(web.Request(auth_url),
                                        follow_redirects=True)

            login_form, username_field_idx, password_field_idx = page.find_login_form(
            )
            if login_form:
                post_params = login_form.post_params
                get_params = login_form.get_params

                if login_form.method == "POST":
                    post_params[username_field_idx][1] = username
                    post_params[password_field_idx][1] = password
                else:
                    get_params[username_field_idx][1] = username
                    get_params[password_field_idx][1] = password

                login_request = web.Request(path=login_form.url,
                                            method=login_form.method,
                                            post_params=post_params,
                                            get_params=get_params,
                                            referer=login_form.referer,
                                            link_depth=login_form.link_depth)

                login_response = await self.async_send(login_request,
                                                       follow_redirects=True)

                # ensure logged in
                if login_response.soup.find_all(text=re.compile(
                        r'(?i)((log|sign)\s?out|disconnect|déconnexion)')):
                    self.is_logged_in = True
                    logging.success(_("Login success"))

                else:
                    logging.warning(
                        _("Login failed") + " : " +
                        _("Credentials might be invalid"))
            else:
                logging.warning(
                    _("Login failed") + " : " + _("No login form detected"))

        except ConnectionError:
            logging.error(_("[!] Connection error with URL"), auth_url)
        except httpx.RequestError as error:
            logging.error(
                _("[!] {} with url {}").format(error.__class__.__name__,
                                               auth_url))
Ejemplo n.º 3
0
    def __init__(self, crawler, xml_report_generator, logger, attack_options):
        Attack.__init__(self, crawler, xml_report_generator, logger, attack_options)
        user_config_dir = os.getenv('HOME') or os.getenv('USERPROFILE')
        user_config_dir += "/config"

        if not os.path.isdir(user_config_dir):
            os.makedirs(user_config_dir)
        try:
            with open(os.path.join(user_config_dir, self.NIKTO_DB)) as fd:
                reader = csv.reader(fd)
                self.nikto_db = [line for line in reader if line != [] and line[0].isdigit()]
        except IOError:
            try:
                print(_("Problem with local nikto database."))
                print(_("Downloading from the web..."))
                nikto_req = web.Request("http://cirt.net/nikto/UPDATES/2.1.5/db_tests")
                response = self.crawler.send(nikto_req)

                csv.register_dialect("nikto", quoting=csv.QUOTE_ALL, doublequote=False, escapechar="\\")
                reader = csv.reader(response.content.split("\n"), "nikto")
                self.nikto_db = [line for line in reader if line != [] and line[0].isdigit()]

                with open(os.path.join(user_config_dir, self.NIKTO_DB), "w") as fd:
                    writer = csv.writer(fd)
                    writer.writerows(self.nikto_db)

            except socket.timeout:
                print(_("Error downloading Nikto database"))
Ejemplo n.º 4
0
    async def _async_try_login_basic_digest_ntlm(
            self, auth_url: str) -> Tuple[bool, dict, List[str]]:
        page = await self.async_get(web.Request(auth_url))

        if page.status in (401, 403, 404):
            return False, {}, []
        return True, {}, []
Ejemplo n.º 5
0
    def test_credentials(self, login_form, username_index, password_index,
                         username, password):
        post_params = login_form.post_params
        get_params = login_form.get_params

        if login_form.method == "POST":
            post_params[username_index][1] = username
            post_params[password_index][1] = password
        else:
            get_params[username_index][1] = username
            get_params[password_index][1] = password

        login_request = web.Request(path=login_form.url,
                                    method=login_form.method,
                                    post_params=post_params,
                                    get_params=get_params,
                                    referer=login_form.referer,
                                    link_depth=login_form.link_depth)

        try:
            login_response = self.crawler.send(login_request,
                                               follow_redirects=True)
        except ReadTimeout:
            return ""

        return login_response.content
Ejemplo n.º 6
0
    def __init__(self,
                 base_url: str,
                 timeout: float = 10.0,
                 secure: bool = False,
                 compression: bool = True,
                 proxies: dict = None,
                 user_agent: str = None):
        self._timeout = timeout
        self._session = requests.Session()
        if user_agent:
            self._session.headers["User-Agent"] = user_agent
        else:
            self._session.headers[
                "User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0"
        self._session.headers["Accept-Language"] = "en-US"
        self._session.headers["Accept-Encoding"] = "gzip, deflate, br"
        self._session.headers[
            "Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
        self._session.max_redirects = 5
        self._session.verify = secure
        self._scope = Scope.FOLDER
        self._base = web.Request(base_url)
        self.auth_url = self._base.url
        self.is_logged_in = False

        if not compression:
            self._session.headers["accept-encoding"] = "identity"

        if proxies is not None and isinstance(proxies, dict):
            # ex: {'http': 'http://127.0.0.1:8080'}
            self._session.proxies = proxies

        self._auth_credentials = {}
        self._auth_method = "basic"
Ejemplo n.º 7
0
    def attack(self):
        http_resources = self.persister.get_links(attack_module=self.name) if self.do_get else []

        for original_request in http_resources:
            try:
                url = original_request.path

                if self.verbose == 2:
                    print("[¨] {0}".format(url))

                if url not in self.attacked_get:
                    self.attacked_get.append(url)

                    evil_req = web.Request(url)

                    resp = self.crawler.send(evil_req, headers=self.hdrs)
                    if resp:
                        data = resp.content
                        if self.rand_string in data:
                            self.log_red(_("URL {0} seems vulnerable to Shellshock attack!").format(url))

                            self.add_vuln(
                                request_id=original_request.path_id,
                                category=Vulnerability.EXEC,
                                level=Vulnerability.HIGH_LEVEL,
                                request=evil_req,
                                info=_("URL {0} seems vulnerable to Shellshock attack").format(url)
                            )
            except (RequestException, KeyboardInterrupt) as exception:
                yield exception

            yield original_request
Ejemplo n.º 8
0
    def find_login_form(self):
        """Returns the login Request extracted from the Page, the username and password fields."""

        for form in self.soup.find_all("form"):
            username_field_idx = []
            password_field_idx = []

            for i, input_field in enumerate(form.find_all("input")):
                input_type = input_field.attrs.get("type", "text").lower()
                input_name = input_field.attrs.get("name", "undefined").lower()
                input_id = input_field.attrs.get("id", "undefined").lower()
                if input_type == "email":
                    username_field_idx.append(i)

                elif input_type == "text" and (any(
                        field_name in input_name
                        for field_name in ["mail", "user", "login", "name"]
                ) or any(field_id in input_id
                         for field_id in ["mail", "user", "login", "name"])):
                    username_field_idx.append(i)

                elif input_type == "password":
                    password_field_idx.append(i)

            # ensure login form
            if len(username_field_idx) == 1 and len(password_field_idx) == 1:
                inputs = form.find_all("input", attrs={"name": True})

                url = self.make_absolute(
                    form.attrs.get("action", "").strip() or self.url)
                method = form.attrs.get("method", "GET").strip().upper()
                enctype = form.attrs.get(
                    "enctype", "application/x-www-form-urlencoded").lower()
                post_params = []
                get_params = []
                if method == "POST":
                    post_params = [[
                        input_data["name"],
                        input_data.get("value", "")
                    ] for input_data in inputs]
                else:
                    get_params = [[
                        input_data["name"],
                        input_data.get("value", "")
                    ] for input_data in inputs]

                login_form = web.Request(
                    url,
                    method=method,
                    post_params=post_params,
                    get_params=get_params,
                    encoding=self.apparent_encoding,
                    referer=self.url,
                    enctype=enctype,
                )

                return login_form, username_field_idx[0], password_field_idx[0]

        return None, "", ""
Ejemplo n.º 9
0
    def get_path_by_id(self, path_id):
        cursor = self._conn.cursor()

        cursor.execute("SELECT * FROM paths WHERE path_id = ? LIMIT 1",
                       (path_id, ))

        row = cursor.fetchone()
        if not row:
            return None

        get_params = []
        post_params = []
        file_params = []

        for param_row in cursor.execute(
            ("SELECT type, name, value1, value2, meta "
             "FROM params "
             "WHERE path_id = ? "
             "ORDER BY type, param_order"), (path_id, )):
            name = param_row[1]
            value1 = param_row[2]

            if param_row[0] == "GET":
                get_params.append([name, value1])
            elif param_row[0] == "POST":
                if name == "__RAW__" and not post_params:
                    # First POST parameter is __RAW__, it should mean that we have raw content
                    post_params = value1
                elif isinstance(post_params, list):
                    post_params.append([name, value1])
            elif param_row[0] == "FILE":
                if param_row[4]:
                    file_params.append(
                        [name, (value1, param_row[3], param_row[4])])
                else:
                    file_params.append([name, (value1, param_row[3])])
            else:
                raise ValueError("Unknown param type {}".format(param_row[0]))

        request = web.Request(row[1],
                              method=row[2],
                              encoding=row[5],
                              enctype=row[3],
                              referer=row[8],
                              get_params=get_params,
                              post_params=post_params,
                              file_params=file_params)

        if row[6]:
            request.status = row[6]

        if row[7]:
            request.set_headers(json.loads(row[7]))

        request.link_depth = row[4]
        request.path_id = path_id

        return request
Ejemplo n.º 10
0
    def attack(self):
        http_resources = self.persister.get_links(
            attack_module=self.name) if self.do_get else []

        for original_request in http_resources:

            if original_request.file_name == "":
                yield original_request
                continue

            page = original_request.path
            headers = original_request.headers

            # Do not attack application-type files
            if "content-type" not in headers:
                # Sometimes there's no content-type... so we rely on the document extension
                if (page.split(".")[-1]
                        not in self.allowed) and page[-1] != "/":
                    yield original_request
                    continue
            elif "text" not in headers["content-type"]:
                yield original_request
                continue

            for payload, flags in self.payloads:
                try:
                    payload = payload.replace("[FILE_NAME]",
                                              original_request.file_name)
                    payload = payload.replace(
                        "[FILE_NOEXT]",
                        splitext(original_request.file_name)[0])
                    url = page.replace(original_request.file_name, payload)

                    if self.verbose == 2:
                        print("[¨] {0}".format(url))

                    if url not in self.attacked_get:
                        self.attacked_get.append(url)
                        evil_req = web.Request(url)

                        response = self.crawler.send(evil_req)
                        if response and response.status == 200:
                            self.log_red(
                                _("Found backup file {}".format(evil_req.url)))

                            self.add_vuln(
                                request_id=original_request.path_id,
                                category=NAME,
                                level=LOW_LEVEL,
                                request=evil_req,
                                info=_("Backup file {0} found for {1}").format(
                                    url, page))

                except (KeyboardInterrupt, RequestException) as exception:
                    yield exception

            yield original_request
Ejemplo n.º 11
0
    def test_directory(self, path: str):
        if self.verbose == 2:
            print("[¨] Testing directory {0}".format(path))

        test_page = web.Request(path + "does_n0t_exist.htm")
        try:
            response = self.crawler.send(test_page)
            if response.status not in [403, 404]:
                # we don't want to deal with this at the moment
                return

            for candidate, flags in self.payloads:
                url = path + candidate
                if url not in self.known_dirs and url not in self.known_pages and url not in self.new_resources:
                    page = web.Request(path + candidate)
                    try:
                        response = self.crawler.send(page)
                        if response.redirection_url:
                            loc = response.redirection_url
                            # if loc in self.known_dirs or loc in self.known_pages:
                            #     continue
                            if response.is_directory_redirection:
                                self.log_red("Found webpage {0}", loc)
                                self.new_resources.append(loc)
                            else:
                                self.log_red("Found webpage {0}", page.path)
                                self.new_resources.append(page.path)
                        elif response.status not in [403, 404]:
                            self.log_red("Found webpage {0}", page.path)
                            self.new_resources.append(page.path)
                    except Timeout:
                        continue
                    except ConnectionError:
                        continue

        except Timeout:
            pass
Ejemplo n.º 12
0
    def attack(self):
        http_resources = self.persister.get_links(
            attack_module=self.name) if self.do_get else []

        for original_request in http_resources:
            url = original_request.path
            referer = original_request.referer
            headers = {}
            if referer:
                headers["referer"] = referer

            if url not in self.attacked_get:
                if original_request.status in (401, 402, 403, 407):
                    # The ressource is forbidden
                    try:
                        evil_req = web.Request(url, method="ABC")
                        response = self.crawler.send(evil_req, headers=headers)
                        unblocked_content = response.content

                        if response.status == 404 or response.status < 400 or response.status >= 500:
                            # Every 4xx status should be uninteresting (specially bad request in our case)

                            self.log_red("---")
                            self.add_vuln(
                                request_id=original_request.path_id,
                                category=Vulnerability.HTACCESS,
                                level=Vulnerability.HIGH_LEVEL,
                                request=evil_req,
                                info=_(
                                    "{0} bypassable weak restriction").format(
                                        evil_req.url))
                            self.log_red(
                                _("Weak restriction bypass vulnerability: {0}"
                                  ), evil_req.url)
                            self.log_red(
                                _("HTTP status code changed from {0} to {1}").
                                format(original_request.status,
                                       response.status))

                            if self.verbose == 2:
                                self.log_red(_("Source code:"))
                                self.log_red(unblocked_content)
                            self.log_red("---")

                        self.attacked_get.append(url)
                    except (RequestException, KeyboardInterrupt) as exception:
                        yield exception

            yield original_request
Ejemplo n.º 13
0
    def __init__(self, crawler, persister, logger, attack_options):
        Attack.__init__(self, crawler, persister, logger, attack_options)
        csv.register_dialect("nikto",
                             quoting=csv.QUOTE_ALL,
                             doublequote=False,
                             escapechar="\\")
        user_config_dir = os.getenv("HOME") or os.getenv("USERPROFILE")
        user_config_dir += "/config"

        if not os.path.isdir(user_config_dir):
            os.makedirs(user_config_dir)
        try:
            with open(os.path.join(user_config_dir, self.NIKTO_DB)) as fd:
                reader = csv.reader(fd, "nikto")
                self.nikto_db = [
                    line for line in reader
                    if line != [] and line[0].isdigit()
                ]
        except IOError:
            # Disable downloading of Nikto database because the license of the file
            # forbids it.
            self.nikto_db = []
            return
            try:
                print(_("Problem with local nikto database."))
                print(_("Downloading from the web..."))
                nikto_req = web.Request(
                    "https://raw.githubusercontent.com/sullo/nikto/master/program/databases/db_tests"
                )
                response = self.crawler.send(nikto_req)

                csv.register_dialect("nikto",
                                     quoting=csv.QUOTE_ALL,
                                     doublequote=False,
                                     escapechar="\\")
                reader = csv.reader(response.content.split("\n"), "nikto")
                self.nikto_db = [
                    line for line in reader
                    if line != [] and line[0].isdigit()
                ]

                with open(os.path.join(user_config_dir, self.NIKTO_DB),
                          "w") as fd:
                    writer = csv.writer(fd, "nikto")
                    writer.writerows(self.nikto_db)

            except socket.timeout:
                print(_("Error downloading Nikto database"))
Ejemplo n.º 14
0
    def _get_path_by_id(self, path_id):
        cursor = self._conn.cursor()

        cursor.execute("SELECT * FROM paths WHERE path_id = ? LIMIT 1",
                       (path_id, ))

        row = cursor.fetchone()
        if not row:
            return None

        get_params = []
        post_params = []
        file_params = []

        for param_row in cursor.execute(
                "SELECT type, name, value FROM params WHERE path_id = ? ORDER BY type, param_order",
            (path_id, )):
            name = param_row[1]
            value = param_row[2]
            if param_row[0] == "GET":
                get_params.append([name, value])
            elif param_row[0] == "POST":
                post_params.append([name, value])
            else:
                file_params.append([name, [value, "GIF89a", "image/gif"]])

        request = web.Request(row[1],
                              method=row[2],
                              encoding=row[5],
                              multipart=bool(row[3]),
                              referer=row[8],
                              get_params=get_params,
                              post_params=post_params,
                              file_params=file_params)

        if row[6]:
            request.status = row[6]

        if row[7]:
            request.set_headers(json.loads(row[7]))

        request.link_depth = row[4]
        request.path_id = path_id

        return request
Ejemplo n.º 15
0
    def update(self):
        try:
            request = web.Request(self.NIKTO_DB_URL)
            response = self.crawler.send(request)

            csv.register_dialect("nikto",
                                 quoting=csv.QUOTE_ALL,
                                 doublequote=False,
                                 escapechar="\\")
            reader = csv.reader(response.content.split("\n"), "nikto")
            self.nikto_db = [
                line for line in reader if line != [] and line[0].isdigit()
            ]

            with open(
                    os.path.join(self.persister.CRAWLER_DATA_DIR,
                                 self.NIKTO_DB), "w") as nikto_db_file:
                writer = csv.writer(nikto_db_file)
                writer.writerows(self.nikto_db)

        except IOError:
            print(_("Error downloading nikto database."))
Ejemplo n.º 16
0
    def __init__(self,
                 base_url: str,
                 timeout: float = 10.0,
                 secure: bool = False,
                 compression: bool = True):
        self._timeout = timeout
        self.stream = False
        self._scope = Scope.FOLDER
        self._base = web.Request(base_url)
        self.auth_url = self._base.url
        self.is_logged_in = False
        self._user_agent = "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0"
        self._headers = {
            "User-Agent":
            self._user_agent,
            "Accept-Language":
            "en-US",
            "Accept-Encoding":
            "gzip, deflate, br",
            "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
        }

        if not compression:
            self._client.headers["Accept-Encoding"] = "identity"

        self._secure = secure
        self._proxies = None
        self._transport = None
        self._drop_cookies = False
        self._cookies = None

        self._client = None
        self._auth_credentials = {}
        self._auth_method = "basic"
        self._auth = None
Ejemplo n.º 17
0
    async def async_analyze(self, request) -> Tuple[bool, List]:
        async with self._sem:
            self._processed_requests.append(request)  # thread safe

            if self._log:
                print("[+] {0}".format(request))

            dir_name = request.dir_name
            async with self._shared_lock:
                # lock to prevent launching duplicates requests that would otherwise waste time
                if dir_name not in self._custom_404_codes:
                    invalid_page = "zqxj{0}.html".format("".join(
                        [choice(ascii_letters) for __ in range(10)]))
                    invalid_resource = web.Request(dir_name + invalid_page)
                    try:
                        page = await self._crawler.async_get(invalid_resource)
                        self._custom_404_codes[dir_name] = page.status
                    except httpx.RequestError:
                        pass

            self._hostnames.add(request.hostname)

            resource_url = request.url

            try:
                page = await self._crawler.async_send(request)
            except (TypeError, UnicodeDecodeError) as exception:
                print("{} with url {}".format(exception,
                                              resource_url))  # debug
                return False, []
            # except SSLError:
            #     print(_("[!] SSL/TLS error occurred with URL"), resource_url)
            #     return False, []
            # TODO: what to do of connection errors ? sleep a while before retrying ?
            except ConnectionError:
                print(_("[!] Connection error with URL"), resource_url)
                return False, []
            except httpx.RequestError as error:
                print(
                    _("[!] {} with url {}").format(error.__class__.__name__,
                                                   resource_url))
                return False, []

            if self._max_files_per_dir:
                async with self._shared_lock:
                    self._file_counts[dir_name] += 1

            if self._qs_limit and request.parameters_count:
                async with self._shared_lock:
                    self._pattern_counts[request.pattern] += 1

            if request.link_depth == self._max_depth:
                # We are at the edge of the depth so next links will have depth + 1 so to need to parse the page.
                return True, []

            # Above this line we need the content of the page. As we are in stream mode we must force reading the body.
            await page.read()

            # Sur les ressources statiques le content-length est généralement indiqué
            if self._max_page_size > 0:
                if page.raw_size > self._max_page_size:
                    await page.clean()
                    return False, []

            await asyncio.sleep(0)
            resources = self.extract_links(page, request)
            # TODO: there's more situations where we would not want to attack the resource... must check this
            if page.is_directory_redirection:
                return False, resources

            return True, resources
Ejemplo n.º 18
0
    def extract_links(self, page, request) -> List:
        swf_links = []
        js_links = []
        allowed_links = []

        new_requests = []

        if "application/x-shockwave-flash" in page.type or request.file_ext == "swf":
            try:
                swf_links = swf.extract_links_from_swf(page.bytes)
            except Exception:
                pass
        elif "/x-javascript" in page.type or "/x-js" in page.type or "/javascript" in page.type:
            js_links = lamejs.LameJs(page.content).get_links()
            js_links += jsparser_angular.JsParserAngular(
                page.url, page.content).get_links()

        elif page.type.startswith(MIME_TEXT_TYPES):
            allowed_links.extend(filter(self._crawler.is_in_scope, page.links))
            allowed_links.extend(
                filter(self._crawler.is_in_scope,
                       page.js_redirections + page.html_redirections))

            for extra_url in filter(self._crawler.is_in_scope,
                                    page.extra_urls):
                parts = urlparse(extra_url)
                # There are often css and js URLs with useless parameters like version or random number
                # used to prevent caching in browser. So let's exclude those extensions
                if parts.path.endswith(".css"):
                    continue

                if parts.path.endswith(".js") and parts.query:
                    # For JS script, allow to process them but remove parameters
                    allowed_links.append(extra_url.split("?")[0])
                    continue

                allowed_links.append(extra_url)

            for form in page.iter_forms():
                # TODO: apply bad_params filtering in form URLs
                if self._crawler.is_in_scope(form):
                    if form.hostname not in self._hostnames:
                        form.link_depth = 0
                    else:
                        form.link_depth = request.link_depth + 1

                    new_requests.append(form)

        for url in swf_links + js_links:
            if url:
                url = page.make_absolute(url)
                if url and self._crawler.is_in_scope(url):
                    allowed_links.append(url)

        for new_url in allowed_links:
            if "?" in new_url:
                path_only = new_url.split("?")[0]
                if path_only not in allowed_links and self._crawler.is_in_scope(
                        path_only):
                    allowed_links.append(path_only)

        for new_url in set(allowed_links):
            if new_url == "":
                continue

            if self.is_forbidden(new_url):
                continue

            if "?" in new_url:
                path, query_string = new_url.split("?", 1)
                # TODO: encoding parameter ?
                get_params = [
                    list(t) for t in filter(
                        lambda param_tuple: param_tuple[0] not in self.
                        _bad_params, web.parse_qsl(query_string))
                ]
            elif new_url.endswith(EXCLUDED_MEDIA_EXTENSIONS):
                # exclude static media files
                continue
            else:
                path = new_url
                get_params = []

            if page.is_directory_redirection and new_url == page.redirection_url:
                depth = request.link_depth
            else:
                depth = request.link_depth + 1

            new_requests.append(
                web.Request(path, get_params=get_params, link_depth=depth))

        return new_requests
Ejemplo n.º 19
0
    def attack(self):
        http_resources = self.persister.get_links(
            attack_module=self.name) if self.do_get else []
        forms = self.persister.get_forms(
            attack_module=self.name) if self.do_post else []

        for original_request in chain(http_resources, forms):
            # We leverage the fact that the crawler will fill password entries with a known placeholder
            if "Letm3in_" not in (original_request.encoded_data +
                                  original_request.encoded_params):
                continue

            # We may want to remove this but if not available fallback to target URL
            if not original_request.referer:
                continue

            if self.verbose >= 1:
                print("[+] {}".format(original_request))

            request = Request(original_request.referer)
            page = self.crawler.get(request, follow_redirects=True)

            login_form, username_field_idx, password_field_idx = page.find_login_form(
            )
            if not login_form:
                continue

            failure_text = self.test_credentials(login_form,
                                                 username_field_idx,
                                                 password_field_idx, "invalid",
                                                 "invalid")

            if self.check_success_auth(failure_text):
                # Ignore this case as it raise false positives
                continue

            for username, password in product(self.get_usernames(),
                                              self.get_passwords()):
                response = self.test_credentials(login_form,
                                                 username_field_idx,
                                                 password_field_idx, username,
                                                 password)

                if self.check_success_auth(
                        response) and failure_text != response:
                    vuln_message = _(
                        "Credentials found for URL {} : {} / {}").format(
                            original_request.referer, username, password)

                    # Recreate the request that succeed in order to print and store it
                    post_params = login_form.post_params
                    get_params = login_form.get_params

                    if login_form.method == "POST":
                        post_params[username_field_idx][1] = username
                        post_params[password_field_idx][1] = password
                    else:
                        get_params[username_field_idx][1] = username
                        get_params[password_field_idx][1] = password

                    evil_request = web.Request(
                        path=login_form.url,
                        method=login_form.method,
                        post_params=post_params,
                        get_params=get_params,
                        referer=login_form.referer,
                        link_depth=login_form.link_depth)

                    self.add_vuln(request_id=original_request.path_id,
                                  category=Vulnerability.WEAK_CREDENTIALS,
                                  level=Vulnerability.HIGH_LEVEL,
                                  request=evil_request,
                                  info=vuln_message)

                    self.log_red("---")
                    self.log_red(vuln_message),
                    self.log_red(Vulnerability.MSG_EVIL_REQUEST)
                    self.log_red(evil_request.http_repr())
                    self.log_red("---")

                    break

            yield original_request
Ejemplo n.º 20
0
    def explore(self, to_explore: deque, excluded_urls: list = None):
        """Explore a single TLD or the whole Web starting with an URL

        @param to_explore: A list of URL to scan the scan with.
        @type to_explore: list
        @param excluded_urls: A list of URLs to skip. Request objects or strings which may contain wildcards.
        @type excluded_urls: list

        @rtype: generator
        """
        invalid_page = "zqxj{0}.html".format("".join(
            [choice(ascii_letters) for __ in range(10)]))

        if isinstance(excluded_urls, list):
            while True:
                try:
                    bad_request = excluded_urls.pop()
                except IndexError:
                    break
                else:
                    if isinstance(bad_request, str):
                        self._regexes.append(wildcard_translate(bad_request))
                    elif isinstance(bad_request, web.Request):
                        self._excluded_requests.append(bad_request)

        self._crawler._session.stream = True

        if self._max_depth < 0:
            raise StopIteration

        while to_explore:
            request = to_explore.popleft()
            if not isinstance(request, web.Request):
                # We treat start_urls as if they are all valid URLs (ie in scope)
                request = web.Request(request, link_depth=0)

            if request in self._excluded_requests:
                continue

            resource_url = request.url

            if request.link_depth > self._max_depth:
                continue

            dir_name = request.dir_name
            if self._max_files_per_dir and self._file_counts[
                    dir_name] >= self._max_files_per_dir:
                continue

            # Won't enter if qs_limit is 0 (aka insane mode)
            if self._qs_limit:
                if request.parameters_count:
                    try:
                        if self._pattern_counts[request.pattern] >= 220 / (
                                math.exp(request.parameters_count *
                                         self._qs_limit)**2):
                            continue
                    except OverflowError:
                        # Oh boy... that's not good to try to attack a form with more than 600 input fields
                        # but I guess insane mode can do it as it is insane
                        continue

            if self.is_forbidden(resource_url):
                continue

            if self._log:
                print("[+] {0}".format(request))

            if dir_name not in self._custom_404_codes:
                invalid_resource = web.Request(dir_name + invalid_page)
                try:
                    page = self._crawler.get(invalid_resource)
                    self._custom_404_codes[dir_name] = page.status
                except RequestException:
                    pass

            self._hostnames.add(request.hostname)

            try:
                page = self._crawler.send(request)
            except (TypeError, UnicodeDecodeError) as exception:
                print("{} with url {}".format(exception,
                                              resource_url))  # debug
                continue
            except SSLError:
                print(_("[!] SSL/TLS error occurred with URL"), resource_url)
                continue
            # TODO: what to do of connection errors ? sleep a while before retrying ?
            except ConnectionError:
                print(_("[!] Connection error with URL"), resource_url)
                continue
            except RequestException as error:
                print(
                    _("[!] {} with url {}").format(error.__class__.__name__,
                                                   resource_url))
                continue

            if self._max_files_per_dir:
                self._file_counts[dir_name] += 1

            if self._qs_limit and request.parameters_count:
                self._pattern_counts[request.pattern] += 1

            self._excluded_requests.append(request)

            # Sur les ressources statiques le content-length est généralement indiqué
            if self._max_page_size > 0:
                if page.raw_size > self._max_page_size:
                    page.clean()
                    continue

            # TODO: there's more situations where we would not want to attack the resource... must check this
            if not page.is_directory_redirection:
                yield request

            if request.link_depth == self._max_depth:
                # We are at the edge of the depth so next links will have depth + 1 so to need to parse the page.
                continue

            accepted_urls = 0
            for unfiltered_request in self.extract_links(page, request):
                if BAD_URL_REGEX.search(unfiltered_request.file_path):
                    # Malformed link due to HTML issues
                    continue

                if not self._crawler.is_in_scope(unfiltered_request):
                    continue

                if unfiltered_request.hostname not in self._hostnames:
                    unfiltered_request.link_depth = 0

                if unfiltered_request not in self._excluded_requests and unfiltered_request not in to_explore:
                    to_explore.append(unfiltered_request)
                    accepted_urls += 1

                # TODO: fix this, it doesn't looks valid
                # if self._max_per_depth and accepted_urls >= self._max_per_depth:
                #     break

        self._crawler._session.stream = False
Ejemplo n.º 21
0
    def attack(self, get_resources, forms):
        """This method searches XSS which could be permanently stored in the web application"""
        for original_request in get_resources:
            # First we will crawl again each webpage to look for tainted value the mod_xss module may have injected.
            # So let's skip methods other than GET.
            if original_request.method != "GET":
                continue

            url = original_request.url
            target_req = web.Request(url)
            referer = original_request.referer
            headers = {}

            if referer:
                headers["referer"] = referer
            if self.verbose >= 1:
                print("[+] {}".format(url))

            try:
                response = self.crawler.send(target_req, headers=headers)
                data = response.content
            except Timeout:
                data = ""
            except OSError as exception:
                data = ""
                # TODO: those error messages are useless, don't give any valuable information
                print(
                    _("error: {0} while attacking {1}").format(
                        exception.strerror, url))
            except Exception as exception:
                print(
                    _("error: {0} while attacking {1}").format(exception, url))
                continue

            # Should we look for taint codes sent with GET in the webpages?
            # Exploiting those may imply sending more GET requests
            if self.do_get == 1:
                # Search in the page source for every taint code used by mod_xss
                for taint in self.GET_XSS:
                    if taint in data:
                        # code found in the webpage !
                        code_url = self.GET_XSS[taint][0].url
                        page = self.GET_XSS[taint][0].path
                        parameter = self.GET_XSS[taint][1]

                        # Did mod_xss saw this as a reflected XSS ?
                        if taint in self.SUCCESSFUL_XSS:
                            # Yes, it means XSS payloads were injected, not just tainted code.

                            if self.valid_xss(data, taint,
                                              self.SUCCESSFUL_XSS[taint]):
                                # If we can find the payload again, this is in fact a stored XSS
                                evil_request = web.Request(
                                    code_url.replace(
                                        taint, self.SUCCESSFUL_XSS[taint]))

                                self.log_red("---")
                                if parameter == "QUERY_STRING":
                                    injection_msg = Vulnerability.MSG_QS_INJECT
                                else:
                                    injection_msg = Vulnerability.MSG_PARAM_INJECT

                                self.log_red(injection_msg, self.MSG_VULN,
                                             page, parameter)
                                self.log_red(Vulnerability.MSG_EVIL_URL,
                                             code_url)
                                self.log_red("---")

                                self.add_vuln(
                                    request_id=original_request.path_id,
                                    category=Vulnerability.XSS,
                                    level=Vulnerability.HIGH_LEVEL,
                                    request=evil_request,
                                    parameter=parameter,
                                    info=_("Found permanent XSS in {0}"
                                           " with {1}").format(
                                               page, escape(evil_request.url)))
                                # we reported the vuln, now search another code
                                continue

                        # Ok the content is stored, but will we be able to inject javascript?
                        else:
                            timeouted = False
                            saw_internal_error = False

                            for xss, flags in self.independant_payloads:
                                payload = xss.replace("__XSS__", taint)
                                evil_request = web.Request(
                                    code_url.replace(taint, payload))
                                try:
                                    http_code = self.crawler.send(
                                        evil_request).status
                                    dat = self.crawler.send(target_req).content
                                except ReadTimeout:
                                    dat = ""
                                    if timeouted:
                                        continue

                                    self.log_orange("---")
                                    self.log_orange(Anomaly.MSG_TIMEOUT, page)
                                    self.log_orange(Anomaly.MSG_EVIL_REQUEST)
                                    self.log_orange(evil_request.http_repr())
                                    self.log_orange("---")

                                    self.add_anom(
                                        request_id=original_request.path_id,
                                        category=Anomaly.RES_CONSUMPTION,
                                        level=Anomaly.MEDIUM_LEVEL,
                                        request=evil_request,
                                        parameter=parameter,
                                        info=Anomaly.MSG_PARAM_TIMEOUT.format(
                                            parameter))
                                    timeouted = True

                                except Exception as exception:
                                    print(
                                        _('error: {0} while attacking {1}').
                                        format(exception, url))
                                    continue

                                if self.valid_xss(dat, taint, payload):
                                    # injection successful :)
                                    if parameter == "QUERY_STRING":
                                        injection_msg = Vulnerability.MSG_QS_INJECT
                                    else:
                                        injection_msg = Vulnerability.MSG_PARAM_INJECT

                                    self.log_red("---")
                                    self.log_red(injection_msg, self.MSG_VULN,
                                                 page, parameter)
                                    self.log_red(
                                        Vulnerability.MSG_EVIL_REQUEST)
                                    self.log_red(evil_request.http_repr())
                                    self.log_red("---")

                                    self.add_vuln(
                                        request_id=original_request.path_id,
                                        category=Vulnerability.XSS,
                                        level=Vulnerability.HIGH_LEVEL,
                                        request=evil_request,
                                        parameter=parameter,
                                        info=_("Found permanent XSS in {0}"
                                               " with {1}").format(
                                                   url,
                                                   escape(evil_request.url)))
                                    # look for another code in the webpage
                                    break
                                elif http_code == 500 and not saw_internal_error:
                                    self.add_anom(
                                        request_id=original_request.path_id,
                                        category=Anomaly.ERROR_500,
                                        level=Anomaly.HIGH_LEVEL,
                                        request=evil_request,
                                        parameter=parameter,
                                        info=Anomaly.MSG_PARAM_500.format(
                                            parameter))

                                    self.log_orange("---")
                                    self.log_orange(Anomaly.MSG_500, page)
                                    self.log_orange(Anomaly.MSG_EVIL_REQUEST)
                                    self.log_orange(evil_request.http_repr())
                                    self.log_orange("---")
                                    saw_internal_error = True

            # Should we look for taint codes sent with POST in the webpages?
            # Exploiting those may probably imply sending more POST requests
            if self.do_post == 1:
                for taint in self.POST_XSS:
                    if taint in data:
                        # Code found in the webpage!
                        # Did mod_xss saw this as a reflected XSS ?
                        if taint in self.SUCCESSFUL_XSS:
                            if self.valid_xss(data, taint,
                                              self.SUCCESSFUL_XSS[taint]):

                                code_req = self.POST_XSS[taint][0]
                                get_params = code_req.get_params
                                post_params = code_req.post_params
                                file_params = code_req.file_params
                                referer = code_req.referer

                                for params_list in [
                                        get_params, post_params, file_params
                                ]:
                                    for i in range(len(params_list)):
                                        parameter, value = params_list[i]
                                        parameter = quote(parameter)
                                        if value == taint:
                                            if params_list is file_params:
                                                params_list[i][1][
                                                    0] = self.SUCCESSFUL_XSS[
                                                        taint]
                                            else:
                                                params_list[i][
                                                    1] = self.SUCCESSFUL_XSS[
                                                        taint]

                                            # we found the xss payload again -> stored xss vuln
                                            evil_request = web.Request(
                                                code_req.path,
                                                method="POST",
                                                get_params=get_params,
                                                post_params=post_params,
                                                file_params=file_params,
                                                referer=referer)

                                            self.add_vuln(
                                                request_id=original_request.
                                                path_id,
                                                category=Vulnerability.XSS,
                                                level=Vulnerability.HIGH_LEVEL,
                                                request=evil_request,
                                                parameter=parameter,
                                                info=_(
                                                    "Found permanent XSS attacked by {0} with fields"
                                                    " {1}").format(
                                                        evil_request.url,
                                                        encode(post_params)))

                                            self.log_red("---")
                                            self.log_red(
                                                Vulnerability.MSG_PARAM_INJECT,
                                                self.MSG_VULN,
                                                evil_request.path, parameter)
                                            self.log_red(
                                                Vulnerability.MSG_EVIL_REQUEST)
                                            self.log_red(
                                                evil_request.http_repr())
                                            self.log_red("---")
                                            # search for the next code in the webpage
                                    continue

                        # we found the code but no attack was made
                        # let's try to break in
                        else:
                            code_req = self.POST_XSS[taint][0]
                            get_params = code_req.get_params
                            post_params = code_req.post_params
                            file_params = code_req.file_params
                            referer = code_req.referer

                            for params_list in [
                                    get_params, post_params, file_params
                            ]:
                                for i in range(len(params_list)):
                                    parameter, value = params_list[i]
                                    parameter = quote(parameter)
                                    if value == taint:
                                        timeouted = False
                                        saw_internal_error = False
                                        for xss, flags in self.independant_payloads:
                                            payload = xss.replace(
                                                "__XSS__", taint)

                                            if params_list is file_params:
                                                params_list[i][1][0] = payload
                                            else:
                                                params_list[i][1] = payload

                                            try:
                                                evil_request = web.Request(
                                                    code_req.path,
                                                    method=code_req.method,
                                                    get_params=get_params,
                                                    post_params=post_params,
                                                    file_params=file_params,
                                                    referer=referer)
                                                http_code = self.crawler.send(
                                                    evil_request).status
                                                dat = self.crawler.send(
                                                    target_req).content
                                            except ReadTimeout:
                                                dat = ""
                                                if timeouted:
                                                    continue

                                                self.log_orange("---")
                                                self.log_orange(
                                                    Anomaly.MSG_TIMEOUT,
                                                    evil_request.url)
                                                self.log_orange(
                                                    Anomaly.MSG_EVIL_REQUEST)
                                                self.log_orange(
                                                    evil_request.http_repr())
                                                self.log_orange("---")

                                                self.add_anom(
                                                    request_id=original_request
                                                    .path_id,
                                                    category=Anomaly.
                                                    RES_CONSUMPTION,
                                                    level=Anomaly.MEDIUM_LEVEL,
                                                    request=evil_request,
                                                    parameter=parameter,
                                                    info=Anomaly.
                                                    MSG_PARAM_TIMEOUT.format(
                                                        parameter))
                                                timeouted = True
                                            except Exception as exception:
                                                print(
                                                    _("error: {0} while attacking {1}"
                                                      ).format(exception, url))
                                                continue

                                            if self.valid_xss(
                                                    dat, taint, payload):
                                                self.add_vuln(
                                                    request_id=original_request
                                                    .path_id,
                                                    category=Vulnerability.XSS,
                                                    level=Vulnerability.
                                                    HIGH_LEVEL,
                                                    request=evil_request,
                                                    parameter=parameter,
                                                    info=
                                                    _("Found permanent XSS attacked by {0} with fields"
                                                      " {1}").format(
                                                          evil_request.url,
                                                          encode(post_params)))

                                                self.log_red("---")
                                                self.log_red(
                                                    Vulnerability.
                                                    MSG_PARAM_INJECT,
                                                    self.MSG_VULN,
                                                    evil_request.path,
                                                    parameter)
                                                self.log_red(Vulnerability.
                                                             MSG_EVIL_REQUEST)
                                                self.log_red(
                                                    evil_request.http_repr())
                                                self.log_red("---")
                                                break
                                            elif http_code == 500 and not saw_internal_error:
                                                self.add_anom(
                                                    request_id=original_request
                                                    .path_id,
                                                    category=Anomaly.ERROR_500,
                                                    level=Anomaly.HIGH_LEVEL,
                                                    request=evil_request,
                                                    parameter=parameter,
                                                    info=Anomaly.MSG_PARAM_500.
                                                    format(parameter))

                                                self.log_orange("---")
                                                self.log_orange(
                                                    Anomaly.MSG_500,
                                                    evil_request.url)
                                                self.log_orange(
                                                    Anomaly.MSG_EVIL_REQUEST)
                                                self.log_orange(
                                                    evil_request.http_repr())
                                                self.log_orange("---")
                                                saw_internal_error = True

            yield original_request
Ejemplo n.º 22
0
    def attack(self):
        """This method searches XSS which could be permanently stored in the web application"""
        get_resources = self.persister.get_links(attack_module=self.name) if self.do_get else []

        for original_request in get_resources:
            if not valid_xss_content_type(original_request) or original_request.status in (301, 302, 303):
                # If that content-type can't be interpreted as HTML by browsers then it is useless
                # Same goes for redirections
                continue

            url = original_request.url
            target_req = web.Request(url)
            referer = original_request.referer
            headers = {}

            if referer:
                headers["referer"] = referer
            if self.verbose >= 1:
                print("[+] {}".format(url))

            try:
                response = self.crawler.send(target_req, headers=headers)
                data = response.content
            except Timeout:
                continue
            except OSError as exception:
                # TODO: those error messages are useless, don't give any valuable information
                print(_("error: {0} while attacking {1}").format(exception.strerror, url))
                continue
            except Exception as exception:
                print(_("error: {0} while attacking {1}").format(exception, url))
                continue

            # Should we look for taint codes sent with GET in the webpages?
            # Exploiting those may imply sending more GET requests

            # Search in the page source for every taint code used by mod_xss
            for taint in self.TRIED_XSS:
                input_request = self.TRIED_XSS[taint][0]

                # Such situations should not occur as it would be stupid to block POST (or GET) requests for mod_xss
                # and not mod_permanentxss, but it is possible so let's filter that.
                if not self.do_get and input_request.method == "GET":
                    continue

                if not self.do_post and input_request.method == "POST":
                    continue

                if taint.lower() in data.lower():
                    # Code found in the webpage !
                    # Did mod_xss saw this as a reflected XSS ?
                    if taint in self.SUCCESSFUL_XSS:
                        # Yes, it means XSS payloads were injected, not just tainted code.
                        payload, flags = self.SUCCESSFUL_XSS[taint]

                        if self.check_payload(response, flags, taint):
                            # If we can find the payload again, this is in fact a stored XSS
                            get_params = input_request.get_params
                            post_params = input_request.post_params
                            file_params = input_request.file_params
                            referer = input_request.referer

                            # The following trick may seems dirty but it allows to treat GET and POST requests
                            # the same way.
                            for params_list in [get_params, post_params, file_params]:
                                for i in range(len(params_list)):
                                    parameter, value = params_list[i]
                                    parameter = quote(parameter)
                                    if value != taint:
                                        continue

                                    if params_list is file_params:
                                        params_list[i][1][0] = payload
                                    else:
                                        params_list[i][1] = payload

                                    # we found the xss payload again -> stored xss vuln
                                    evil_request = web.Request(
                                        input_request.path,
                                        method=input_request.method,
                                        get_params=get_params,
                                        post_params=post_params,
                                        file_params=file_params,
                                        referer=referer
                                    )

                                    if original_request.path == input_request.path:
                                        description = _(
                                            "Permanent XSS vulnerability found via injection in the parameter {0}"
                                        ).format(parameter)
                                    else:
                                        description = _(
                                            "Permanent XSS vulnerability found in {0} by injecting"
                                            " the parameter {1} of {2}"
                                        ).format(
                                            original_request.url,
                                            parameter,
                                            input_request.path
                                        )

                                    if has_csp(response):
                                        description += ".\n" + _("Warning: Content-Security-Policy is present!")

                                    self.add_vuln(
                                        request_id=original_request.path_id,
                                        category=Vulnerability.XSS,
                                        level=Vulnerability.HIGH_LEVEL,
                                        request=evil_request,
                                        parameter=parameter,
                                        info=description
                                    )

                                    if parameter == "QUERY_STRING":
                                        injection_msg = Vulnerability.MSG_QS_INJECT
                                    else:
                                        injection_msg = Vulnerability.MSG_PARAM_INJECT

                                    self.log_red("---")
                                    self.log_red(
                                        injection_msg,
                                        self.MSG_VULN,
                                        original_request.path,
                                        parameter
                                    )

                                    if has_csp(response):
                                        self.log_red(_("Warning: Content-Security-Policy is present!"))

                                    self.log_red(Vulnerability.MSG_EVIL_REQUEST)
                                    self.log_red(evil_request.http_repr())
                                    self.log_red("---")
                                    # FIX: search for the next code in the webpage

                    # Ok the content is stored, but will we be able to inject javascript?
                    else:
                        parameter = self.TRIED_XSS[taint][1]
                        payloads = generate_payloads(response.content, taint, self.independant_payloads)
                        flags = self.TRIED_XSS[taint][2]

                        # TODO: check that and make it better
                        if PayloadType.get in flags:
                            method = "G"
                        elif PayloadType.file in flags:
                            method = "F"
                        else:
                            method = "P"

                        self.attempt_exploit(method, payloads, input_request, parameter, taint, original_request)

            yield original_request
Ejemplo n.º 23
0
    async def _get_paths(self,
                         path=None,
                         method=None,
                         crawled: bool = True,
                         module: str = "",
                         evil: bool = False):
        conditions = [paths.c.evil == evil]

        if path and isinstance(path, str):
            conditions.append(paths.c.path == path)

        if method in ("GET", "POST"):
            conditions.append(paths.c.method == method)

        if crawled:
            conditions.append(paths.c.headers != None)

        async with self._engine.begin() as conn:
            result = await conn.execute(
                select(paths).where(and_(True,
                                         *conditions)).order_by(paths.c.path))

        for row in result.fetchall():
            path_id = row[0]

            if module:
                # Exclude requests matching the attack module, we want requests that aren't attacked yet
                statement = select(attack_logs).where(
                    attack_logs.c.path_id == path_id,
                    attack_logs.c.module == module).limit(1)
                async with self._engine.begin() as conn:
                    result = await conn.execute(statement)

                if result.fetchone():
                    continue

            get_params = []
            post_params = []
            file_params = []

            statement = select(
                params.c.type, params.c.name, params.c.value1, params.c.value2,
                params.c.meta).where(params.c.path_id == path_id).order_by(
                    params.c.type, params.c.position)

            async with self._engine.begin() as conn:
                async_result = await conn.stream(statement)

                async for param_row in async_result:
                    name = param_row[1]
                    value1 = param_row[2]

                    if param_row[0] == "GET":
                        get_params.append([name, value1])
                    elif param_row[0] == "POST":
                        if name == "__RAW__" and not post_params:
                            # First POST parameter is __RAW__, it should mean that we have raw content
                            post_params = value1
                        elif isinstance(post_params, list):
                            post_params.append([name, value1])
                    elif param_row[0] == "FILE":
                        if param_row[4]:
                            file_params.append(
                                [name, (value1, param_row[3], param_row[4])])
                        else:
                            file_params.append([name, (value1, param_row[3])])
                    else:
                        raise ValueError("Unknown param type {}".format(
                            param_row[0]))

                http_res = web.Request(row[1],
                                       method=row[2],
                                       encoding=row[5],
                                       enctype=row[3],
                                       referer=row[8],
                                       get_params=get_params,
                                       post_params=post_params,
                                       file_params=file_params)

                if row[6]:
                    http_res.status = row[6]

                if row[7]:
                    http_res.set_headers(row[7])

                http_res.link_depth = row[4]
                http_res.path_id = path_id

                yield http_res
Ejemplo n.º 24
0
    def _get_paths(self,
                   path=None,
                   method=None,
                   crawled: bool = True,
                   attack_module: str = "",
                   evil: bool = False):
        cursor = self._conn.cursor()

        conditions = ["evil = ?"]
        args = [int(evil)]

        if path and isinstance(path, str):
            conditions.append("path = ?")
            args.append(path)

        if method in ("GET", "POST"):
            conditions.append("method = ?")
            args.append(method)

        if crawled:
            conditions.append("headers IS NOT NULL")

        conditions = " AND ".join(conditions)
        conditions = "WHERE " + conditions

        cursor.execute(
            "SELECT * FROM paths {} ORDER BY path".format(conditions), args)

        for row in cursor.fetchall():
            path_id = row[0]

            if attack_module:
                # Exclude requests matching the attack module, we want requests that aren't attacked yet
                cursor.execute(
                    "SELECT * FROM attack_log WHERE path_id = ? AND module_name = ? LIMIT 1",
                    (path_id, attack_module))

                if cursor.fetchone():
                    continue

            get_params = []
            post_params = []
            file_params = []

            for param_row in cursor.execute(
                ("SELECT type, name, value1, value2, meta "
                 "FROM params "
                 "WHERE path_id = ? "
                 "ORDER BY type, param_order"), (path_id, )):
                name = param_row[1]
                value1 = param_row[2]

                if param_row[0] == "GET":
                    get_params.append([name, value1])
                elif param_row[0] == "POST":
                    if name == "__RAW__" and not post_params:
                        # First POST parameter is __RAW__, it should mean that we have raw content
                        post_params = value1
                    elif isinstance(post_params, list):
                        post_params.append([name, value1])
                elif param_row[0] == "FILE":
                    if param_row[4]:
                        file_params.append(
                            [name, (value1, param_row[3], param_row[4])])
                    else:
                        file_params.append([name, (value1, param_row[3])])
                else:
                    raise ValueError("Unknown param type {}".format(
                        param_row[0]))

            http_res = web.Request(row[1],
                                   method=row[2],
                                   encoding=row[5],
                                   enctype=row[3],
                                   referer=row[8],
                                   get_params=get_params,
                                   post_params=post_params,
                                   file_params=file_params)

            if row[6]:
                http_res.status = row[6]

            if row[7]:
                http_res.set_headers(json.loads(row[7]))

            http_res.link_depth = row[4]
            http_res.path_id = path_id

            yield http_res
Ejemplo n.º 25
0
    async def async_explore(self,
                            to_explore: deque,
                            excluded_urls: list = None):
        """Explore a single TLD or the whole Web starting with an URL

        @param to_explore: A list of URL to scan the scan with.
        @type to_explore: list
        @param excluded_urls: A list of URLs to skip. Request objects or strings which may contain wildcards.
        @type excluded_urls: list

        @rtype: generator
        """
        if isinstance(excluded_urls, list):
            while True:
                try:
                    bad_request = excluded_urls.pop()
                except IndexError:
                    break
                else:
                    if isinstance(bad_request, str):
                        self._regexes.append(wildcard_translate(bad_request))
                    elif isinstance(bad_request, web.Request):
                        self._processed_requests.append(bad_request)

        self._crawler.stream = True

        if self._max_depth < 0:
            raise StopIteration

        task_to_request = {}
        while True:
            while to_explore:
                # Concurrent tasks are limited through the use of the semaphore BUT we don't want the to_explore
                # queue to be empty everytime (as we may need to extract remaining URLs) and overload the event loop
                # with pending tasks.
                # There may be more suitable way to do this though.
                if len(task_to_request) > self._max_tasks:
                    break

                if self._stopped.is_set():
                    break

                request = to_explore.popleft()
                if not isinstance(request, web.Request):
                    # We treat start_urls as if they are all valid URLs (ie in scope)
                    request = web.Request(request, link_depth=0)

                if request in self._processed_requests:
                    continue

                resource_url = request.url

                if request.link_depth > self._max_depth:
                    continue

                dir_name = request.dir_name
                if self._max_files_per_dir and self._file_counts[
                        dir_name] >= self._max_files_per_dir:
                    continue

                # Won't enter if qs_limit is 0 (aka insane mode)
                if self._qs_limit:
                    if request.parameters_count:
                        try:
                            if self._pattern_counts[request.pattern] >= 220 / (
                                    math.exp(request.parameters_count *
                                             self._qs_limit)**2):
                                continue
                        except OverflowError:
                            # Oh boy... that's not good to try to attack a form with more than 600 input fields
                            # but I guess insane mode can do it as it is insane
                            continue

                if self.is_forbidden(resource_url):
                    continue

                task = asyncio.create_task(self.async_analyze(request))
                task_to_request[task] = request

            if task_to_request:
                done, __ = await asyncio.wait(
                    task_to_request,
                    timeout=0.25,
                    return_when=asyncio.FIRST_COMPLETED)
            else:
                done = []

            # process any completed task
            for task in done:
                request = task_to_request[task]
                try:
                    success, resources = await task
                except Exception as exc:
                    print('%r generated an exception: %s' % (request, exc))
                else:
                    if success:
                        yield request

                    accepted_urls = 0
                    for unfiltered_request in resources:
                        if BAD_URL_REGEX.search(unfiltered_request.file_path):
                            # Malformed link due to HTML issues
                            continue

                        if not self._crawler.is_in_scope(unfiltered_request):
                            continue

                        if unfiltered_request.hostname not in self._hostnames:
                            unfiltered_request.link_depth = 0

                        if unfiltered_request not in self._processed_requests and unfiltered_request not in to_explore:
                            to_explore.append(unfiltered_request)
                            accepted_urls += 1

                        # TODO: fix this, it doesn't looks valid
                        # if self._max_per_depth and accepted_urls >= self._max_per_depth:
                        #     break

                # remove the now completed task
                del task_to_request[task]

            if not task_to_request and (self._stopped.is_set()
                                        or not to_explore):
                break

        self._crawler.stream = False
Ejemplo n.º 26
0
    def iter_forms(self, autofill=True):
        """Returns a generator of Request extracted from the Page.

        @rtype: generator
        """
        for form in self.soup.find_all("form"):
            url = self.make_absolute(form.attrs.get("action", "").strip() or self.url)
            # If no method is specified then it's GET. If an invalid method is set it's GET.
            method = "POST" if form.attrs.get("method", "GET").upper() == "POST" else "GET"
            enctype = "" if method == "GET" else form.attrs.get("enctype", "application/x-www-form-urlencoded").lower()
            get_params = []
            post_params = []
            # If the form must be sent in multipart, everything should be given to requests in the files parameter
            # but internally we use the file_params list only for file inputs sent with multipart (as they must be
            # threated differently in persister). Crawler.post() method will join post_params and file_params for us
            # if the enctype is multipart.
            file_params = []
            form_actions = set()

            defaults = {
                "checkbox": "default",
                "color": "#bada55",
                "date": "2019-03-03",
                "datetime": "2019-03-03T20:35:34.32",
                "datetime-local": "2019-03-03T22:41",
                "email": "*****@*****.**",
                "file": ("pix.gif", "GIF89a", "image/gif"),
                "hidden": "default",
                "month": "2019-03",
                "number": "1337",
                "password": "******",  # 8 characters with uppercase, digit and special char for common rules
                "radio": "beton",  # priv8 j0k3
                "range": "37",
                "search": "default",
                "submit": "submit",
                "tel": "0606060606",
                "text": "default",
                "time": "13:37",
                "url": "https://wapiti.sourceforge.io/",
                "username": "******",
                "week": "2019-W24"
            }

            radio_inputs = {}
            for input_field in form.find_all("input", attrs={"name": True}):
                input_type = input_field.attrs.get("type", "text").lower()

                if input_type in {"reset", "button"}:
                    # Those input types doesn't send any value
                    continue
                if input_type == "image":
                    if method == "GET":
                        get_params.append([input_field["name"] + ".x", "1"])
                        get_params.append([input_field["name"] + ".y", "1"])
                    else:
                        post_params.append([input_field["name"] + ".x", "1"])
                        post_params.append([input_field["name"] + ".y", "1"])
                elif input_type in defaults:
                    if input_type == "text" and "mail" in input_field["name"] and autofill:
                        # If an input text match name "mail" then put a valid email address in it
                        input_value = defaults["email"]
                    elif input_type == "text" and "pass" in input_field["name"] or \
                            "pwd" in input_field["name"] and autofill:
                        # Looks like a text field but waiting for a password
                        input_value = defaults["password"]
                    elif input_type == "text" and "user" in input_field["name"] or \
                            "login" in input_field["name"] and autofill:
                        input_value = defaults["username"]
                    else:
                        input_value = input_field.get("value", defaults[input_type] if autofill else "")

                    if input_type == "file":
                        # With file inputs the content is only sent if the method is POST and enctype multipart
                        # otherwise only the file name is sent.
                        # Having a default value set in HTML for a file input doesn't make sense... force our own.
                        if method == "GET":
                            get_params.append([input_field["name"], "pix.gif"])
                        else:
                            if "multipart" in enctype:
                                file_params.append([input_field["name"], defaults["file"]])
                            else:
                                post_params.append([input_field["name"], "pix.gif"])
                    elif input_type == "radio":
                        # Do not put in forms now, do it at the end
                        radio_inputs[input_field["name"]] = input_value
                    else:
                        if method == "GET":
                            get_params.append([input_field["name"], input_value])
                        else:
                            post_params.append([input_field["name"], input_value])

            # A formaction doesn't need a name
            for input_field in form.find_all("input", attrs={"formaction": True}):
                form_actions.add(self.make_absolute(input_field["formaction"].strip() or self.url))

            for button_field in form.find_all("button", formaction=True):
                # If formaction is empty it basically send to the current URL
                # which can be different from the defined action attribute on the form...
                form_actions.add(self.make_absolute(button_field["formaction"].strip() or self.url))

            if form.find("input", attrs={"name": False, "type": "image"}):
                # Unnamed input type file => names will be set as x and y
                if method == "GET":
                    get_params.append(["x", "1"])
                    get_params.append(["y", "1"])
                else:
                    post_params.append(["x", "1"])
                    post_params.append(["y", "1"])

            for select in form.find_all("select", attrs={"name": True}):
                all_values = []
                selected_value = None
                for option in select.find_all("option", value=True):
                    all_values.append(option["value"])
                    if "selected" in option.attrs:
                        selected_value = option["value"]

                if selected_value is None and all_values:
                    # First value may be a placeholder but last entry should be valid
                    selected_value = all_values[-1]

                if method == "GET":
                    get_params.append([select["name"], selected_value])
                else:
                    post_params.append([select["name"], selected_value])

            # if form.find("input", attrs={"type": "image", "name": False}):
            #     new_form.add_image_field()

            for text_area in form.find_all("textarea", attrs={"name": True}):
                if method == "GET":
                    get_params.append([text_area["name"], "Hi there!" if autofill else ""])
                else:
                    post_params.append([text_area["name"], "Hi there!" if autofill else ""])

            # I guess I should raise a new form for every possible radio values...
            # For the moment, just use the last value
            for radio_name, radio_value in radio_inputs.items():
                if method == "GET":
                    get_params.append([radio_name, radio_value])
                else:
                    post_params.append([radio_name, radio_value])

            if method == "POST" and not post_params and not file_params:
                # Ignore empty forms. Those are either webdev issues or forms having only "button" types that
                # only rely on JS code.
                continue

            # First raise the form with the URL specified in the action attribute
            new_form = web.Request(
                url,
                method=method,
                get_params=get_params,
                post_params=post_params,
                file_params=file_params,
                encoding=self.apparent_encoding,
                referer=self.url,
                enctype=enctype
            )
            yield new_form

            # Then if we saw some formaction attribute, raise the form with the given formaction URL
            for url in form_actions:
                new_form = web.Request(
                    url,
                    method=method,
                    get_params=get_params,
                    post_params=post_params,
                    file_params=file_params,
                    encoding=self.apparent_encoding,
                    referer=self.url,
                    enctype=enctype
                )
                yield new_form
Ejemplo n.º 27
0
    def _get_paths(self,
                   path=None,
                   method=None,
                   crawled: bool = True,
                   attack_module: str = "",
                   evil: bool = False):
        cursor = self._conn.cursor()

        conditions = ["evil = ?"]
        args = [int(evil)]

        if path and isinstance(path, str):
            conditions.append("path = ?")
            args.append(path)

        if method in ("GET", "POST"):
            conditions.append("method = ?")
            args.append(method)

        if crawled:
            conditions.append("headers IS NOT NULL")

        conditions = " AND ".join(conditions)
        conditions = "WHERE " + conditions

        cursor.execute(
            "SELECT * FROM paths {} ORDER BY path".format(conditions), args)

        for row in cursor.fetchall():
            path_id = row[0]

            if attack_module:
                # Exclude requests matching the attack module, we wan't requests that aren't attacked yet
                cursor.execute(
                    "SELECT * FROM attack_log WHERE path_id = ? AND module_name = ? LIMIT 1",
                    (path_id, attack_module))

                if cursor.fetchone():
                    continue

            get_params = []
            post_params = []
            file_params = []

            for param_row in cursor.execute(
                    "SELECT type, name, value FROM params WHERE path_id = ? ORDER BY type, param_order",
                (path_id, )):
                name = param_row[1]
                value = param_row[2]
                if param_row[0] == "GET":
                    get_params.append([name, value])
                elif param_row[0] == "POST":
                    post_params.append([name, value])
                else:
                    file_params.append([name, [value, "GIF89a", "image/gif"]])

            http_res = web.Request(row[1],
                                   method=row[2],
                                   encoding=row[5],
                                   multipart=bool(row[3]),
                                   referer=row[8],
                                   get_params=get_params,
                                   post_params=post_params,
                                   file_params=file_params)

            if row[6]:
                http_res.status = row[6]

            if row[7]:
                http_res.set_headers(json.loads(row[7]))

            http_res.link_depth = row[4]
            http_res.path_id = path_id

            yield http_res
Ejemplo n.º 28
0
    async def get_path_by_id(self, path_id):
        path_id = int(path_id)
        async with self._engine.begin() as conn:
            result = await conn.execute(
                select(paths).where(paths.c.path_id == path_id).limit(1))

        row = result.fetchone()
        if not row:
            return None

        get_params = []
        post_params = []
        file_params = []

        statement = select(
            params.c.type, params.c.name, params.c.value1, params.c.value2,
            params.c.meta).where(params.c.path_id == path_id).order_by(
                params.c.type, params.c.position)

        async with self._engine.begin() as conn:
            async_result = await conn.stream(statement)

            async for param_row in async_result:
                name = param_row[1]
                value1 = param_row[2]

                if param_row[0] == "GET":
                    get_params.append([name, value1])
                elif param_row[0] == "POST":
                    if name == "__RAW__" and not post_params:
                        # First POST parameter is __RAW__, it should mean that we have raw content
                        post_params = value1
                    elif isinstance(post_params, list):
                        post_params.append([name, value1])
                elif param_row[0] == "FILE":
                    if param_row[4]:
                        file_params.append(
                            [name, (value1, param_row[3], param_row[4])])
                    else:
                        file_params.append([name, (value1, param_row[3])])
                else:
                    raise ValueError("Unknown param type {}".format(
                        param_row[0]))

            request = web.Request(row[1],
                                  method=row[2],
                                  encoding=row[5],
                                  enctype=row[3],
                                  referer=row[8],
                                  get_params=get_params,
                                  post_params=post_params,
                                  file_params=file_params)

            if row[6]:
                request.status = row[6]

            if row[7]:
                request.set_headers(row[7])

            request.link_depth = row[4]
            request.path_id = path_id

            return request
Ejemplo n.º 29
0
    def attack(self):
        junk_string = "w" + "".join([
            random.choice("0123456789abcdefghjijklmnopqrstuvwxyz")
            for __ in range(0, 5000)
        ])
        urls = self.persister.get_links(
            attack_module=self.name) if self.do_get else []
        server = next(urls).hostname

        for line in self.nikto_db:
            match = match_or = match_and = False
            fail = fail_or = False

            osv_id = line[1]
            path = line[3]
            method = line[4]
            vuln_desc = line[10]
            post_data = line[11]

            path = path.replace("@CGIDIRS", "/cgi-bin/")
            path = path.replace("@ADMIN", "/admin/")
            path = path.replace("@NUKE", "/modules/")
            path = path.replace("@PHPMYADMIN", "/phpMyAdmin/")
            path = path.replace("@POSTNUKE", "/postnuke/")
            path = re.sub(r"JUNK\((\d+)\)",
                          lambda x: junk_string[:int(x.group(1))], path)

            if path[0] == "@":
                continue
            if not path.startswith("/"):
                path = "/" + path

            try:
                url = "http://" + server + path
            except UnicodeDecodeError:
                continue

            if method == "GET":
                evil_request = web.Request(url)
            elif method == "POST":
                evil_request = web.Request(url,
                                           post_params=post_data,
                                           method=method)
            else:
                evil_request = web.Request(url,
                                           post_params=post_data,
                                           method=method)

            if self.verbose == 2:
                if method == "GET":
                    print("[¨] {0}".format(evil_request.url))
                else:
                    print("[¨] {0}".format(evil_request.http_repr()))

            try:
                response = self.crawler.send(evil_request)
            except RequestException as exception:
                # requests bug
                yield exception
                continue
            except ValueError:
                # ValueError raised by urllib3 (Method cannot contain non-token characters), we don't want to raise
                yield
            else:
                yield

            page = response.content
            code = response.status
            raw = " ".join([x + ": " + y for x, y in response.headers.items()])
            raw += page

            # First condition (match)
            if len(line[5]) == 3 and line[5].isdigit():
                if code == int(line[5]):
                    match = True
            else:
                if line[5] in raw:
                    match = True

            # Second condition (or)
            if line[6] != "":
                if len(line[6]) == 3 and line[6].isdigit():
                    if code == int(line[6]):
                        match_or = True
                else:
                    if line[6] in raw:
                        match_or = True

            # Third condition (and)
            if line[7] != "":
                if len(line[7]) == 3 and line[7].isdigit():
                    if code == int(line[7]):
                        match_and = True
                else:
                    if line[7] in raw:
                        match_and = True
            else:
                match_and = True

            # Fourth condition (fail)
            if line[8] != "":
                if len(line[8]) == 3 and line[8].isdigit():
                    if code == int(line[8]):
                        fail = True
                else:
                    if line[8] in raw:
                        fail = True

            # Fifth condition (or)
            if line[9] != "":
                if len(line[9]) == 3 and line[9].isdigit():
                    if code == int(line[9]):
                        fail_or = True
                else:
                    if line[9] in raw:
                        fail_or = True

            if ((match or match_or) and match_and) and not (fail or fail_or):
                self.log_red("---")
                self.log_red(vuln_desc)
                self.log_red(url)

                refs = []
                if osv_id != "0":
                    refs.append("http://osvdb.org/show/osvdb/" + osv_id)

                # CERT
                cert_advisory = re.search("(CA-[0-9]{4}-[0-9]{2})", vuln_desc)
                if cert_advisory is not None:
                    refs.append("http://www.cert.org/advisories/" +
                                cert_advisory.group(0) + ".html")

                # SecurityFocus
                securityfocus_bid = re.search("BID-([0-9]{4})", vuln_desc)
                if securityfocus_bid is not None:
                    refs.append("http://www.securityfocus.com/bid/" +
                                securityfocus_bid.group(1))

                # Mitre.org
                mitre_cve = re.search("((CVE|CAN)-[0-9]{4}-[0-9]{4,})",
                                      vuln_desc)
                if mitre_cve is not None:
                    refs.append(
                        "http://cve.mitre.org/cgi-bin/cvename.cgi?name=" +
                        mitre_cve.group(0))

                # CERT Incidents
                cert_incident = re.search("(IN-[0-9]{4}-[0-9]{2})", vuln_desc)
                if cert_incident is not None:
                    refs.append("http://www.cert.org/incident_notes/" +
                                cert_incident.group(0) + ".html")

                # Microsoft Technet
                ms_bulletin = re.search("(MS[0-9]{2}-[0-9]{3})", vuln_desc)
                if ms_bulletin is not None:
                    refs.append(
                        "http://www.microsoft.com/technet/security/bulletin/" +
                        ms_bulletin.group(0) + ".asp")

                info = vuln_desc
                if refs:
                    self.log_red(_("References:"))
                    self.log_red("  {0}".format("\n  ".join(refs)))

                    info += "\n" + _("References:") + "\n"
                    info += "\n".join(refs)

                self.log_red("---")

                self.add_vuln(category=Vulnerability.NIKTO,
                              level=Vulnerability.HIGH_LEVEL,
                              request=evil_request,
                              info=info)