Skip to content

Commit

Permalink
Custom ports support, better verification of input, support for distr…
Browse files Browse the repository at this point in the history
…ibuted NERVE, readme
  • Loading branch information
dolevf committed Oct 4, 2020
1 parent 6251036 commit 8772d68
Show file tree
Hide file tree
Showing 24 changed files with 202 additions and 65 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [Deployment Recommendations](#Deployment-Recommendation)
* [Installation - Docker](#docker)
* [Installation - Bare Metal](#server)
* [Multi-node installation](#Multi-Node-Installation)
* [Security](#security)
* [Usage](#usage)
* [License](#license)
Expand Down Expand Up @@ -127,6 +128,15 @@ In your browser, navigate to http://ip.add.re.ss:80 and login with the credentia

In your browser, navigate to http://ip.add.re.ss:8080 and use the credentials printed in your terminal.


# Multi Node Installation
If you want to install NERVE in a multi-node deployment, you can follow the normal bare metal installation process, afterwards:
1. Modify the config.py file on each node
2. Change the server address of Redis `RDS_HOST` to point to a central Redis server that all NERVE instances will report to.
3. Run `service nerve restart` or `systemctl restart nerve` to reload the configuration
4. Run `apt-get remove redis` / `yum remove redis` (Depending on the Linux Distribution) since you will no longer need each instance to report to itself.
Don't forget to allow port 3769 inbound on the Redis instance, so that the NERVE instances can communicate with it.

# Security
There are a few security mechanisms implemented into NERVE you need to be aware of.

Expand Down
3 changes: 2 additions & 1 deletion bin/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def scanner():
if hosts:
conf = rds.get_scan_config()
scan_data = scanner.scan(hosts,
ports = conf['config']['scan_opts']['max_ports'],
max_ports = conf['config']['scan_opts']['max_ports'],
custom_ports = conf['config']['scan_opts']['custom_ports'],
interface = conf['config']['scan_opts']['interface'])

if scan_data:
Expand Down
6 changes: 4 additions & 2 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
MAX_LOGIN_ATTEMPTS = 5

# Redis Configuration
# This should not be set to anything else except localhost
# This should not be set to anything else except localhost unless you want to do a multi-node deployment.
RDS_HOST = '127.0.0.1'
RDS_PORT = 6379
RDS_PASSW = None

# Scan Configuration
USER_AGENT = 'NERVE'
Expand All @@ -45,7 +46,7 @@
'config':{
'name':'Default',
'description':'My Default Scan',
'engineer':'Default',
'engineer':'John Doe',
'allow_aggressive':3,
'allow_dos':False,
'allow_bf':False,
Expand All @@ -57,6 +58,7 @@
'scan_opts':{
'interface':None,
'max_ports':100,
'custom_ports':[],
'parallel_scan':50,
'parallel_attack':30,
},
Expand Down
160 changes: 107 additions & 53 deletions core/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,94 @@ def verify(self):
dictionary_usernames = self.data['config']['dictionary']['usernames']
dictionary_passwords = self.data['config']['dictionary']['passwords']
max_ports = self.data['config']['scan_opts']['max_ports']
custom_ports = self.data['config']['scan_opts']['custom_ports']
net_interface = self.data['config']['scan_opts']['interface']
parallel_scan = self.data['config']['scan_opts']['parallel_scan']
parallel_attack = self.data['config']['scan_opts']['parallel_attack']
frequency = self.data['config']['frequency']

"""
Check Structure
"""
if not isinstance(name, str):
error = 'Option [ASSESSMENT_NAME] must be a String'
verified = False

if not isinstance(networks, list):
error = 'Option [NETWORKS] must be an Array'
verified = False

if not isinstance(excluded_networks, list):
error = 'Option [EXCLUDED_NETWORKS] must be an Array'
verified = False

if not isinstance(domains, list):
error = 'Option [DOMAINS] must be an Array'
verified = False

if not isinstance(intrusive_level, int):
error = 'Option [AGGRESSIVENESS_LEVEL] must be an Integer'
verified = False

if not isinstance(allow_dos, bool):
error = 'Option [ALLOW_DENIAL_OF_SERVICE] must be a Boolean'
verified = False

if not isinstance(allow_inet, bool):
error = 'Option [ALLOW_INTERNET_OUTBOUND] must be a Boolean'
verified = False

if not isinstance(allow_bf, bool):
error = 'Option [ALLOW_BRUTE_FORCE] must be a Boolean'
verified = False

if not isinstance(dictionary_usernames, list):
error = 'Option [DICTIONARY_USERNAMES] must be an Array'
verified = False

if not isinstance(dictionary_passwords, list):
error = 'Option [DICTIONARY_PASSWORDS] must be an Array'
verified = False

if not isinstance(net_interface, (str, type(None))):
error = 'Option [NET_INTERFACE] must be null or a String'
verified = False

if not isinstance(max_ports, int):
error = 'Option [MAX_PORTS] must be an Integer'
verified = False

if not isinstance(custom_ports, list):
error = 'Option [MAX_PORTS] must be an Array'
verified = False

if not isinstance(custom_ports, list):
error = 'Option [CUSTOM_PORTS] must be an Array'
verified = False

if not isinstance(parallel_scan, int):
error = 'Option [PARALLEL_SCAN] must be an Integer'
verified = False

if not isinstance(parallel_attack, int):
error = 'Option [PARALLEL_ATTACK] must be an Integer'
verified = False

if not isinstance(webhook, (str, type(None))):
error = 'Option [WEB_HOOK] must be null or a String'
verified = False

if not isinstance(frequency, str):
error = 'Option [FREQUENCY] must be a String'
verified = False

if not verified:
return (verified, error, self.data)

"""
Check passed values
"""

if len(name) > 30 or not self.utils.is_string_safe(name):
error = 'Option [ASSESSMENT_NAME] must not exceed 30 characters and must not have special characters.'
verified = False
Expand All @@ -63,42 +146,27 @@ def verify(self):

if webhook:
if not self.utils.is_string_url(webhook):
error = 'Option [WEBHOOK] must be a valid URL.'
error = 'Option [WEB_HOOK] must be a valid URL.'
verified = False

if frequency not in ('once', 'continuous'):
error = 'Option [SCHEDULE] must be "once" or "continuous"'
verified = False

if allow_dos not in (True, False):
error = 'Option [ALLOW_DENIAL-OF-SERVICE] must be true or false.'
verified = False

if allow_bf not in (True, False):
error = 'Option [ALLOW_BRUTEFORCE] must be true or false.'
verified = False

if allow_inet not in (True, False):
error = 'Option [ALLOW_INTERNET-OUTBOUND] must be true or false.'

if not 0 <= intrusive_level <= 3:
error = 'Option [AGGRESSIVE_LEVEL] must be between 0-3'
verified = False

try:
if intrusive_level < 0 or intrusive_level > 3:
error = 'Option [AGGRESSIVE_LEVEL] must be between 0-3'
verified = False

except TypeError:
error = 'Option [AGGRESSIVE_LEVEL] must be an Integer'
verified = False

try:
if max_ports < 10 or max_ports > 65535:
error = 'Option [MAX_PORTS] must be between 10-7000'
verified = False

except TypeError:
error = 'Option [MAX_PORTS] must be an Integer'
verified = False
if max_ports:
if not self.netutils.is_valid_port(max_ports):
error = 'Option [MAX_PORTS] must be a value between 0-65535'
verified = False

if custom_ports:
for cport in custom_ports:
if not self.netutils.is_valid_port(cport):
error = 'Option [CUSTOM_PORTS] must be an array of values between 0-65535'
verified = False

if not networks and not domains:
error = 'Options [DOMAINS] or Options [NETWORKS] must not be empty'
Expand Down Expand Up @@ -135,39 +203,22 @@ def verify(self):
if net_interface:
n = Network()
if not net_interface in n.get_nics():
error = 'Option [INTERFACE] must be valid'
error = 'Option [NET_INTERFACE] must be valid'
verified = False

else:
self.data['config']['scan_opts']['interface'] = None

try:
if parallel_attack < 10 or parallel_attack > 100:
error = 'Option [ATTACK_THREADS] must be between 10-1000'
verified = False
except TypeError:
error = 'Option [ATTACK_THREADS] must be an Integer'
verified = False

try:
if parallel_scan < 10 or parallel_scan > 100:
error = 'Option [SCAN_THREADS] must be between 10-1000'
verified = False
except TypeError:
error = 'Option [SCAN_THREADS] must be an Integer'
if not 1 <= parallel_attack <= 100:
error = 'Option [ATTACK_THREADS] must be between 1-100'
verified = False

if not isinstance(dictionary_usernames, list):
error = 'Option [DICTIONARY_USERNAMES] must be an array'
verified = False

if not isinstance(dictionary_passwords, list):
error = 'Option [DICTIONARY_PASSWORDS] must be an array'
if not 1 <= parallel_scan <= 100:
error = 'Option [SCAN_THREADS] must be between 1-100'
verified = False

except KeyError as e:
print(e)
error = 'One or more options are missing'
error = 'One or more options are missing: {}'.format(e)
verified = False

return (verified, error, self.data)
Expand Down Expand Up @@ -247,6 +298,9 @@ def get_cfg_allow_bf(self):
def get_cfg_max_ports(self):
return self.values['config']['scan_opts']['max_ports']

def get_cfg_custom_ports(self):
return self.values['config']['scan_opts']['custom_ports']

def get_cfg_usernames(self):
return self.values['config']['dictionary']['usernames']

Expand Down
14 changes: 12 additions & 2 deletions core/port_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ def __init__(self):
}
self.utils = Utils()

def scan(self, hosts, ports=100, interface=None):
def scan(self, hosts, max_ports, custom_ports, interface=None):
data = {}
hosts = ' '.join(hosts.keys())
ports = '--top-ports {}'.format(ports)
extra_args = ''
scan_cmdline = 'unpriv_scan'
ports = ''

if custom_ports:
ports = '-p {}'.format(','.join([str(p) for p in set(custom_ports)]))

elif max_ports:
ports = '--top-ports {}'.format(max_ports)

else:
ports = '--top-ports 100'


if interface:
extra_args += '-e {}'.format(interface)

Expand Down
3 changes: 2 additions & 1 deletion core/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import redis
import threading
import pickle

from core.logging import logger
from core.utils import Utils

Expand All @@ -11,7 +12,7 @@ def __init__(self):
self.utils = Utils()
self.r = None
try:
self.conn_pool = redis.ConnectionPool(host=config.RDS_HOST, port=config.RDS_PORT, db=0)
self.conn_pool = redis.ConnectionPool(host=config.RDS_HOST, port=config.RDS_PORT, password=config.RDS_PASSW, db=0)
self.r = redis.Redis(connection_pool=self.conn_pool)
except TimeoutError:
logger.error('Redis Connection Timed Out!')
Expand Down
9 changes: 9 additions & 0 deletions core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ def is_dns(self, addr):
if validators.domain(addr):
return True
return False

def is_valid_port(self, port):
try:
if 0 <= port <= 65535:
return True
return False
except TypeError:
return False


def get_primary_ip(self):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Expand Down
15 changes: 14 additions & 1 deletion static/css/nerve.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,19 @@ span.banner_sm {
.scan_type_indicator {
font-size:10px;
font-weight: bold;
color:red;
color:darkorange;
letter-spacing: 3px;
}

.withlove {
font-size:7px;
font-weight: bold;
color:lightgrey;
letter-spacing: 2px;
text-decoration: none;
}


.logo-text {
font-size:8px;
letter-spacing: 2.5px;
Expand Down Expand Up @@ -106,6 +115,10 @@ span.banner_sm {
color:orange
}

.c-darkorange {
color:darkorange
}

.c-red {
color:red
}
Expand Down
Binary file added static/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions templates/alert.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>NERVE</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href={{ url_for('static', filename="vendor/bootstrap4/css/bootstrap.min.css") }} rel="stylesheet">
<link href={{ url_for('static', filename="vendor/toastr/toastr.min.css") }} rel="stylesheet">
<link href={{ url_for('static', filename="css/nerve.css") }} rel="stylesheet">
Expand Down
Loading

0 comments on commit 8772d68

Please sign in to comment.