示例#1
0
    def __init__(self, files=None, string=None):

        self.files = files if files else []
        # Init config parser
        self.parser = configparser.ConfigParser()
        # Load default configuration
        self.parser.read_dict({"wpwatcher": self.DEFAULT_CONFIG})

        if string:
            self.parser.read_string(string)
        else:
            if not self.files:
                self.files = self.find_config_files()
                if not self.files:
                    log.info(
                        "Could not find default config: `~/.wpwatcher/wpwatcher.conf`, `~/wpwatcher.conf` or `./wpwatcher.conf`"
                    )
            else:
                for f in self.files:
                    try:
                        with open(f, "r") as fp:
                            self.parser.read_file(fp)
                    except (FileNotFoundError, OSError) as err:
                        raise ValueError(
                            "Could not read config %s. Make sure the file exists and you have correct access right."
                            % (f)
                        ) from err
示例#2
0
    def __init__(self, files=None, string=None):
        self.files = files if files else []
        # Init config parser
        self.parser = configparser.ConfigParser()
        # Load default configuration
        self.parser.read_dict({'wpwatcher': self.DEFAULT_CONFIG})

        if string:
            self.parser.read_string(string)
        elif not self.files or len(self.files) == 0:
            self.files = self.find_config_files()
            if not self.files:
                log.info(
                    "Could not find default config: `~/.wpwatcher/wpwatcher.conf`, `~/wpwatcher.conf` or `./wpwatcher.conf`"
                )
        if self.files:
            for f in self.files:
                try:
                    with open(f, 'r') as fp:
                        self.parser.read_file(fp)
                except (OSError):
                    log.error(
                        "Could not read config %s. Make sure the file exists and you have correct access right."
                        % (f))
                    raise

        # No config file notice
        else:
            log.info("No config file loaded")
示例#3
0
def print_progress_bar(count: int, total: int) -> None:
    """Helper method to print progress bar.  Stolen on the web"""
    size = 0.3  # size of progress bar
    percent = int(float(count) / float(total) * 100)
    log.info(
        f"Progress - [{'=' * int(int(percent) * size)}{' ' * int((100 - int(percent)) * size)}] {percent}% - {count} / {total}"
    )
示例#4
0
    def _notify(self, wp_site, wp_report, last_wp_report):

        # Send the report to
        if len(self.email_errors_to) > 0 and wp_report['status'] == 'ERROR':
            to = ','.join(self.email_errors_to)
        else:
            to = ','.join(wp_site['email_to'] + self.email_to)

        if not to:
            log.info(
                "Not sending WPWatcher %s email report because no email is configured for site %s"
                % (wp_report['status'], wp_report['site']))
            return

        while mail_lock.locked():
            time.sleep(0.01)

        try:
            with mail_lock:
                self.send_report(wp_report, to)
                return True

        # Handle send mail error
        except (smtplib.SMTPException, ConnectionRefusedError, TimeoutError):
            log.error("Unable to send mail report for site " + wp_site['url'] +
                      "\n" + traceback.format_exc())
            wp_report['errors'].append("Unable to send mail report" + "\n" +
                                       traceback.format_exc())
            raise RuntimeError("Unable to send mail report")
示例#5
0
 def update_wpscan(self):
     # Update wpscan database
     log.info("Updating WPScan")
     exit_code, _ = self._wpscan("--update")
     if exit_code != 0:
         log.error("Error updating WPScan")
         exit(-1)
示例#6
0
    def _handle_wpscan_err_follow_redirect(
        self, failed_process: subprocess.CompletedProcess
    ) -> subprocess.CompletedProcess:  # type: ignore [type-arg]
        """Parse URL in WPScan output and retry. 
        """
        if "The URL supplied redirects to" in failed_process.stdout:
            urls = re.findall(
                r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+",
                failed_process.stdout.split("The URL supplied redirects to")
                [1],
            )

            if len(urls) == 1:
                url = urls[0].strip()
                log.info(f"Following redirection to {url}")
                cmd = failed_process.args
                cmd[cmd.index('--url') + 1] = url
                return self._wpscan(*cmd)

            else:
                raise ValueError(
                    f"Could not parse the URL to follow in WPScan output after words 'The URL supplied redirects to'\nOutput:\n{failed_process.stdout}"
                )
        else:
            return failed_process
