-
-
Save faffyman/3f2b8726e9c52c053a09abbebe6d4676 to your computer and use it in GitHub Desktop.
Wordpress Watcher - WPScan Vulnerabilty Scan on Wordpress Sites and Reporting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: iso-8859-1 -*- | |
# -*- coding: utf-8 -*- | |
# | |
# Wordpress Watcher | |
# Automating WPscan to scan and report vulnerable Wordpress sites | |
# Forked from https://gist.github.com/Neo23x0/2a97509f50c57a2e737e | |
# v0.1.1 | |
# March 2017 | |
# | |
# DISCLAIMER - USE AT YOUR OWN RISK. | |
import os | |
import re | |
import smtplib | |
import traceback | |
from subprocess import check_output, CalledProcessError, STDOUT | |
from email.mime.text import MIMEText | |
from subprocess import Popen, PIPE | |
from datetime import datetime | |
# General Options | |
wpscan_dir = r'/usr/local/bin/' | |
# list of websites to be scanned, with optional commands, e.g. the wordpress content dir, or enumerate users etc | |
wp_sites = [ | |
'websiteone.com', | |
['website-two.com', ['--option_one', 'option-two']], | |
'website-three.com', | |
['website-four.com', ['--wp-content-dir', 'app']], | |
'website-five.com', | |
] | |
false_positive_strings = [ 'XML-RPC', 'GHOST vulnerability' ] | |
# Log file | |
log_file = r'./wpwatcher.log' | |
# Email Report | |
send_email_report = True | |
smtp_server = r'smtp.server.net:587' # 'server:port' | |
smtp_auth = True | |
smtp_user = 'username' | |
smtp_pass = 'password' | |
smtp_ssl = True | |
to_email = r'[email protected]' | |
from_email = r'[email protected]' | |
# Check if WPScan is installed | |
def is_wpscan_installed(): | |
if not os.path.exists(wpscan_dir): | |
result = check_output(r'mkdir -p %s' % wpscan_dir, shell=True) | |
if result: | |
print "[ERROR] %s" % result.replace("\n", " ") | |
sys.exit(1) | |
return 0 | |
else: | |
return 1 | |
# Install WPScan from github | |
# This function is likely to fail as it strongly depends ton the OS platform | |
# and other settings to succeed | |
# I strongly recommend installing WPScan yourself and set the directory in the | |
# options above | |
def install_wpscan(): | |
print "[INFO] Trying to install WPScan" | |
os.chdir(wpscan_dir) | |
try: | |
result = check_output(r'GIT_CURLOPT_SSLVERSION=3 && git clone https://github.com/wpscanteam/wpscan.git && gem install bundler && bundle install --without test', stderr=STDOUT, shell=True, universal_newlines=True) | |
except CalledProcessError as exc: | |
print "[ERROR]", exc.returncode, exc.output | |
print "If errors occur - try to install wpscan and all dependancies manually" | |
print "[INFO] %s" % result | |
# Update WPScan from github | |
def update_wpscan(): | |
print "[INFO] Updating WPScan" | |
os.chdir(wpscan_dir) | |
try: | |
result = check_output(r'./wpscan.rb --batch --update', stderr=STDOUT, shell=True, universal_newlines=True) | |
except CalledProcessError as exc: | |
print "[ERROR]", exc.returncode, exc.output | |
print result | |
# Run WPScan on defined domains | |
def run_scan(): | |
print "[INFO] Starting scans on configured sites" | |
os.chdir(wpscan_dir) | |
for wp_site in wp_sites: | |
result = '' | |
# Scan ---------------------------------------------------------------- | |
try: | |
print "[INFO] Scanning '%s'" % wp_site | |
scan_options = '' # initalise scan_options as empty string | |
if isinstance(wp_site, list): | |
(wp_site, scan_options) = wp_site | |
# set out command and some default scan options | |
cmd = ['./wpscan.rb', '--batch','--url', wp_site, '--random-agent','--follow-redirection'] | |
# if scan_options was passed as array, merge the two arrays | |
if isinstance(scan_options, list): | |
cmd = cmd + scan_options | |
# if scan_options is a string i.e. just a single option, just append it to the cmd array | |
if isinstance(scan_options, str): | |
cmd.append(scan_options) | |
# send the command and grab the output | |
p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE ) | |
result, err = p.communicate() | |
except CalledProcessError as exc: | |
print "[ERROR]", exc.returncode, exc.output | |
# Parse the results --------------------------------------------------- | |
(warnings, alerts) = parse_results(result) | |
# Report Options ------------------------------------------------------ | |
if send_email_report: | |
send_report(wp_site, warnings, alerts, result) | |
# Logfile | |
try: | |
with open(log_file, 'a') as log: | |
for warning in warnings: | |
log.write("%s %s WARNING: %s\n" % (get_timestamp(), wp_site, warning)) | |
for alert in alerts: | |
log.write("%s %s ALERT: %s\n" % (get_timestamp(), wp_site, alert)) | |
except Exception, e: | |
traceback.print_exc() | |
print "[ERROR] Cannot write to log file" | |
# Parsing the results | |
def parse_results(results): | |
print "[INFO] parse results \n" | |
warnings = [] | |
alerts = [] | |
warning_on = False | |
alert_on = False | |
last_message = "" | |
# Parse the lines | |
for line in results.splitlines(): | |
# Remove colorization | |
line = re.sub(r'(\x1b|\[[0-9][0-9]?m)','',line) | |
# Empty line = end of message | |
if line == "" or line.startswith("[+]"): | |
if warning_on: | |
if not is_false_positive(warning): | |
warnings.append(warning) | |
warning_on = False | |
if alert_on: | |
if not is_false_positive(alert): | |
alerts.append(alert) | |
alert_on = False | |
# Add to warning/alert | |
if warning_on: | |
warning += " / %s \n" % line.lstrip(" ") | |
if alert_on: | |
alert += " / %s \n" % line.lstrip(" ") | |
# Start Warning/Alert | |
if line.startswith("[i]"): | |
# Warning message | |
# warning = "%s / %s \n" % ( last_message, line ) | |
warning = line + " \n" | |
warning_on = True | |
if line.startswith("[!]"): | |
# Warning message | |
alert = line + " \n" | |
alert_on = True | |
# Store lase message | |
last_message = line | |
return ( warnings, alerts ) | |
# Send email report | |
def send_report(wp_site, warnings, alerts, raw): | |
print "[INFO] Sending email report stating items found on %s to %s" % (wp_site, to_email) | |
# Remove colorization in raw output | |
raw = re.sub(r'(\x1b|\[[0-9][0-9]?m)','',raw) | |
try: | |
message = "Issues have been detected by WPScan on one of your sites\n" | |
message += "%s \n" % wp_site | |
if warnings: | |
message += "\nWarnings\n" | |
message += "\n".join(warnings) | |
if alerts: | |
message += "\nAlerts\n" | |
message += "\n".join(alerts) | |
# ad d teh entire output of the scan, because sometimes it's easier than the summary | |
message += "\n\nRAW SCAN OUTPUT\n\n%s \n" % raw | |
mime_msg = MIMEText(message) | |
mime_msg['Subject'] = 'WPWatcher report on %s - %s' % (wp_site, get_timestamp()) | |
mime_msg['From'] = from_email | |
mime_msg['To'] = to_email | |
# Uncomment this block to use an SMTP connection | |
# SMTP Connection | |
# s = smtplib.SMTP(smtp_server) | |
# s.ehlo() | |
# SSL | |
# if smtp_ssl: | |
# s.starttls() | |
# SMTP Auth | |
# if smtp_auth: | |
# s.login(smtp_user, smtp_pass) | |
# Send Email | |
# s.sendmail(from_email, to_email, mime_msg.as_string()) | |
# s.quit() | |
# simple sendmail - SMTP preferred | |
p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE) | |
p.communicate(mime_msg.as_string()) | |
except Exception, e: | |
traceback.print_exc() | |
# Is the line defined as false positive | |
def is_false_positive(string): | |
# False Positive Detection | |
for fp_string in false_positive_strings: | |
if fp_string in string: | |
# print fp_string, string | |
return 1 | |
return 0 | |
def get_timestamp(): | |
return datetime.now().strftime('%Y-%m-%d %H:%M:%S') | |
if __name__ == '__main__': | |
# Check if WP-Scan exists | |
if not is_wpscan_installed(): | |
install_wpscan() | |
else: | |
update_wpscan() | |
# Run Scan | |
run_scan() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment