예제 #1
0
 def test_wpscan_output_folder(self):
     RESULTS_FOLDER="./results/"
     WPSCAN_OUTPUT_CONFIG = DEFAULT_CONFIG+"\nwpscan_output_folder=%s"%RESULTS_FOLDER
     scanner=WPWatcherScanner(WPWatcherConfig(string=WPSCAN_OUTPUT_CONFIG).build_config()[0])
     self.assertTrue(os.path.isdir(RESULTS_FOLDER),"WPscan results folder doesn't seem to have been init")
     for s in WP_SITES:
         report={
             "site": s['url'],
             "status": "WARNING",
             "datetime": "2020-04-08T16-05-16",
             "last_email": None,
             "error": '',
             "infos": [
                 "[+]","blablabla"],
             "warnings": [
                 "[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).\n| Found By: Emoji Settings (Passive Detection)\n",
                 "[!] No WPVulnDB API Token given, as a result vulnerability data has not been output.\n[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"
             ],
             "alerts": [],
             "fixed": [],
             "summary":None,
             "wpscan_output":"This is real%s"%(s)
         }
         f=scanner.write_wpscan_output(report)
         f1=os.path.join(RESULTS_FOLDER, 'warning/', get_valid_filename('WPScan_output_%s_%s.txt' % (s['url'], "2020-04-08T16-05-16")))
         self.assertEqual(f, f1, "Inconsistent WPScan output filenames")
         self.assertTrue(os.path.isfile(f1),"WPscan output file doesn't exist")
         with open(f1, 'r') as out:
             self.assertEqual(out.read(), "This is real%s"%(s))
     shutil.rmtree(RESULTS_FOLDER)
예제 #2
0
    def __init__(self, conf):
        # (Re)init logger with config
        init_log(verbose=conf['verbose'],
                 quiet=conf['quiet'],
                 logfile=conf['log_file'])

        self.delete_tmp_wpscan_files()

        # Init DB interface
        self.wp_reports = WPWatcherDataBase(conf['wp_reports'])

        # Dump config
        conf.update({'wp_reports': self.wp_reports.filepath})
        log.debug("WPWatcher configuration:{}".format(self.dump_config(conf)))

        # Init scanner
        self.scanner = WPWatcherScanner(conf)

        # Save sites
        self.wp_sites = conf['wp_sites']

        # Asynchronous executor
        self.executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=conf['asynch_workers'])
        # List of conccurent futures
        self.futures = []

        # Register the signals to be caught ^C , SIGTERM (kill) , service restart , will trigger interrupt()
        signal.signal(signal.SIGINT, self.interrupt)
        signal.signal(signal.SIGTERM, self.interrupt)

        #new reports
        self.new_reports = []
예제 #3
0
 def test_scan_localhost_error_not_wordpress(self):
     # test info, warnings and alerts
     scanner = WPWatcherScanner(
         WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0])
     report = scanner.scan_site(
         WPWatcher.format_site({'url': 'http://localhost:8080'}))
     self.assertEqual(report['status'], 'ERROR')
     self.assertRegex(report['error'],
                      'does not seem to be running WordPress')