示例#7
0
    def interrupt(self, sig=None, frame=None):
        '''Interrupt sequence'''
        log.error("Interrupting...")
        # If called inside ThreadPoolExecutor, raise Exeception
        if not isinstance(threading.current_thread(), threading._MainThread):
            raise InterruptedError()
        # Cancel all scans
        self.cancel_pending_futures()  # future scans
        # Wait all scans finished
        self.scanner.cancel_scans()  # running scans

        # Give a 5 seconds timeout to buggy WPScan jobs to finish or ignore them
        try:
            timeout(5, self.executor.shutdown, kwargs=dict(wait=True))
        except TimeoutError:
            pass

        # Recover reports from futures results
        new_reports = []
        for f in self.futures:
            if f.done():
                try:
                    new_reports.append(f.result())
                except Exception:
                    pass

        # Display results and quit
        self.print_scanned_sites_results(new_reports)
        log.info("Scans interrupted.")
        sys.exit(-1)
示例#8
0
    def _run_scans(self, wp_sites: List[Site]) -> ReportCollection:
        """
        Helper method to deal with :

        - executor, concurent futures
        - Trigger self.interrupt() on InterruptedError (raised if fail fast enabled)
        - Append result to `self.new_reports` list.
        """

        log.info(f"Starting scans on {len(wp_sites)} configured sites")

        # reset new reports and scanned sites list.
        self._futures.clear()
        self.new_reports.clear()
        self.scanner.scanned_sites.clear()

        for wp_site in wp_sites:
            self._futures.append(
                self._executor.submit(self._scan_site, wp_site))
        for f in self._futures:
            try:
                self.new_reports.append(f.result())
            except concurrent.futures.CancelledError:
                pass
        # Ensure everything is down
        self._cancel_pending_futures()
        return self.new_reports
示例#9
0
    def interrupt(self,
                  sig=None,
                  frame=None) -> None:  # type: ignore [no-untyped-def]
        """Interrupt the program and exit. """
        log.error("Interrupting...")
        # If called inside ThreadPoolExecutor, raise Exeception
        if not isinstance(
                threading.current_thread(),
                threading._MainThread):  # type: ignore [attr-defined]
            raise InterruptedError()

        self.interrupt_scans()

        # Give a 5 seconds timeout to buggy WPScan jobs to finish or ignore them
        try:
            timeout(5, self._executor.shutdown, kwargs=dict(wait=True))
        except TimeoutError:
            pass

        # Display results
        log.info(repr(self.new_reports))
        log.info("Scans interrupted.")

        # and quit
        sys.exit(-1)
示例#10
0
    def run_scans_and_notify(self):

        # Check sites are in the config
        if len(self.wp_sites) == 0:
            log.error(
                "No sites configured, please provide wp_sites in config file or use arguments --url URL [URL...] or --urls File path"
            )
            return ((-1, []))

        new_reports = self.run_scans_wrapper(self.wp_sites)
        # Print results and finish
        self.print_scanned_sites_results(new_reports)

        # Second scans if needed
        if len(self.scanner.prescanned_sites_warn) > 0:
            new_reports += self.re_run_scans(
                self.scanner.prescanned_sites_warn)
            self.print_scanned_sites_results(new_reports)

        if not any([r['status'] == 'ERROR' for r in new_reports if r]):
            log.info("Scans finished successfully.")
            return ((0, new_reports))
        else:
            log.info("Scans finished with errors.")
            return ((-1, new_reports))
示例#11
0
 def update_wpscan(self):
     # Update wpscan database
     log.info("Updating WPScan")
     exit_code, out = self._wpscan("--update", "--format", "json",
                                   "--no-banner")
     if exit_code != 0:
         raise Exception("Error updating WPScan.\nOutput:\n{}".format(out))
