Exemple #1
0
    def wpscan(self, *args):
        (exit_code, output) = (0, "")
        # WPScan arguments
        cmd = shlex.split(self.path) + list(args)
        # Log wpscan command without api token
        log.debug("Running WPScan command: %s" %
                  ' '.join(safe_log_wpscan_args(cmd)))
        # Run wpscan -------------------------------------------------------------------
        try:
            process = subprocess.Popen(cmd,
                                       stdout=subprocess.PIPE,
                                       stderr=open(os.devnull, 'w'))
            # Append process to current process list and launch
            self.processes.append(process)
            wpscan_output, _ = process.communicate()
            self.processes.remove(process)
            try:
                wpscan_output = wpscan_output.decode("utf-8")
            except UnicodeDecodeError:
                wpscan_output = wpscan_output.decode("latin1")
            # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability.
            #   See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb
            if process.returncode not in [0, 5]:
                # Handle error
                err_string = "WPScan command '%s' failed with exit code %s %s" % (
                    ' '.join(safe_log_wpscan_args(cmd)), str(
                        process.returncode), ". WPScan output: %s" %
                    wpscan_output if wpscan_output else '')
                log.error(oneline(err_string))
            else:
                # WPScan comamnd success
                log.debug("WPScan raw output:\n" + wpscan_output)
            (exit_code, output) = (process.returncode, wpscan_output)
        except (CalledProcessError) as err:
            # Handle error --------------------------------------------------
            wpscan_output = str(err.output)
            err_string = "WPScan command '%s' failed with exit code %s %s\nError:\n%s" % (
                ' '.join(safe_log_wpscan_args(cmd)), str(process.returncode),
                ". WPScan output: %s" % wpscan_output if wpscan_output else '',
                traceback.format_exc())

            log.error(oneline(err_string))
            (exit_code, output) = (err.returncode, wpscan_output)
        except FileNotFoundError as err:
            err_string = "Could not find wpscan executable. \nError:\n%s" % (
                traceback.format_exc())
            log.error(oneline(err_string))
            (exit_code, output) = (-1, "")
        return ((exit_code, output))
Exemple #2
0
    def _wpscan(self, *args):
        # WPScan arguments
        cmd = self.wpscan_executable + list(args)
        # Log wpscan command without api token
        log.debug("Running WPScan command: %s" %
                  " ".join(safe_log_wpscan_args(cmd)))
        # Run wpscan
        process = subprocess.Popen(cmd,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        # Append process to current process list and launch
        self.processes.append(process)
        wpscan_output, stderr = process.communicate()
        self.processes.remove(process)
        try:
            wpscan_output = wpscan_output.decode("utf-8")
        except UnicodeDecodeError:
            wpscan_output = wpscan_output.decode("latin1")
        """
        # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability.
        #   See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb
        if process.returncode in [0,5]:
            # WPScan comamnd success
            log.debug("WPScan raw output:\n"+wpscan_output)
        
        # Log error
        else : 
            err_string, full_err_string=self.get_full_err_string(cmd, process.returncode, wpscan_output, stderr)
            log.error(err_string)
            log.debug(full_err_string)
        """

        return (process.returncode, wpscan_output)
Exemple #3
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']
Exemple #4
0
 def get_full_err_string(cmd, returncode, wpscan_output, stderr):
     try:
         reason_short = [
             line for line in wpscan_output.splitlines()
             if 'aborted' in line.lower()
         ][0].replace('"', '').strip()
     except IndexError:
         reason_short = ""
     full = "%s %s" % (
         "\nWPScan output: %s" % wpscan_output if wpscan_output else '',
         "\nStandard error output: %s" % stderr if stderr else '')
     short = "WPScan command '%s' failed with exit code %s %s" % (' '.join(
         safe_log_wpscan_args(cmd)), str(returncode), reason_short)
     return (short, full)
Exemple #5
0
 def dump_config(self):
     bump_conf=copy.deepcopy(self.conf)
     string=''
     for k in bump_conf:
         v=bump_conf[k]
         if k == 'wpscan_args':
             v=safe_log_wpscan_args(v)
         if k == 'smtp_pass' and bump_conf[k] != "" :
             v = '***'
         if isinstance(v, (list, dict)):
             v=json.dumps(v)
         else: v=str(v)
         string+=("\n{:<25}\t=\t{}".format(k,v))
     return(string)
Exemple #6
0
 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)
Exemple #7
0
 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
Exemple #8
0
 def __repr__(self) -> str:
     """Get the config representation without passwords, ready for printing. """
     dump_conf = copy.deepcopy(self)
     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 += f"\n{k:<25}\t=\t{v}"
     return string