예제 #4
0
    def test_update_report(self):
        # Init Scanner
        scanner = WPWatcherScanner(WPWatcherConfig(string=DEFAULT_CONFIG).build_config()[0])
        for s in WP_SITES:
            old={
                    "site": s['url'],
                    "status": "WARNING",
                    "datetime": "2020-04-08T16-05-16",
                    "last_email": "2020-04-08T16-05-16",
                    "error": '',
                    "infos": [
                        "[+]","blablabla"],
                    "warnings": [
                        "[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).\nblablabla\n",
                        "[!] No WPVulnDB API Token given, as a result vulnerability data has not been output.\n[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"
                    ],
                    "alerts": [],
                    "fixed": ["This issue was fixed"],
                    "summary":None,
                    "wpscan_output":""
                }

            new={
                    "site": s['url'],
                    "status": "",
                    "datetime": "2020-04-10T16-00-00",
                    "last_email": None,
                    "error": '',
                    "infos": [
                        "[+]","blablabla"],
                    "warnings": [
                        "[!] No WPVulnDB API Token given, as a result vulnerability data has not been output.\n[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"
                    ],
                    "alerts": [],
                    "fixed": [],
                    "summary":None,
                    "wpscan_output":""
                }

            expected={
                    "site": s['url'],
                    "status": "",
                    "datetime": "2020-04-10T16-00-00",
                    "last_email": "2020-04-08T16-05-16",
                    "error": '',
                    "infos": [
                        "[+]","blablabla"],
                    "warnings": [
                        "[!] No WPVulnDB API Token given, as a result vulnerability data has not been output.\n[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"
                    ],
                    "alerts": [],
                    "fixed": [
                        "This issue was fixed",
                        'Issue regarding component "%s" has been fixed since last report.\nLast report datetime is: %s'%("[+] WordPress version 5.2.2 identified (Insecure, released on 2019-06-18).",old['last_email'])    
                    ],
                    "summary":None,
                    "wpscan_output":""
                }
            
            scanner.update_report(new,old,s)
            print(new)
            print(expected)
            self.assertEqual(new, expected, "There is an issue with fixed issues feature: the expected report do not match the report returned by update_report()")