示例#12
0
    def _scan_site(self, wp_site: Site) -> Optional[ScanReport]:
        """
        Helper method to wrap the scanning process of `WPWatcherScanner.scan_site` and add the following:
        
        - Find the last report in the database and launch the scan
        - Write it in DB after scan.
        - Print progress bar

        This function can be called asynchronously.
        """

        last_wp_report = self.wp_reports.find(ScanReport(site=wp_site["url"]))

        # Launch scanner
        wp_report = self.scanner.scan_site(wp_site, last_wp_report)

        # Save report in global instance database and to file when a site has been scanned
        if wp_report:
            self.wp_reports.write([wp_report])
        else:
            log.info(f"No report saved for site {wp_site['url']}")

        # Print progress
        print_progress_bar(len(self.scanner.scanned_sites), len(self.wp_sites))
        return wp_report
示例#13
0
    def scan_site_wrapper(self, wp_site):
        """
        Helper method to wrap the raw scanning process that offer WPWatcherScanner.scan_site() and add the following:
        - Handle site structure formatting
        - Find the last report in the database and launch the scan
        - Write it in DB after scan.
        - Print progress bar
        This function will be called asynchronously.
        Return one report
        """

        wp_site = self.format_site(wp_site)
        last_wp_report = self.wp_reports.find_last_wp_report(
            {"site": wp_site["url"]})

        # Launch scanner
        wp_report = self.scanner.scan_site(wp_site, last_wp_report)
        # Save report in global instance database and to file when a site has been scanned
        if wp_report:
            self.wp_reports.update_and_write_wp_reports([wp_report])
        else:
            log.info("No report saved for site %s" % wp_site["url"])
        # Print progress
        print_progress_bar(len(self.scanner.scanned_sites), len(self.wp_sites))
        return wp_report
示例#14
0
    def find_files(env_location, potential_files, default_content="", create=False):
        """Find existent files based on folders name and file names.

        Arguments:
        - `env_location`: list of environment variable to use as a base path. Exemple: ['HOME', 'XDG_CONFIG_HOME', 'APPDATA', 'PWD']
        - `potential_files`: list of filenames. Exemple: ['.wpwatcher/wpwatcher.conf', 'wpwatcher.conf']
        - `default_content`: Write default content if the file does not exist
        - `create`: Create the file in the first existing env_location with default content if the file does not exist
        """
        potential_paths = []
        existent_files = []
        # build potential_paths of config file
        for env_var in env_location:
            if env_var in os.environ:
                for file_path in potential_files:
                    potential_paths.append(os.path.join(os.environ[env_var], file_path))
        # If file exist, add to list
        for p in potential_paths:
            if os.path.isfile(p):
                existent_files.append(p)
        # If no file foud and create=True, init new template config
        if len(existent_files) == 0 and create:
            os.makedirs(os.path.dirname(potential_paths[0]), exist_ok=True)
            with open(potential_paths[0], "w") as config_file:
                config_file.write(default_content)
            log.info("Init new file: %s" % (p))
            existent_files.append(potential_paths[0])
        return existent_files
示例#15
0
def print_progress_bar(count, total):
    """Helper method to print progress bar.  Stolen on the web"""
    size = 0.3  #size of progress bar
    percent = int(float(count) / float(total) * 100)
    log.info("Progress - [{}{}] {}% - {} / {}".format(
        '=' * int(int(percent) * size), ' ' * int((100 - int(percent)) * size),
        percent, count, total))
示例#16
0
    def __init__(self):
        """Main program entrypoint"""

        # Parse arguments
        args = self.parse_args()
        # Init logger with CLi arguments
        init_log(args.verbose, args.quiet)
        # If template conf , print and exit
        if args.template_conf: self.template_conf()
        # Print "banner"
        log.info(
            "WPWatcher -  Automating WPscan to scan and report vulnerable Wordpress sites"
        )
        # If version, print and exit
        if args.version: self.verion()
        # Init WPWatcher obhect and dump reports
        if args.wprs != False: self.wprs(args.wprs, args.daemon)

        # Read config
        configuration = self.build_config_cli(args)

        # If daemon lopping
        if configuration['daemon']:
            # Run 4 ever
            WPWatcherDaemon(configuration)

        else:
            # Run scans and quit
            # Create main object
            wpwatcher = WPWatcher(configuration)
            exit_code, _ = wpwatcher.run_scans_and_notify()
            exit(exit_code)
