├── .gitignore ├── README.md ├── nseinfo-run ├── nseinfo ├── NseScript.py ├── __init__.py ├── __main__.py ├── actions │ ├── __init__.py │ ├── search.py │ └── usage.py ├── cli.py ├── nseinfo.py └── parsing │ ├── NseParsingException.py │ ├── NseScriptParser.py │ └── __init__.py ├── requirements.txt ├── setup.py └── test ├── data ├── http-shellshock.nse ├── nfs-showmount.nse ├── openvas-otp-brute.nse ├── telnet-ntlm-info.nse └── weblogic-t3-info.nse └── test_parser.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | nse_info.egg-info/ 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSEInfo 2 | 3 | NSEInfo is a tool to interactively search through nmap's NSE scripts. 4 | 5 | [![asciicast](https://asciinema.org/a/4av670luoetzj17y7oxho3juh.png)](https://asciinema.org/a/4av670luoetzj17y7oxho3juh) 6 | 7 | 8 | # Installation 9 | 10 | Needs Python 2.7. To install, run: 11 | 12 | ``` 13 | $ pip install nltk prettytable git+https://github.com/christophetd/nmap-nse-info.git 14 | ``` 15 | 16 | # Sample usages 17 | 18 | ## Search 19 | 20 | Find all NSE scripts related to NFS: 21 | 22 | ``` 23 | $ nseinfo search nfs 24 | 25 | 3 matches found. 26 | 27 | +---------------+----------------------------------------------------------+-----------------+ 28 | | Script name | Description | Categories | 29 | +---------------+----------------------------------------------------------+-----------------+ 30 | | nfs-statfs | Retrieves disk space statistics and information from a | discovery, safe | 31 | | | remote NFS share. | | 32 | +---------------+----------------------------------------------------------+-----------------+ 33 | | nfs-showmount | Shows NFS exports, like the showmount -e command. | discovery, safe | 34 | +---------------+----------------------------------------------------------+-----------------+ 35 | | nfs-ls | Attempts to get useful information about files from NFS | discovery, safe | 36 | | | exports. | | 37 | +---------------+----------------------------------------------------------+-----------------+ 38 | 39 | ``` 40 | 41 | Find all NSE exploit scripts related to SMB: 42 | 43 | ``` 44 | $ nseinfo search smb --category exploit 45 | ``` 46 | 47 | Display all the NSE scripts in the category `brute` (bruteforce): 48 | 49 | ``` 50 | $ nseinfo --show-all --category brute 51 | ``` 52 | 53 | Show the first 5 NSE scripts in the category `discover`: 54 | 55 | ``` 56 | $ nseinfo --show-all --category discovery --limit 5 57 | ``` 58 | 59 | Display all the NSE scripts installed: 60 | 61 | ``` 62 | $ nseinfo --show-all 63 | ``` 64 | 65 | If your NSE scripts are not the standard location `/usr/share/nmap/scripts/`, you can use the `-l` or `--location` option to provide your customized path. 66 | 67 | ## Usage samples 68 | 69 | Show a quick description and sample usages (if available) of the NSE script `http-wordpress-enum`: 70 | 71 | ``` 72 | $ nseinfo usage nfs-ls 73 | 74 | nfs-ls: Attempts to get useful information about files from NFS exports. 75 | 76 | 2 sample usages found: 77 | 78 | nmap -p 111 --script=nfs-ls 79 | nmap -sV --script=nfs-ls 80 | 81 | Run "nmap --script nfs-ls --help" for more information. 82 | ``` 83 | 84 | ## Running the tests 85 | 86 | ``` 87 | python -m unittest discover test 88 | ``` 89 | -------------------------------------------------------------------------------- /nseinfo-run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from nseinfo.nseinfo import main 4 | 5 | if __name__ == '__main__': 6 | main() -------------------------------------------------------------------------------- /nseinfo/NseScript.py: -------------------------------------------------------------------------------- 1 | 2 | class NseScript: 3 | def __init__(self, name=None, short_description=None, full_description=None, sample_usages=None, categories=None, reference_links=None): 4 | self.name = name 5 | self.short_description = short_description 6 | self.full_description = full_description 7 | self.sample_usages = sample_usages 8 | self.categories = categories 9 | self.reference_links = reference_links 10 | 11 | def __str__(self): 12 | return """NseScript( 13 | name = {0} 14 | description = {1}, 15 | categories = {2}, 16 | sample_usages = {3} 17 | )""".format(self.name, self.short_description, self.categories, self.sample_usages) -------------------------------------------------------------------------------- /nseinfo/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from actions import * 4 | from parsing import * -------------------------------------------------------------------------------- /nseinfo/__main__.py: -------------------------------------------------------------------------------- 1 | import nseinfo 2 | -------------------------------------------------------------------------------- /nseinfo/actions/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python 2 | 3 | import search 4 | import usage -------------------------------------------------------------------------------- /nseinfo/actions/search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import prettytable 4 | 5 | from sys import stderr 6 | from glob import glob 7 | from nseinfo.parsing.NseScriptParser import NseScriptParser 8 | 9 | COLOR_RED= '\033[91m' 10 | COLOR_RESET = '\033[0m' 11 | COLOR_BOLD = '\033[1m' 12 | 13 | CATEGORY_SHOW_ALL = 'show scripts from all categories' 14 | 15 | def build_nse_scripts_list(args): 16 | nse_scripts = glob(args.nse_location + '/*.nse') 17 | if len(nse_scripts) == 0: 18 | stderr.write('There does not seem to be any NSE script in {}\n'.format(args.nse_location)) 19 | 20 | scripts = [] 21 | parser = NseScriptParser() 22 | 23 | for nse_script_file in nse_scripts: 24 | with open(nse_script_file, 'r') as file: 25 | try: 26 | parsed_script = parser.parse(os.path.basename(nse_script_file), file.read()) 27 | scripts.append(parsed_script) 28 | except Exception as e: 29 | stderr.write('Unable to parse NSE file {}: {}\n'.format(nse_script_file, e.message)) 30 | exit(1) 31 | 32 | return scripts 33 | 34 | def get_script_score(script, query): 35 | score = 0 36 | keywords = re.compile("\s*").split(query) 37 | for keyword in keywords: 38 | if keyword.lower() in script.name: 39 | score += 4 40 | 41 | if keyword.lower() in script.short_description.lower(): 42 | score += 2 43 | elif keyword.lower() in script.full_description.lower(): 44 | score += 1 45 | 46 | return score 47 | 48 | def rank_nse_scripts(scripts, query): 49 | # Keep a list of scripts with their ranking 50 | rankings = [] 51 | for script in scripts: 52 | score = get_script_score(script, query) 53 | if score > 0: 54 | rankings.append( (script, score) ) 55 | 56 | return sorted(rankings, reverse = True, key = lambda el: el[1]) 57 | 58 | 59 | def get_displayable_description(description): 60 | MAX_COL_LEN = 60 61 | words = re.compile("\s*").split(description) 62 | parts = [""] 63 | curr_part = 0 64 | 65 | for word in words: 66 | if len(parts[curr_part] + word) <= MAX_COL_LEN: 67 | parts[curr_part] += word + " " 68 | else: 69 | parts.append(word+" ") 70 | curr_part += 1 71 | 72 | return "\n ".join(parts) 73 | 74 | def color_matches(str, query): 75 | if query is None or len(query.strip()) is 0: 76 | return str 77 | 78 | keywords = re.compile("\s*").split(query) 79 | for keyword in keywords: 80 | regxp = re.compile("("+re.escape(keyword)+")", re.IGNORECASE) 81 | str = regxp.sub(COLOR_RED + COLOR_BOLD + "\\1" + COLOR_RESET, str) 82 | 83 | return str 84 | 85 | def print_result(args, matched_scripts): 86 | query = args.search_query 87 | num_matches = len(matched_scripts) 88 | if num_matches == 0: 89 | filter_msg = "in categorie(s) {}".format(', '.join(args.category_filter)) if len(args.category_filter) > 0 else '' 90 | print 'No match found for "{}"{}'.format(query, filter_msg) 91 | return 92 | 93 | if num_matches == 1: 94 | msg = '1 match found.' 95 | else: 96 | msg = '{} matches found.'.format(num_matches) 97 | 98 | print msg 99 | print '' 100 | 101 | table = prettytable.PrettyTable([ 102 | 'Script name', 103 | 'Description', 104 | 'Categories' 105 | ], hrules=prettytable.ALL) 106 | 107 | for script_match in matched_scripts: 108 | script = script_match[0] 109 | table.add_row([ 110 | color_matches(script.name, query), 111 | color_matches(get_displayable_description(script.short_description), query), 112 | ', '.join(script.categories) if script.categories else '' 113 | ]) 114 | 115 | print table 116 | 117 | 118 | def filter_category(args, ranked_scripts): 119 | # Filter out by category if needed 120 | if len(args.category_filter) == 0: 121 | return ranked_scripts 122 | 123 | matched_scripts = [] 124 | for script in ranked_scripts: 125 | if len(set(script[0].categories).intersection(args.category_filter)) > 0: 126 | matched_scripts.append(script) 127 | 128 | return matched_scripts 129 | 130 | def do_search(args): 131 | nse_scripts = build_nse_scripts_list(args) 132 | ranked_scripts = rank_nse_scripts(nse_scripts, args.search_query) 133 | matched_scripts = filter_category(args, ranked_scripts) 134 | 135 | if args.limit > 0: 136 | matched_scripts = matched_scripts[0:args.limit] 137 | 138 | print_result(args, matched_scripts) 139 | 140 | def do_show_all(args): 141 | nse_scripts = build_nse_scripts_list(args) 142 | nse_scripts = [ (s, 1) for s in nse_scripts] 143 | matches = filter_category(args, nse_scripts) 144 | 145 | if args.limit > 0: 146 | matches = matches[0:args.limit] 147 | 148 | print_result(args, matches) -------------------------------------------------------------------------------- /nseinfo/actions/usage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sys import stderr 3 | from nseinfo.parsing.NseScriptParser import NseScriptParser 4 | from nseinfo.parsing.NseParsingException import NseParsingException 5 | 6 | 7 | def do_usage(args): 8 | try: 9 | path = os.path.join(args.nse_location, args.search_query + '.nse') 10 | with open(path) as f: 11 | try: 12 | script = NseScriptParser().parse(args.search_query, f.read()) 13 | except NseParsingException as e: 14 | stderr.write('Unable to parse NSE script {}: {}\n'.format(path, e.message)) 15 | exit(1) 16 | except IOError as e: 17 | stderr.write('Unable to find NSE script "{}" in {}\n'.format(args.search_query, args.nse_location)) 18 | exit(1) 19 | 20 | 21 | print '' 22 | print '{}: {}\n'.format(args.search_query, script.short_description) 23 | num_usages = len(script.sample_usages) 24 | if num_usages == 0: 25 | print 'No sample usage found for script "{}".'.format(args.search_query) 26 | else: 27 | print '{} sample usage{} found:'.format(num_usages, 's' if num_usages > 1 else '') 28 | print '' 29 | sep = ' ' 30 | print sep + ('\n'+sep).join(script.sample_usages) 31 | print '' 32 | 33 | print 'Run "nmap --script {} --help" for more information.'.format(args.search_query) -------------------------------------------------------------------------------- /nseinfo/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | # https://nmap.org/nsedoc/index.html 4 | categories = [ 5 | 'auth', 6 | 'broadcast', 7 | 'brute', 8 | 'default', 9 | 'discovery', 10 | 'dos', 11 | 'exploit', 12 | 'external', 13 | 'fuzzer', 14 | 'intrusive', 15 | 'malware', 16 | 'safe', 17 | 'version', 18 | 'vuln', 19 | ] 20 | 21 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 22 | 23 | parser.add_argument( 24 | '-l', '--nse-location', 25 | metavar = 'location', 26 | help = 'The location in which the NSE scripts are located', 27 | default = '/usr/share/nmap/scripts/', 28 | dest = 'nse_location' 29 | ) 30 | 31 | parser.add_argument( 32 | '-c', '--category', 33 | metavar = 'category', 34 | help = 'Only show NSE scripts belonging to specific categories (OR matching).', 35 | choices = categories, 36 | default = [], 37 | action = 'append', 38 | dest = 'category_filter' 39 | ) 40 | 41 | parser.add_argument( 42 | '--limit', 43 | metavar = 'limit', 44 | help = 'Limit the number of search results', 45 | default = 0, 46 | type = int 47 | ) 48 | 49 | parser.add_argument( 50 | '--show-all', 51 | action = 'store_true', 52 | dest = 'show_all' 53 | ) 54 | 55 | parser.add_argument( 56 | 'action', 57 | help = 'The action to perform', 58 | choices = ['search', 'usage'], 59 | nargs = '?' 60 | ) 61 | 62 | parser.add_argument( 63 | 'search_query', 64 | help = 'Search query (or NSE script name depending on the action)', 65 | nargs = '?' 66 | ) -------------------------------------------------------------------------------- /nseinfo/nseinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from sys import stderr 5 | from actions import search, usage 6 | import cli 7 | 8 | def main(): 9 | args = cli.parser.parse_args() 10 | if not os.path.isdir(args.nse_location): 11 | stderr.write('Invalid NSE scripts location: {}\n'.format(args.nse_location)) 12 | exit(1) 13 | 14 | if args.show_all: 15 | search.do_show_all(args) 16 | elif args.action == 'search' and args.search_query: 17 | search.do_search(args) 18 | 19 | elif args.action == 'usage' and args.search_query: 20 | usage.do_usage(args) 21 | else: 22 | cli.parser.print_help() -------------------------------------------------------------------------------- /nseinfo/parsing/NseParsingException.py: -------------------------------------------------------------------------------- 1 | class NseParsingException(ValueError): 2 | def __init__(self, message): 3 | super(ValueError, self).__init__(message) -------------------------------------------------------------------------------- /nseinfo/parsing/NseScriptParser.py: -------------------------------------------------------------------------------- 1 | from NseParsingException import NseParsingException 2 | from nseinfo.NseScript import NseScript 3 | import nltk 4 | from nltk import tokenize 5 | import re 6 | 7 | NSE_EXTENSION = '.nse' 8 | 9 | class NseScriptParser: 10 | """ 11 | Builds a NseScript object from the contents of a NSE file 12 | """ 13 | def parse(self, file_name, nse_file_contents): 14 | try: 15 | tokenize.sent_tokenize('') 16 | except LookupError as e: 17 | nltk.download('punkt') 18 | 19 | if nse_file_contents is None or len(nse_file_contents) == 0: 20 | raise NseParsingException("Empty NSE script contents") 21 | 22 | # Remove extension if needed 23 | if file_name.endswith(NSE_EXTENSION): 24 | file_name = file_name[:-len(NSE_EXTENSION)] 25 | 26 | 27 | full_description = self.__parse_full_description(nse_file_contents) 28 | short_description = self.__get_short_description(full_description) 29 | categories = self.__parse_categories(nse_file_contents) 30 | sample_usages = self.__parse_sample_usages(nse_file_contents) 31 | 32 | return NseScript( 33 | full_description = full_description, 34 | short_description = short_description, 35 | categories = categories, 36 | sample_usages = sample_usages, 37 | name = file_name 38 | ) 39 | 40 | def __parse_full_description(self, nse_file_contents): 41 | description_regex = r'.*?description\s*=\s*\[\[(.*?)\]\]' 42 | full_description_match = re.match(description_regex, nse_file_contents, re.S|re.I) 43 | 44 | if not full_description_match or len(full_description_match.groups()) < 1: 45 | # Attempt the alternative format where the description is on a single line 46 | description_regex = r'.*?description\s*=\s*"(.*?)"' 47 | full_description_match = re.match(description_regex, nse_file_contents, re.S | re.I) 48 | if not full_description_match or len(full_description_match.groups()) < 1: 49 | raise NseParsingException("Unable to parse description from NSE script content") 50 | 51 | full_description_raw = full_description_match.groups(1)[0] 52 | 53 | return self.__strip_html_tags(full_description_raw.strip()) 54 | 55 | def __get_short_description(self, full_description): 56 | sentences = tokenize.sent_tokenize(full_description) 57 | return sentences[0].replace('\n', ' ') 58 | 59 | def __parse_categories(self, nse_file_contents): 60 | categories_regex = r'.*?categories\s*=\s*\{(.*?)\}' 61 | categories_match = re.match(categories_regex, nse_file_contents, re.S|re.I) 62 | 63 | if not categories_match or len(categories_match.groups()) < 1: 64 | raise NseParsingException("Unable to parse categories from NSE script content") 65 | 66 | categories_raw = re.sub(r'[^a-zA-Z,]', '', categories_match.groups(1)[0]) 67 | categories = categories_raw.split(',') 68 | 69 | return categories 70 | 71 | def __parse_sample_usages(self, nse_file_contents): 72 | sample_usages_regex = r'.*?@usage(.*)@output' 73 | sample_usages_match = re.match(sample_usages_regex, nse_file_contents, re.S|re.I) 74 | 75 | if not sample_usages_match or len(sample_usages_match.groups()) < 1: 76 | return [] 77 | 78 | sample_usages_raw = sample_usages_match.groups(1)[0].lstrip().replace("-- ", "").split("\n")[:-1] 79 | return [ el for el in sample_usages_raw if re.match("^\s*nmap.+", el, re.I) ] 80 | 81 | def __strip_html_tags(self, str): 82 | return re.sub('<[^<]+?>', '', str) -------------------------------------------------------------------------------- /nseinfo/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christophetd/nmap-nse-info/2a62b7c7c26bde77a65b50b81b69174a382f6477/nseinfo/parsing/__init__.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nltk 2 | prettytable 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | setup( 8 | name = "nse-info", 9 | version = "0.1", 10 | author = "Christophe Tafani-Dereeper", 11 | author_email = "christophe@tafani-dereeper.me", 12 | description = ("An interactive command-line tool to search and browse NMAP NSE scripts"), 13 | keywords = "nse nmap script", 14 | packages=['nseinfo', 'nseinfo/actions', 'nseinfo/parsing'], 15 | entry_points={ 16 | "console_scripts": ['nseinfo = nseinfo.nseinfo:main'] 17 | }, 18 | long_description=read('README.md'), 19 | classifiers=[ 20 | "Development Status :: 3 - Alpha", 21 | "Topic :: Utilities", 22 | ], 23 | ) -------------------------------------------------------------------------------- /test/data/http-shellshock.nse: -------------------------------------------------------------------------------- 1 | local http = require "http" 2 | local shortport = require "shortport" 3 | local stdnse = require "stdnse" 4 | local string = require "string" 5 | local vulns = require "vulns" 6 | 7 | description = [[ 8 | Attempts to exploit the "shellshock" vulnerability (CVE-2014-6271 and CVE-2014-7169) in web applications. 9 | 10 | To detect this vulnerability the script executes a command that prints a 11 | random string and then attempts to find it inside the response body. Web apps that 12 | don't print back information won't be detected with this method. 13 | 14 | By default the script injects the payload in the HTTP headers User-Agent, 15 | Cookie, Referer and also uses the payload as the header name. 16 | 17 | Vulnerability originally discovered by Stephane Chazelas. 18 | 19 | References: 20 | * http://www.openwall.com/lists/oss-security/2014/09/24/10 21 | * http://seclists.org/oss-sec/2014/q3/685 22 | * https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169 23 | * http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271 24 | ]] 25 | 26 | --- 27 | -- @usage 28 | -- nmap -sV -p- --script http-shellshock 29 | -- nmap -sV -p- --script http-shellshock --script-args uri=/cgi-bin/bin,cmd=ls 30 | -- @output 31 | -- PORT STATE SERVICE REASON 32 | -- 80/tcp open http syn-ack 33 | -- | http-shellshock: 34 | -- | VULNERABLE: 35 | -- | HTTP Shellshock vulnerability 36 | -- | State: VULNERABLE (Exploitable) 37 | -- | IDs: CVE:CVE-2014-6271 38 | -- | This web application might be affected by the vulnerability known as Shellshock. It seems the server 39 | -- | is executing commands injected via malicious HTTP headers. 40 | -- | 41 | -- | Disclosure date: 2014-09-24 42 | -- | References: 43 | -- | http://www.openwall.com/lists/oss-security/2014/09/24/10 44 | -- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169 45 | -- | http://seclists.org/oss-sec/2014/q3/685 46 | -- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271 47 | -- 48 | -- @xmloutput 49 | -- HTTP Shellshock vulnerability 50 | -- VULNERABLE (Exploitable) 51 | -- 52 | -- CVE:CVE-2014-6271 53 | --
54 | -- 55 | -- This web application might be affected by the vulnerability known as Shellshock. It seems the server 56 | -- is executing commands injected via malicious HTTP headers. 57 | --
58 | -- 59 | --
60 | -- 2014 61 | -- 24 62 | -- 09 63 | --
64 | -- 65 | -- 2014-09-24 66 | -- 67 | -- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169 68 | -- http://www.openwall.com/lists/oss-security/2014/09/24/10 69 | -- http://seclists.org/oss-sec/2014/q3/685 70 | -- http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271 71 | --
72 | -- @args http-shellshock.uri URI. Default: / 73 | -- @args http-shellshock.header HTTP header to use in requests. Default: User-Agent 74 | -- @args http-shellshock.cmd Custom command to send inside payload. Default: nil 75 | --- 76 | author = {"Paulino Calderon "} 77 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 78 | categories = {"exploit","vuln","intrusive"} 79 | 80 | portrule = shortport.http 81 | 82 | function generate_http_req(host, port, uri, custom_header, cmd) 83 | local rnd = nil 84 | --Set custom or probe with random string as cmd 85 | if cmd ~= nil then 86 | cmd = '() { :;}; '..cmd 87 | else 88 | rnd = stdnse.generate_random_string(15) 89 | cmd = '() { :;}; echo; echo "'..rnd..'"' 90 | end 91 | -- Plant the payload in the HTTP headers 92 | local options = {header={}} 93 | options["no_cache"] = true 94 | if custom_header == nil then 95 | stdnse.debug1("Sending '%s' in HTTP headers:User-Agent,Cookie and Referer", cmd) 96 | options["header"]["User-Agent"] = cmd 97 | options["header"]["Referer"] = cmd 98 | options["header"]["Cookie"] = cmd 99 | options["header"][cmd] = cmd 100 | else 101 | stdnse.debug1("Sending '%s' in HTTP header '%s'", cmd, custom_header) 102 | options["header"][custom_header] = cmd 103 | end 104 | local req = http.get(host, port, uri, options) 105 | 106 | if not(cmd) then 107 | return req 108 | else 109 | return req, rnd 110 | end 111 | end 112 | 113 | action = function(host, port) 114 | local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or nil 115 | local http_header = stdnse.get_script_args(SCRIPT_NAME..".header") or nil 116 | local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or '/' 117 | local rnd = nil 118 | local req, rnd = generate_http_req(host, port, uri, http_header, nil) 119 | if req.status == 200 and string.match(req.body, rnd) ~= nil then 120 | local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port) 121 | local vuln = { 122 | title = 'HTTP Shellshock vulnerability', 123 | state = vulns.STATE.NOT_VULN, 124 | description = [[ 125 | This web application might be affected by the vulnerability known as Shellshock. It seems the server 126 | is executing commands injected via malicious HTTP headers. 127 | ]], 128 | IDS = {CVE = 'CVE-2014-6271'}, 129 | references = { 130 | 'http://www.openwall.com/lists/oss-security/2014/09/24/10', 131 | 'http://seclists.org/oss-sec/2014/q3/685', 132 | 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169' 133 | }, 134 | dates = { 135 | disclosure = {year = '2014', month = '09', day = '24'}, 136 | }, 137 | } 138 | stdnse.debug1("Random pattern '%s' was found in page. Host seems vulnerable.", rnd) 139 | vuln.state = vulns.STATE.EXPLOIT 140 | if cmd ~= nil then 141 | req = generate_http_req(host, port, uri, http_header, cmd) 142 | vuln.exploit_results = req.body 143 | end 144 | return vuln_report:make_output(vuln) 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/data/nfs-showmount.nse: -------------------------------------------------------------------------------- 1 | local rpc = require "rpc" 2 | local shortport = require "shortport" 3 | local stdnse = require "stdnse" 4 | local table = require "table" 5 | local string = require "string" 6 | 7 | description = [[ 8 | Shows NFS exports, like the showmount -e command. 9 | ]] 10 | 11 | --- 12 | -- @output 13 | -- PORT STATE SERVICE 14 | -- 111/tcp open rpcbind 15 | -- | nfs-showmount: 16 | -- | /home/storage/backup 10.46.200.0/255.255.255.0 17 | -- |_ /home 1.2.3.4/255.255.255.255 10.46.200.0/255.255.255.0 18 | -- 19 | 20 | -- Version 0.7 21 | 22 | -- Created 11/23/2009 - v0.1 - created by Patrik Karlsson 23 | -- Revised 11/24/2009 - v0.2 - added RPC query to find mountd ports 24 | -- Revised 11/24/2009 - v0.3 - added a hostrule instead of portrule 25 | -- Revised 11/26/2009 - v0.4 - reduced packet sizes and documented them 26 | -- Revised 01/24/2009 - v0.5 - complete rewrite, moved all NFS related code into nselib/nfs.lua 27 | -- Revised 02/22/2009 - v0.6 - adapted to support new RPC library 28 | -- Revised 03/13/2010 - v0.7 - converted host to port rule 29 | 30 | 31 | author = "Patrik Karlsson" 32 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 33 | categories = {"discovery", "safe"} 34 | dependencies = {"rpc-grind"} 35 | 36 | 37 | portrule = shortport.port_or_service(111, {"rpcbind", "mountd"}, {"tcp", "udp"} ) 38 | 39 | local function get_exports(host, port) 40 | local mnt = rpc.Mount:new() 41 | local mountver 42 | if host.registry.nfs then 43 | mountver = host.registry.nfs.mountver 44 | else 45 | host.registry.nfs = {} 46 | end 47 | if mountver == nil then 48 | local low, high = string.match(port.version.version, "(%d)%-(%d)") 49 | if high == nil then 50 | mountver = tonumber(port.version.version) 51 | else 52 | mountver = tonumber(high) 53 | end 54 | end 55 | local mnt_comm = rpc.Comm:new('mountd', mountver) 56 | local status, result = mnt_comm:Connect(host, port) 57 | if ( not(status) ) then 58 | stdnse.debug4("get_exports: %s", result) 59 | return false, result 60 | end 61 | host.registry.nfs.mountver = mountver 62 | host.registry.nfs.mountport = port 63 | local status, mounts = mnt:Export(mnt_comm) 64 | mnt_comm:Disconnect() 65 | if ( not(status) ) then 66 | stdnse.debug4("get_exports: %s", mounts) 67 | end 68 | return status, mounts 69 | end 70 | 71 | action = function(host, port) 72 | 73 | local status, mounts, proto 74 | local result = {} 75 | 76 | if port.service == "mountd" then 77 | status, mounts = get_exports( host, port ) 78 | else 79 | status, mounts = rpc.Helper.ShowMounts( host, port ) 80 | end 81 | 82 | if not status or mounts == nil then 83 | return stdnse.format_output(false, mounts) 84 | end 85 | 86 | for _, v in ipairs( mounts ) do 87 | local entry = v.name .. " " .. stdnse.strjoin(" ", v) 88 | table.insert( result, entry ) 89 | end 90 | 91 | return stdnse.format_output( true, result ) 92 | 93 | end 94 | -------------------------------------------------------------------------------- /test/data/openvas-otp-brute.nse: -------------------------------------------------------------------------------- 1 | local brute = require "brute" 2 | local creds = require "creds" 3 | local nmap = require "nmap" 4 | local shortport = require "shortport" 5 | local stdnse = require "stdnse" 6 | local string = require "string" 7 | 8 | local openssl = stdnse.silent_require "openssl" 9 | 10 | description=[[ 11 | Performs brute force password auditing against a OpenVAS vulnerability scanner daemon using the OTP 1.0 protocol. 12 | ]] 13 | 14 | --- 15 | -- @output 16 | -- PORT STATE SERVICE REASON VERSION 17 | -- 9391/tcp open ssl/openvas syn-ack 18 | -- | openvas-otp-brute: 19 | -- | Accounts 20 | -- | openvas:openvas - Valid credentials 21 | -- | Statistics 22 | -- |_ Performed 4 guesses in 4 seconds, average tps: 1 23 | -- 24 | -- @args openvas-otp-brute.threads sets the number of threads. Default: 4 25 | 26 | author = "Vlatko Kosturjak" 27 | 28 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 29 | 30 | categories = {"intrusive", "brute"} 31 | 32 | 33 | portrule = shortport.port_or_service({9390,9391}, "openvas", "tcp") 34 | 35 | Driver = 36 | { 37 | new = function (self, host, port) 38 | local o = { host = host, port = port } 39 | setmetatable (o,self) 40 | self.__index = self 41 | return o 42 | end, 43 | 44 | connect = function ( self ) 45 | self.socket = nmap.new_socket() 46 | if ( not(self.socket:connect(self.host, self.port, "ssl")) ) then 47 | return false 48 | end 49 | return true 50 | end, 51 | 52 | login = function( self, username, password ) 53 | local status, err = self.socket:send("< OTP/1.0 >\n") 54 | 55 | if ( not ( status ) ) then 56 | local err = brute.Error:new( "Unable to send handshake" ) 57 | err:setAbort(true) 58 | return false, err 59 | end 60 | 61 | local response 62 | status, response = self.socket:receive_buf("\r?\n", false) 63 | if ( not(status) or response ~= "< OTP/1.0 >" ) then 64 | local err = brute.Error:new( "Bad handshake from server: "..response ) 65 | err:setAbort(true) 66 | return false, err 67 | end 68 | 69 | status, err = self.socket:send(username.."\n") 70 | if ( not(status) ) then 71 | local err = brute.Error:new( "Couldn't send user: "..username ) 72 | err:setAbort( true ) 73 | return false, err 74 | end 75 | 76 | status, err = self.socket:send(password.."\n") 77 | if ( not(status) ) then 78 | local err = brute.Error:new( "Couldn't send password: "..password ) 79 | err:setAbort( true ) 80 | return false, err 81 | end 82 | 83 | -- Create a buffer and receive the first line 84 | local line 85 | status, line = self.socket:receive_buf("\r?\n", false) 86 | 87 | if (line == nil or string.match(line,"Bad login")) then 88 | stdnse.debug2("Bad login: %s/%s", username, password) 89 | return false, brute.Error:new( "Bad login" ) 90 | elseif (string.match(line,"SERVER <|>")) then 91 | 92 | stdnse.debug1("Good login: %s/%s", username, password) 93 | return true, creds.Account:new(username, password, creds.State.VALID) 94 | end 95 | 96 | stdnse.debug1("WARNING: Unhandled response: %s", line) 97 | return false, brute.Error:new( "unhandled response" ) 98 | end, 99 | 100 | disconnect = function( self ) 101 | self.socket:close() 102 | end, 103 | } 104 | 105 | action = function(host, port) 106 | local engine = brute.Engine:new(Driver, host, port) 107 | engine:setMaxThreads(1) 108 | engine.options.script_name = SCRIPT_NAME 109 | local status, result = engine:start() 110 | return result 111 | end 112 | 113 | -------------------------------------------------------------------------------- /test/data/telnet-ntlm-info.nse: -------------------------------------------------------------------------------- 1 | local bin = require "bin" 2 | local comm = require "comm" 3 | local shortport = require "shortport" 4 | local stdnse = require "stdnse" 5 | local smbauth = require "smbauth" 6 | local string = require "string" 7 | 8 | 9 | description = [[ 10 | This script enumerates information from remote Microsoft Telnet services with NTLM 11 | authentication enabled. 12 | 13 | Sending a MS-TNAP NTLM authentication request with null credentials will cause the 14 | remote service to respond with a NTLMSSP message disclosing information to include 15 | NetBIOS, DNS, and OS build version. 16 | ]] 17 | 18 | 19 | --- 20 | -- @usage 21 | -- nmap -p 23 --script telnet-ntlm-info 22 | -- 23 | -- @output 24 | -- 23/tcp open telnet 25 | -- | telnet-ntlm-info: 26 | -- | Target_Name: ACTIVETELNET 27 | -- | NetBIOS_Domain_Name: ACTIVETELNET 28 | -- | NetBIOS_Computer_Name: HOST-TEST2 29 | -- | DNS_Domain_Name: somedomain.com 30 | -- | DNS_Computer_Name: host-test2.somedomain.com 31 | -- | DNS_Tree_Name: somedomain.com 32 | -- |_ Product_Version: 5.1.2600 33 | -- 34 | --@xmloutput 35 | -- ACTIVETELNET 36 | -- ACTIVETELNET 37 | -- HOST-TEST2 38 | -- somedomain.com 39 | -- host-test2.somedomain.com 40 | -- somedomain.com 41 | -- 5.1.2600 42 | 43 | 44 | author = "Justin Cacak" 45 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 46 | categories = {"default", "discovery", "safe"} 47 | 48 | 49 | local _, ntlm_auth_blob = smbauth.get_security_blob( 50 | nil, nil, nil, nil, nil, nil, nil, 51 | 0x00000001 + -- Negotiate Unicode 52 | 0x00000002 + -- Negotiate OEM strings 53 | 0x00000004 + -- Request Target 54 | 0x00000200 + -- Negotiate NTLM 55 | 0x00008000 + -- Negotiate Always Sign 56 | 0x00080000 + -- Negotiate NTLM2 Key 57 | 0x20000000 + -- Negotiate 128 58 | 0x80000000 -- Negotiate 56 59 | ) 60 | 61 | -- 62 | -- Create MS-TNAP Login Packet (Option Command IS) 63 | -- Ref: http://msdn.microsoft.com/en-us/library/cc247789.aspx 64 | local tnap_login_packet = bin.pack(" 0 then 108 | output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name 109 | end 110 | 111 | if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then 112 | output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name 113 | end 114 | 115 | if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then 116 | output.DNS_Domain_Name = ntlm_decoded.dns_domain_name 117 | end 118 | 119 | if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then 120 | output.DNS_Computer_Name = ntlm_decoded.fqdn 121 | end 122 | 123 | if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then 124 | output.DNS_Tree_Name = ntlm_decoded.dns_forest_name 125 | end 126 | 127 | if ntlm_decoded.os_major_version then 128 | output.Product_Version = string.format("%d.%d.%d", 129 | ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build) 130 | end 131 | 132 | return output 133 | 134 | end 135 | -------------------------------------------------------------------------------- /test/data/weblogic-t3-info.nse: -------------------------------------------------------------------------------- 1 | local comm = require "comm" 2 | local string = require "string" 3 | local shortport = require "shortport" 4 | local nmap = require "nmap" 5 | 6 | description = "Detect the T3 RMI protocol and Weblogic version" 7 | author = "Alessandro ZANNI , Daniel Miller" 8 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 9 | categories = {"default","safe","discovery","version"} 10 | 11 | portrule = function(host, port) 12 | if type(port.version) == "table" and port.version.name_confidence > 3 and port.version.product ~= nil then 13 | return string.find(port.version.product, "WebLogic", 1, true) and nmap.version_intensity() >= 7 14 | end 15 | return shortport.version_port_or_service({7001,7002,7003},"http")(host,port) 16 | end 17 | 18 | action = function(host, port) 19 | local status, result = comm.exchange(host, port, 20 | "t3 12.1.2\nAS:2048\nHL:19\n\n") 21 | 22 | if (not status) then 23 | return nil 24 | end 25 | 26 | local weblogic_version = string.match(result, "^HELO:(%d+%.%d+%.%d+%.%d+)%.") 27 | 28 | local rval = nil 29 | port.version = port.version or {} 30 | local extrainfo = port.version.extrainfo 31 | if extrainfo == nil then 32 | extrainfo = "" 33 | else 34 | extrainfo = extrainfo .. "; " 35 | end 36 | if weblogic_version then 37 | if weblogic_version == "12.1.2" then 38 | status, result = comm.exchange(host, port, 39 | "t3 11.1.2\nAS:2048\nHL:19\n\n") 40 | weblogic_version = string.match(result, "^HELO:(%d+%.%d+%.%d+%.%d+)%.") 41 | if weblogic_version == "11.1.2" then 42 | -- Server just echoes whatever version we send. 43 | rval = "T3 protocol in use (Unknown WebLogic version)" 44 | else 45 | port.version.version = weblogic_version 46 | rval = "T3 protocol in use (WebLogic version: " .. weblogic_version .. ")" 47 | end 48 | else 49 | port.version.version = weblogic_version 50 | rval = "T3 protocol in use (WebLogic version: " .. weblogic_version .. ")" 51 | end 52 | port.version.extrainfo = extrainfo .. "T3 enabled" 53 | elseif string.match(result, "^LGIN:") then 54 | port.version.extrainfo = extrainfo .. "T3 enabled" 55 | rval = "T3 protocol in use (handshake failed)" 56 | elseif string.match(result, "^SERV:") then 57 | port.version.extrainfo = extrainfo .. "T3 enabled" 58 | rval = "T3 protocol in use (No such service)" 59 | elseif string.match(result, "^UNAV:") then 60 | port.version.extrainfo = extrainfo .. "T3 enabled" 61 | rval = "T3 protocol in use (Service unavailable)" 62 | elseif string.match(result, "^LICN:") then 63 | port.version.extrainfo = extrainfo .. "T3 enabled" 64 | rval = "T3 protocol in use (No license)" 65 | elseif string.match(result, "^RESC:") then 66 | port.version.extrainfo = extrainfo .. "T3 enabled" 67 | rval = "T3 protocol in use (No resource)" 68 | elseif string.match(result, "^VERS:") then 69 | weblogic_version = string.match(result, "^VERS:Incompatible versions %- this server:(%d+%.%d+%.%d+%.%d+)") 70 | if weblogic_version then 71 | port.version.version = weblogic_version 72 | end 73 | port.version.extrainfo = extrainfo .. "T3 enabled" 74 | rval = "T3 protocol in use (Incompatible version)" 75 | elseif string.match(result, "^CATA:") then 76 | port.version.extrainfo = extrainfo .. "T3 enabled" 77 | rval = "T3 protocol in use (Catastrophic failure)" 78 | elseif string.match(result, "^CMND:") then 79 | port.version.extrainfo = extrainfo .. "T3 enabled" 80 | rval = "T3 protocol in use (No such command)" 81 | end 82 | 83 | if rval then 84 | if port.version.product == nil then 85 | port.version.product = "WebLogic application server" 86 | end 87 | nmap.set_port_version(host, port, "hardmatched") 88 | end 89 | 90 | return rval 91 | end 92 | -------------------------------------------------------------------------------- /test/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from nseinfo.parsing.NseScriptParser import NseScriptParser 4 | 5 | CURR_DIR = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | class TestParser(unittest.TestCase): 8 | def setUp(self): 9 | self.parser = NseScriptParser() 10 | 11 | def parse(self, data_file_name): 12 | with open(CURR_DIR + '/data/' + data_file_name) as data_file: 13 | return self.parser.parse(data_file_name, data_file.read()) 14 | 15 | 16 | def test_parsing(self): 17 | script = self.parse('http-shellshock.nse') 18 | self.assertEquals(script.name, 'http-shellshock') 19 | self.assertEquals(script.short_description, 'Attempts to exploit the "shellshock" vulnerability (CVE-2014-6271 and CVE-2014-7169) in web applications.') 20 | self.assertEquals(set(script.categories), set(["exploit", "vuln", "intrusive"])) 21 | self.assertEquals(script.sample_usages, [ 22 | 'nmap -sV -p- --script http-shellshock ', 23 | 'nmap -sV -p- --script http-shellshock --script-args uri=/cgi-bin/bin,cmd=ls ' 24 | ]) 25 | 26 | def test_parsing2(self): 27 | script = self.parse('openvas-otp-brute.nse') 28 | self.assertEquals(script.full_description, 'Performs brute force password auditing against a OpenVAS vulnerability scanner daemon using the OTP 1.0 protocol.') 29 | 30 | def test_strips_html(self): 31 | script = self.parse('nfs-showmount.nse') 32 | self.assertEquals(script.short_description, 'Shows NFS exports, like the showmount -e command.') 33 | 34 | def test_parses_short_description_on_multiple_lines(self): 35 | script = self.parse('telnet-ntlm-info.nse') 36 | self.assertEquals(script.short_description, 'This script enumerates information from remote Microsoft Telnet services with NTLM authentication enabled.') 37 | 38 | def test_parses_description_alternative_format(self): 39 | script = self.parse('weblogic-t3-info.nse') 40 | self.assertEquals(script.short_description, 'Detect the T3 RMI protocol and Weblogic version') 41 | self.assertEquals(script.short_description, script.full_description) 42 | """ 43 | def test_isupper(self): 44 | self.assertTrue('FOoO'.isupper()) 45 | self.assertFalse('Foo'.isupper()) 46 | 47 | def test_split(self): 48 | s = 'hello world' 49 | self.assertEqual(s.split(), ['hello', 'world']) 50 | # check that s.split fails when the separator is not a string 51 | with self.assertRaises(TypeError): 52 | s.split(2) 53 | """ --------------------------------------------------------------------------------