예제 #5
0
class WPWatcher():
    '''WPWacther object  

    Arguments:
    - `conf`: the configuration dict. Required

    Usage exemple:

        from wpwatcher.config import WPWatcherConfig
        from wpwatcher.core import WPWatcher
        config, files = WPWatcherConfig().build_config()
        config.update({ 'send_infos':   True,
                        'wp_sites':     [   {'url':'exemple1.com'},
                                            {'url':'exemple2.com'}  ],
                        'wpscan_args': ['--format', 'json', '--stealthy']
                    })
        w=WPWatcher(config)
        exit_code, reports = w.run_scans_and_notify()
        for r in reports:
            print("%s\t\t%s"%( r['site'], r['status'] ))
    '''

    # WPWatcher must use a configuration dict
    def __init__(self, conf):
        # (Re)init logger with config
        init_log(verbose=conf['verbose'],
                 quiet=conf['quiet'],
                 logfile=conf['log_file'])

        self.delete_tmp_wpscan_files()

        # Init DB interface
        self.wp_reports = WPWatcherDataBase(conf['wp_reports'])

        # Update config before passing it to WPWatcherScanner
        conf.update({'wp_reports': self.wp_reports.filepath})

        # Init scanner
        self.scanner = WPWatcherScanner(conf)

        # Dump config
        log.debug("WPWatcher configuration:{}".format(self.dump_config(conf)))

        # Save sites
        self.wp_sites = conf['wp_sites']

        # Asynchronous executor
        self.executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=conf['asynch_workers'])

        # List of conccurent futures
        self.futures = []

        # Register the signals to be caught ^C , SIGTERM (kill) , service restart , will trigger interrupt()
        signal.signal(signal.SIGINT, self.interrupt)
        signal.signal(signal.SIGTERM, self.interrupt)

        #new reports
        self.new_reports = []

    @staticmethod
    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()))

    @staticmethod
    def dump_config(conf):
        '''Print the config without passwords'''
        dump_conf = copy.deepcopy(conf)
        string = ''
        for k in dump_conf:
            v = dump_conf[k]
            if k == 'wpscan_args':
                v = safe_log_wpscan_args(v)
            if k == 'smtp_pass' and v != "":
                v = '***'
            if isinstance(v, (list, dict)):
                v = json.dumps(v)
            else:
                v = str(v)
            string += ("\n{:<25}\t=\t{}".format(k, v))
        return (string)

    def cancel_pending_futures(self):
        '''Cancel all asynchronous jobs'''
        for f in self.futures:
            if not f.done():
                f.cancel()

    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)

    def print_scanned_sites_results(self, new_reports):
        '''Print the result summary for the scanned sites'''
        new_reports = [n for n in new_reports if n]
        if len(new_reports) > 0:
            log.info(self.results_summary(new_reports))
            if self.wp_reports.filepath != "null":
                log.info("Updated %s reports in database: %s" %
                         (len(new_reports), self.wp_reports.filepath))
            else:
                log.info("No reports updated in local database")

    @staticmethod
    def results_summary(results):
        '''Print the summary table of all sites.  
        Columns : "Site", "Status", "Last scan", "Last email", "Issues", "Problematic component(s)"
        '''
        string = 'Results summary\n'
        header = ("Site", "Status", "Last scan", "Last email", "Issues",
                  "Problematic component(s)")
        sites_w = 20
        # Determine the longest width for site column
        for r in results:
            sites_w = len(
                r['site']) + 4 if r and len(r['site']) > sites_w else sites_w
        frow = "{:<%d} {:<8} {:<20} {:<20} {:<8} {}" % sites_w
        string += frow.format(*header)
        for row in results:
            pb_components = []
            for m in row['alerts'] + row['warnings']:
                pb_components.append(m.splitlines()[0])
            if row['error']:
                pb_components.append("Failure")
            string += '\n'
            string += frow.format(str(row['site']), str(row['status']),
                                  str(row['datetime']), str(row['last_email']),
                                  len(row['alerts'] + row['warnings']),
                                  ', '.join(pb_components))
        return string

    @staticmethod
    def format_site(wp_site):
        '''Make sure the site structure is correct, parse 'url', init optionals 'email_to','false_positive_strings','wpscan_args' to empty list if not present.
        Raise ValueError if url key is not present'''
        if 'url' not in wp_site:
            raise ValueError("Invalid site %s\nMust contain 'url' key" %
                             wp_site)
        else:
            # Strip URL string
            wp_site['url'] = wp_site['url'].strip()
            # Format sites with scheme indication
            p_url = list(urlparse(wp_site['url']))
            if p_url[0] == "":
                wp_site['url'] = 'http://' + wp_site['url']
        # Read the wp_site dict and assing default values if needed
        optionals = ['email_to', 'false_positive_strings', 'wpscan_args']
        for op in optionals:
            if op not in wp_site or wp_site[op] is None: wp_site[op] = []

        return wp_site

    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)

    def run_scans_wrapper(self, wp_sites):
        """Helper method to deal with : 
        - executor, concurent futures
        - Trigger self.interrupt() on InterruptedError (raised if fail fast enabled)
        
        Pass `kwargs` arguments to scan_site_wrapper() """

        log.info("Starting scans on %s configured sites" % (len(wp_sites)))
        for wp_site in wp_sites:
            self.futures.append(
                self.executor.submit(self.scan_site_wrapper, wp_site))
        for f in self.futures:
            try:
                self.new_reports.append(f.result())
            # Handle interruption from inside threads when using --ff
            except InterruptedError:
                self.interrupt()
            except concurrent.futures.CancelledError:
                pass
        # Ensure everything is down
        self.cancel_pending_futures()
        return self.new_reports

    def run_scans_and_notify(self):
        """
        Run WPScan on defined websites and send notifications.  
        Returns a `tuple (exit code, reports)`"""

        # 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)

        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))