示例#17
0
    def __init__(self, files=None, string=None):
        self.files = files
        # Init config parser
        self.parser = configparser.ConfigParser()
        # Load default configuration
        self.parser.read_dict({'wpwatcher': self.DEFAULT_CONFIG})

        if string:
            self.parser.read_string(string)

        if (not self.files or len(self.files) == 0) and not string:
            self.files = self.find_config_files()

        if self.files:
            read_files = self.parser.read(self.files)
            if len(read_files) < len(self.files):
                log.error(
                    "Could not read config " +
                    str(list(set(self.files) - set(read_files))) +
                    ". Make sure the file exists, the format is OK and you have correct access right."
                )
                exit(-1)

        # No config file notice
        else:
            log.info(
                "No config file loaded and could not find default config `~/.wpwatcher/wpwatcher.conf`, `~/wpwatcher.conf` or `./wpwatcher.conf`"
            )
示例#18
0
 def skip_this_site(self, wp_report, last_wp_report):
     '''Return true if the daemon mode is enabled and scan already happend in the last configured `daemon_loop_wait`'''
     if ( self.daemon and 
         datetime.strptime(wp_report['datetime'],DATE_FORMAT) - datetime.strptime(last_wp_report['datetime'],DATE_FORMAT) < self.daemon_loop_sleep):
         log.info("Daemon skipping site %s because already scanned in the last %s"%(wp_report['site'] , self.daemon_loop_sleep))
         self.scanned_sites.append(None)
         return True
     return False
示例#19
0
 def _log_db_reports_infos(self) -> None:
     if len(self.new_reports) > 0 and repr(
             self.new_reports) != "No scan report to show":
         if self.wp_reports.filepath != "null":
             log.info(
                 f"Updated reports in database: {self.wp_reports.filepath}")
         else:
             log.info("Local database disabled, no reports updated.")
示例#20
0
    def _scan_site(self, wp_site: Site,
                   wp_report: ScanReport) -> Optional[ScanReport]:
        """
        Handled WPScan scanning , parsing, errors and reporting.
        Returns filled wp_report, None if interrupted or killed.
        Can raise `RuntimeError` if any errors.
        """

        # WPScan arguments
        wpscan_arguments = (self.wpscan_args + wp_site["wpscan_args"] +
                            ["--url", wp_site["url"]])

        # Output
        log.info(f"Scanning site {wp_site['url']}")

        # Launch WPScan
        wpscan_process = self.wpscan.wpscan(*wpscan_arguments)
        wp_report["wpscan_output"] = wpscan_process.stdout

        log.debug(f"WPScan raw output:\n{wp_report['wpscan_output']}")
        log.debug("Parsing WPScan output")

        try:
            # Use wpscan_out_parse module
            try:
                parser = WPScanJsonParser(
                    json.loads(wp_report["wpscan_output"]),
                    self.false_positive_strings +
                    wp_site["false_positive_strings"] + [
                        "No WPVulnDB API Token given",
                        "No WPScan API Token given"
                    ])
            except ValueError as err:
                parser = WPScanCliParser(
                    wp_report["wpscan_output"], self.false_positive_strings +
                    wp_site["false_positive_strings"] + [
                        "No WPVulnDB API Token given",
                        "No WPScan API Token given"
                    ])
            finally:
                wp_report.load_parser(parser)

        except Exception as err:
            raise RuntimeError(
                f"Could not parse WPScan output for site {wp_site['url']}\nOutput:\n{wp_report['wpscan_output']}"
            ) from err

        # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors
        if wpscan_process.returncode in [0, 5]:
            return wp_report

        # Quick return if interrupting and/or if user cancelled scans
        if self.interrupting or wpscan_process.returncode in [2, -2, -9]:
            return None

        # Other errors codes : 127, etc, simply raise error
        err_str = f"WPScan failed with exit code {wpscan_process.returncode}. \nArguments: {safe_log_wpscan_args(wpscan_arguments)}. \nOutput: \n{remove_color(wp_report['wpscan_output'])}\nError: \n{wpscan_process.stderr}"
        raise RuntimeError(err_str)