Exemple #9
0
    def _wpscan_site(self, wp_site, wp_report):
        # WPScan arguments
        wpscan_arguments = self.wpscan_args + wp_site['wpscan_args'] + [
            '--url', wp_site['url']
        ]
        # Output
        log.info("Scanning site %s" % wp_site['url'])
        # Launch WPScan
        (wpscan_exit_code,
         wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments)

        # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors
        if wpscan_exit_code in [0, 5]:
            # Call parse_result from parser.py
            log.debug("Parsing WPScan output")
            try:
                wp_report['infos'], wp_report['warnings'], wp_report[
                    'alerts'] = parse_results(
                        wp_report['wpscan_output'],
                        self.false_positive_strings +
                        wp_site['false_positive_strings'] +
                        ['No WPVulnDB API Token given'])
                wp_report['errors'] = []  # clear errors if any
            except Exception as err:
                err_str = "Could not parse WPScan output for site %s\n%s" % (
                    wp_site['url'], traceback.format_exc())
                log.error(err_str)
                raise RuntimeError(err_str) from err
            else:
                return wp_report

        # Handle scan errors -----

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

        # Other errors codes : -9, -2, 127, etc:
        # or wpscan_exit_code not in [1,3,4]
        # If WPScan error, add the error to the reports
        # This types if errors will be written into the Json database file exit codes 1,3,4
        err_str = "WPScan failed with exit code %s. \nWPScan arguments: %s. \nWPScan output: \n%s" % (
            (wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments),
             wp_report['wpscan_output']))
        raise RuntimeError(err_str)
Exemple #10
0
 def wpscan_site(self, wp_site, wp_report):
     '''Timeout wrapper arround `WPWatcherScanner._wpscan_site()`  
     Launch WPScan.  
     Returns filled wp_report or None
     '''
     try:
         wp_report_new= timeout(self.scan_timeout.total_seconds(), self._wpscan_site, args=(wp_site, wp_report) )
         if wp_report_new: wp_report.update(wp_report_new)
         else : return None
     except TimeoutError:
         wp_report['error']+="Timeout scanning site after %s seconds.\nSetup scan_timeout in config file to allow more time"%self.scan_timeout.total_seconds()
         log.error("Timeout scanning site %s after %s seconds. Setup scan_timeout in config file to allow more time"%(wp_site['url'], self.scan_timeout.total_seconds()))
         # Kill process
         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()
         self.check_fail_fast()
     return wp_report
Exemple #11
0
    def _wpscan_site(self, wp_site, wp_report):
        '''Handled WPScan scanning , parsing, errors and reporting.  
        Returns filled wp_report, None if interrupted or killed.  
        Can raise RuntimeError if WPScan failed'''
        # WPScan arguments
        wpscan_arguments=self.wpscan_args+wp_site['wpscan_args']+['--url', wp_site['url']]
        # Output
        log.info("Scanning site %s"%wp_site['url'] )
        # Launch WPScan 
        (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments)

        log.debug("Parsing WPScan output")
        try:
            # Call parse_results_from_string from wpscan_out_parse module 
            results = parse_results_from_string(wp_report['wpscan_output'] ,
                self.false_positive_strings + wp_site['false_positive_strings'] + ['No WPVulnDB API Token given'] )

            wp_report['infos'], wp_report['warnings'] , wp_report['alerts'], wp_report['summary'] = results['infos'], results['warnings'], results['alerts'], results['summary']
            
            if results['error']:
                wp_report['error']+=results['error']

        except Exception as err:
            err_str="Could not parse WPScan output for site %s\n%s"%(wp_site['url'],traceback.format_exc())
            log.error(err_str)
            raise RuntimeError(err_str) from err

        # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors
        if wpscan_exit_code in [0,5]:
            return wp_report
        
        # Quick return if interrupting and/or if user cacelled scans
        if self.interrupting or wpscan_exit_code in [2, -2, -9]:
            return None

        # Other errors codes : 127, etc, simply raise error
        err_str="WPScan failed with exit code %s. \nArguments: %s. \nOutput: \n%s"%((wpscan_exit_code, 
            safe_log_wpscan_args(wpscan_arguments), 
            re.sub(r'(\x1b|\[[0-9][0-9]?m)','', wp_report['wpscan_output']) ))
        raise RuntimeError(err_str)