예제 #6
0
class WPWatcher():

    # WPWatcher must use a configuration dict
    def __init__(self, conf):
        # (Re)init logger with config
        init_log(verbose=conf['verbose'],
                 quiet=conf['quiet'],
                 logfile=conf['log_file'])

        self.delete_tmp_wpscan_files()

        # Init DB interface
        self.wp_reports = WPWatcherDataBase(conf['wp_reports'])

        # Dump config
        conf.update({'wp_reports': self.wp_reports.filepath})
        log.debug("WPWatcher configuration:{}".format(self.dump_config(conf)))

        # Init scanner
        self.scanner = WPWatcherScanner(conf)

        # Save sites
        self.wp_sites = conf['wp_sites']

        # Asynchronous executor
        self.executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=conf['asynch_workers'])
        # List of conccurent futures
        self.futures = []

        # Register the signals to be caught ^C , SIGTERM (kill) , service restart , will trigger interrupt()
        signal.signal(signal.SIGINT, self.interrupt)
        signal.signal(signal.SIGTERM, self.interrupt)

        #new reports
        self.new_reports = []

    @staticmethod
    def delete_tmp_wpscan_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()))

    @staticmethod
    def dump_config(conf):

        dump_conf = copy.deepcopy(conf)
        string = ''
        for k in dump_conf:
            v = dump_conf[k]
            if k == 'wpscan_args':
                v = safe_log_wpscan_args(v)
            if k == 'smtp_pass' and v != "":
                v = '***'
            if isinstance(v, (list, dict)):
                v = json.dumps(v)
            else:
                v = str(v)
            string += ("\n{:<25}\t=\t{}".format(k, v))
        return (string)

    # def wait_all_wpscan_process(self):

    #     while len(self.scanner.wpscan.processes)>0:
    #         time.sleep(0.05)

    def tear_down_jobs(self):
        # Cancel all jobs
        for f in self.futures:
            if not f.done(): f.cancel()

    def interrupt(self, sig=None, frame=None):
        # Lock for interrupting
        log.error("Interrupting...")
        # If called inside ThreadPoolExecutor, raise Exeception
        if not isinstance(threading.current_thread(), threading._MainThread):
            raise InterruptedError()
        # Cancel all scans
        self.scanner.cancel_scans()
        # Wait all scans finished, print results and quit
        self.tear_down_jobs()
        # 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
        new_reports = []
        for f in self.futures:
            if f.done():
                try:
                    new_reports.append(f.result())
                except Exception:
                    pass
        self.print_scanned_sites_results(new_reports)
        log.info("Scans interrupted.")
        exit(-1)

    def print_scanned_sites_results(self, new_reports):
        new_reports = [n for n in new_reports if n]
        if len(new_reports) > 0:
            log.info(results_summary(new_reports))
            if self.wp_reports.filepath != "null":
                log.info("Updated %s reports in database: %s" %
                         (len(new_reports), self.wp_reports.filepath))
            else:
                log.info("No reports updated in local database")

    @staticmethod
    def format_site(wp_site):

        if 'url' not in wp_site:
            log.error("Invalid site %s" % wp_site)
            wp_site = {'url': ''}
        else:

            #Strip URL string
            wp_site['url'] = wp_site['url'].strip()

            # Format sites with scheme indication
            p_url = list(urlparse(wp_site['url']))
            if p_url[0] == "":
                wp_site['url'] = 'http://' + wp_site['url']
        # Read the wp_site dict and assing default values if needed
        optionals = ['email_to', 'false_positive_strings', 'wpscan_args']
        for op in optionals:
            if op not in wp_site or wp_site[op] is None: wp_site[op] = []

        return wp_site

    # Orchestrate the scanning of a site
    def scan_site_wrapper(self, wp_site, with_api_token=False):

        wp_site = self.format_site(wp_site)
        last_wp_report = self.wp_reports.find_last_wp_report(
            {'site': wp_site['url']})
        if with_api_token:
            wp_site['wpscan_args'].extend(
                ["--api-token", self.scanner.api_token])

        # 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)

    def run_scans_wrapper(self, wp_sites, **kwargs):
        log.info("Starting scans on %s configured sites" % (len(wp_sites)))
        for wp_site in wp_sites:
            self.futures.append(
                self.executor.submit(self.scan_site_wrapper, wp_site,
                                     **kwargs))
        for f in self.futures:
            try:
                self.new_reports.append(f.result())
                # Handle interruption from inside threads when using --ff
            except InterruptedError:
                self.interrupt()
            except concurrent.futures.CancelledError:
                pass
        # Ensure everything is down
        self.tear_down_jobs()
        return self.new_reports

    # Run WPScan on defined websites
    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))

    def re_run_scans(self, wp_sites):
        self.scanner.scanned_sites = []
        self.futures = []
        self.wp_sites = wp_sites
        return self.run_scans_wrapper(wp_sites, with_api_token=True)