示例#21
0
 def terminate_scan(self, wp_site, wp_report):
     # Kill process if stilla live
     for p in self.wpscan.processes:
         if (wp_site['url'] in p.args) and not p.returncode:
             log.info('Killing WPScan process %s' %
                      (safe_log_wpscan_args(p.args)))
             p.kill()
     # Discard wpscan_output from report
     if 'wpscan_output' in wp_report: del wp_report['wpscan_output']
示例#22
0
 def _update_wpscan(self) -> None:
     # Update wpscan database
     log.info("Updating WPScan")
     process = self._wpscan("--update", "--format", "json", "--no-banner")
     if process.returncode != 0:
         raise RuntimeError(
             f"Error updating WPScan.\nOutput:{process.stdout}\nError:\n{process.stderr}"
         )
     self._lazy_last_db_update = datetime.now()
示例#23
0
    def __init__(self, conf):

        # Create (lazy) wpscan link
        self.wpscan = WPScanWrapper(conf['wpscan_path'])
        # Init mail link
        self.mail = WPWatcherNotification(conf)

        # Storing the Event object to wait and cancel the waiting
        self.api_wait = threading.Event()
        # Toogle if aborting so other errors doesnt get triggerred and exit faster
        self.interrupting = False
        # List of urls scanend
        self.scanned_sites = []

        # Save required config options
        self.api_limit_wait = conf['api_limit_wait']
        self.follow_redirect = conf['follow_redirect']
        self.wpscan_output_folder = conf['wpscan_output_folder']
        self.wpscan_args = conf['wpscan_args']
        self.fail_fast = conf['fail_fast']
        self.false_positive_strings = conf['false_positive_strings']
        self.daemon = conf['daemon']
        self.daemon_loop_sleep = conf['daemon_loop_sleep']
        self.prescan_without_api_token = conf['prescan_without_api_token']

        # Scan timeout
        self.scan_timeout = conf['scan_timeout']

        # Setup prescan options
        self.prescanned_sites_warn = []
        self.api_token = None
        if self.prescan_without_api_token:
            log.info("Prescan without API token...")
            if not self.check_api_token_not_installed():
                exit(-1)
            self.api_token = self.retreive_api_token(self.wpscan_args)
            if not self.api_token:
                log.error(
                    "No --api-token in WPScan arguments, please set --api-token in config file wpscan_args values or use --wpargs [...] to allow WPWatcher to handle WPScan API token"
                )
                exit(-1)
            api_token_index = self.wpscan_args.index("--api-token") + 1
            del self.wpscan_args[api_token_index]
            del self.wpscan_args[api_token_index - 1]

        # Init wpscan output folder
        if conf['wpscan_output_folder']:
            os.makedirs(conf['wpscan_output_folder'], exist_ok=True)
            os.makedirs(os.path.join(conf['wpscan_output_folder'], 'error/'),
                        exist_ok=True)
            os.makedirs(os.path.join(conf['wpscan_output_folder'], 'alert/'),
                        exist_ok=True)
            os.makedirs(os.path.join(conf['wpscan_output_folder'], 'warning/'),
                        exist_ok=True)
            os.makedirs(os.path.join(conf['wpscan_output_folder'], 'info/'),
                        exist_ok=True)
示例#24
0
 def __init__(self, conf):
     log.info("Daemon mode selected, looping for ever...")
     # keep data in memory
     wpwatcher = WPWatcher(conf)
     while True:
         # Run scans for ever
         wpwatcher.run_scans_and_notify()
         log.info("Daemon sleeping %s and scanning again..." %
                  conf['daemon_loop_sleep'])
         time.sleep(conf['daemon_loop_sleep'].total_seconds())