Exemple #12
0
    def _wpscan(self, *args):
        (exit_code, output) = (0, "")
        # WPScan arguments
        cmd = self.wpscan_executable + list(args)
        # Log wpscan command without api token
        log.debug("Running WPScan command: %s" %
                  ' '.join(safe_log_wpscan_args(cmd)))
        # Run wpscan -------------------------------------------------------------------
        try:
            process = subprocess.Popen(cmd,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
            # Append process to current process list and launch
            self.processes.append(process)
            wpscan_output, stderr = process.communicate()
            self.processes.remove(process)
            try:
                wpscan_output = wpscan_output.decode("utf-8")
            except UnicodeDecodeError:
                wpscan_output = wpscan_output.decode("latin1")
            # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability.
            #   See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb
            if process.returncode in [0, 5]:
                # WPScan comamnd success
                log.debug("WPScan raw output:\n" + wpscan_output)

            # Log error ----
            else:
                err_string, full_err_string = self.get_full_err_string(
                    cmd, process.returncode, wpscan_output, stderr)
                log.error(err_string)
                log.debug(full_err_string)

            return ((process.returncode, wpscan_output))

        except FileNotFoundError as err:
            err_string = "Could not find wpscan executable.\n%s" % (
                traceback.format_exc())
            log.error(oneline(err_string))
            raise RuntimeError(err_string) from err
Exemple #13
0
    def scan_site(self,
                  wp_site: Site,
                  last_wp_report: Optional[ScanReport] = None
                  ) -> Optional[ScanReport]:
        """
        Orchestrate the scanning of a site.

        :Return: The scan report or `None` if something happened.
        """

        # Init report variables
        wp_report: ScanReport = ScanReport({
            "site":
            wp_site["url"],
            "datetime":
            datetime.now().strftime(DATE_FORMAT)
        })

        # Launch WPScan
        try:
            # If report is None, return None right away
            if not self._scan_site(wp_site, wp_report):
                return None

        except RuntimeError:

            self._fail_scan(
                wp_report,
                f"Could not scan site {wp_site['url']} \n{traceback.format_exc()}",
            )

        # Updating report entry with data from last scan
        wp_report.update_report(last_wp_report)

        self.log_report_results(wp_report)

        wpscan_command = " ".join(
            safe_log_wpscan_args(["wpscan"] + self.wpscan_args +
                                 wp_site["wpscan_args"] +
                                 ["--url", wp_site["url"]]))

        try:

            # Notify recepients if match triggers
            if self.mail.notify(wp_site,
                                wp_report,
                                last_wp_report,
                                wpscan_command=wpscan_command):
                # Store report time
                wp_report["last_email"] = wp_report["datetime"]
                # Discard fixed items because infos have been sent
                wp_report["fixed"] = []

        # Handle sendmail errors
        except (SMTPException, ConnectionRefusedError, TimeoutError):
            self._fail_scan(
                wp_report,
                f"Could not send mail report for site {wp_site['url']}\n{traceback.format_exc()}",
            )

        # Send syslog if self.syslog is not None
        if self.syslog:
            try:
                self.syslog.emit_messages(wp_report)
            except Exception:
                self._fail_scan(
                    wp_report,
                    f"Could not send syslog messages for site {wp_site['url']}\n{traceback.format_exc()}",
                )

        # Save scanned site
        self.scanned_sites.append(wp_site["url"])

        # Discard wpscan_output from report
        if "wpscan_output" in wp_report:
            del wp_report["wpscan_output"]

        # Discard wpscan_parser from report
        if "wpscan_parser" in wp_report:
            del wp_report["wpscan_parser"]

        return wp_report
Exemple #14
0
    def scan_site(self, wp_site):
        wp_site=self.format_site(wp_site)
        # Init report variables
        wp_report={
            "site":wp_site['url'],
            "status":None,
            "datetime": datetime.now().strftime('%Y-%m-%dT%H-%M-%S'),
            "last_email":None,
            "errors":[],
            "infos":[],
            "warnings":[],
            "alerts":[],
            "fixed":[],
            "wpscan_output":None # will be deleted
        }

        # Find last site result if any
        last_wp_report=[r for r in self.wp_reports if r['site']==wp_site['url']]
        if len(last_wp_report)>0: 
            last_wp_report=last_wp_report[0]
            # Skip if the daemon mode is enabled and scan already happend in the last configured `daemon_loop_wait`
            if ( self.conf['daemon'] and 
                datetime.strptime(wp_report['datetime'],'%Y-%m-%dT%H-%M-%S') - datetime.strptime(last_wp_report['datetime'],'%Y-%m-%dT%H-%M-%S') < self.conf['daemon_loop_sleep']):
                log.info("Daemon skipping site %s because already scanned in the last %s"%(wp_site['url'] , self.conf['daemon_loop_sleep']))
                self.scanned_sites.append(None)
                return None
        else: last_wp_report=None
        
        # WPScan arguments
        wpscan_arguments=self.conf['wpscan_args']+wp_site['wpscan_args']+['--url', wp_site['url']]

        # Output
        log.info("Scanning site %s"%wp_site['url'] )
        # Launch WPScan -------------------------------------------------------
        (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments)
        
        # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors
        # Handle scan errors
        if wpscan_exit_code not in [0,5]:
            # Quick return if interrupting
            if self.interrupting: return None
            
            # Quick return if user cacelled scans
            if wpscan_exit_code in [2]: return None

            # Fail fast
            if self.conf['fail_fast']:
                if not self.interrupting: 
                    log.error("Failure")
                    self.interrupt()
                else: return None # Interrupt will generate other errors

            # If WPScan error, add the error to the reports
            # This types if errors will be written into the Json database file
            if wpscan_exit_code in [1,3,4]:

                err_str="WPScan failed with exit code %s. \nWPScan arguments: %s. \nWPScan output: \n%s"%((wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), wp_report['wpscan_output']))
                wp_report['errors'].append(err_str)
                log.error("Could not scan site %s"%wp_site['url'])

                # Try to handle error and return
                wp_report, handled = self.handle_wpscan_err(wp_site, wp_report)
                if handled: return wp_report

            # Other errors codes : -9, -2, 127, etc: Just return None right away
            else: return None 
            
        # No errors with wpscan -----------------------------
        else:
            # Write wpscan output 
            wpscan_results_file=None
            if self.conf['wpscan_output_folder'] :
                wpscan_results_file=os.path.join(self.conf['wpscan_output_folder'],
                    get_valid_filename('WPScan_results_%s_%s.txt' % (wp_site['url'], wp_report['datetime'])))
                with open(wpscan_results_file, 'w') as wpout:
                    wpout.write(re.sub(r'(\x1b|\[[0-9][0-9]?m)','', str(wp_report['wpscan_output'])))
            
            log.debug("Parsing WPScan output")
            # Call parse_result from parser.py ------------------------
            wp_report['infos'], wp_report['warnings'] , wp_report['alerts']  = parse_results(wp_report['wpscan_output'] , 
                self.conf['false_positive_strings']+wp_site['false_positive_strings'] )
            
            # Updating report entry with data from last scan if any
            if last_wp_report:
                self.update_report(wp_report, last_wp_report)
            
            # Print WPScan findings ------------------------------------------------------
            for info in wp_report['infos']:
                log.info(oneline("** WPScan INFO %s ** %s" % (wp_site['url'], info )))
            for fix in wp_report['fixed']:
                log.info(oneline("** FIXED %s ** %s" % (wp_site['url'], fix )))
            for warning in wp_report['warnings']:
                log.warning(oneline("** WPScan WARNING %s ** %s" % (wp_site['url'], warning )))
            for alert in wp_report['alerts']:
                log.critical(oneline("** WPScan ALERT %s ** %s" % (wp_site['url'], alert )))

            if wpscan_results_file: log.info("WPScan results saved to file %s"%wpscan_results_file)
        
        # Report status ------------------------------------------------
        if len(wp_report['errors'])>0:wp_report['status']="ERROR"
        elif len(wp_report['warnings'])>0 and len(wp_report['alerts']) == 0: wp_report['status']='WARNING'
        elif len(wp_report['alerts'])>0: wp_report['status']='ALERT'
        elif len(wp_report['fixed'])>0: wp_report['status']='FIXED'
        else: wp_report['status']='INFO'

        # Will print parsed readable Alerts, Warnings, etc as they will appear in email reports
        log.debug("\n%s\n"%(build_message(wp_report, 
                warnings=self.conf['send_warnings'] or self.conf['send_infos'], # switches to include or not warnings and infos
                infos=self.conf['send_infos'])))

        # Notify recepients if match triggers and no errors
        self.notify(wp_site, wp_report, last_wp_report)
        # Save scanned site
        self.scanned_sites.append(wp_site['url'])
        # Discard wpscan_output from report
        del wp_report['wpscan_output']
        # Save report in global instance database and to file when a site has been scanned
        self.update_and_write_wp_reports([wp_report])
        # Print progress
        print_progress_bar(len(self.scanned_sites), len(self.conf['wp_sites'])) 
        return(wp_report)