示例#25
0
    def handle_wpscan_err_api_wait(self, wp_site, wp_report):
        log.info(
            "API limit has been reached after %s sites, sleeping %s and continuing the scans..."
            % (len(self.scanned_sites), API_WAIT_SLEEP))
        self.wpscan.init_check_done = False  # will re-trigger wpscan update next time wpscan() is called
        self.api_wait.wait(API_WAIT_SLEEP.total_seconds())
        if self.interrupting: return ((None, True))

        new_report = self.scan_site(wp_site)
        return ((new_report, new_report != None))
示例#26
0
 def delete_tmp_wpscan_files():
     '''Delete temp wpcan files'''
     # Try delete temp files.
     if os.path.isdir('/tmp/wpscan'):
         try:
             shutil.rmtree('/tmp/wpscan')
             log.info("Deleted temp WPScan files in /tmp/wpscan/")
         except (FileNotFoundError, OSError, Exception):
             log.info(
                 "Could not delete temp WPScan files in /tmp/wpscan/\n%s" %
                 (traceback.format_exc()))
示例#27
0
 def _delete_tmp_wpscan_files() -> None:
     """Delete temp wpcan files"""
     # Try delete temp files.
     if os.path.isdir("/tmp/wpscan"):
         try:
             shutil.rmtree("/tmp/wpscan")
             log.info("Deleted temp WPScan files in /tmp/wpscan/")
         except (FileNotFoundError, OSError, Exception):
             log.info(
                 f"Could not delete temp WPScan files in /tmp/wpscan/\n{traceback.format_exc()}"
             )
示例#28
0
 def log_report_results(self, wp_report: ScanReport) -> None:
     """Print WPScan findings"""
     for info in wp_report["infos"]:
         log.info(oneline(f"** WPScan INFO {wp_report['site']} ** {info}"))
     for fix in wp_report["fixed"]:
         log.info(oneline(f"** FIXED Issue {wp_report['site']} ** {fix}"))
     for warning in wp_report["warnings"]:
         log.warning(
             oneline(f"** WPScan WARNING {wp_report['site']} ** {warning}"))
     for alert in wp_report["alerts"]:
         log.critical(
             oneline(f"** WPScan ALERT {wp_report['site']} ** {alert}"))
示例#29
0
def main(_args: Optional[Sequence[Text]] = None) -> None:
    """Main program entrypoint"""
    # Parse arguments
    args: argparse.Namespace = get_arg_parser().parse_args(_args)

    # Init logger with CLi arguments
    _init_log(args.verbose, args.quiet)

    # If template conf , print and exit
    if args.template_conf:
        template_conf()

    # Print "banner"
    log.info(
        "WPWatcher - Automating WPscan to scan and report vulnerable Wordpress sites"
    )

    if args.version:
        # Print and exit
        version()

    if args.wprs != False:
        # Init WPWatcherDataBase object and dump reports
        wprs(filepath=args.wprs, daemon=args.daemon)

    # Read config
    configuration = Config.fromcliargs(args)

    if args.show:
        # Init WPWatcherDataBase object and dump cli formatted report
        show(
            urlpart=args.show,
            filepath=configuration["wp_reports"],
            daemon=args.daemon,
        )

    # Launch syslog test
    if args.syslog_test:
        syslog_test(configuration)

    # If daemon lopping
    if configuration["daemon"]:

        # Run 4 ever
        daemon = Daemon(configuration)
        daemon.loop()

    else:
        # Run scans and quit
        wpwatcher = WPWatcher(configuration)
        exit_code, reports = wpwatcher.run_scans()
        exit(exit_code)
示例#30
0
 def _handle_wpscan_err_api_wait(
     self, failed_process: subprocess.CompletedProcess
 ) -> subprocess.CompletedProcess:  # type: ignore [type-arg]
     """
     Sleep 24 hours and retry. 
     """
     log.info(
         f"API limit has been reached, sleeping 24h and continuing the scans..."
     )
     self._api_wait.wait(API_WAIT_SLEEP.total_seconds())
     if self._interrupting:
         return failed_process
     return self._wpscan(*failed_process.args)