Codebase list faraday-plugins / e02d6cf faraday_plugins / plugins / repo / nikto / plugin.py
e02d6cf

Tree @e02d6cf (Download .tar.gz)

plugin.py @e02d6cfraw · history · blame

"""
Faraday Penetration Test IDE
Copyright (C) 2013  Infobyte LLC (http://www.infobytesec.com/)
See the file 'doc/LICENSE' for the license information
"""
import re
import xml.etree.ElementTree as ET
from html.parser import HTMLParser

from faraday_plugins.plugins import plugins_utils
from faraday_plugins.plugins.plugin import PluginXMLFormat

__author__ = "Francisco Amato"
__copyright__ = "Copyright (c) 2013, Infobyte LLC"
__credits__ = ["Facundo de Guzmán", "Francisco Amato"]
__license__ = ""
__version__ = "1.0.0"
__maintainer__ = "Francisco Amato"
__email__ = "[email protected]"
__status__ = "Development"


class NiktoXmlParser:
    """
    The objective of this class is to parse an xml file generated by the nikto tool.

    TODO: Handle errors.
    TODO: Test nikto output version. Handle what happens if the parser doesn't support it.
    TODO: Test cases.

    @param nikto_xml_filepath A proper xml generated by nikto
    """

    def __init__(self, xml_output):

        tree = self.parse_xml(xml_output)

        if tree:
            self.hosts = self.get_hosts(tree)
        else:
            self.hosts = []

    def parse_xml(self, xml_output):
        """
        Open and parse an xml file.

        TODO: Write custom parser to just read the nodes that we need instead of
        reading the whole file.

        @return xml_tree An xml tree instance. None if error.
        """
        try:
            tree = ET.fromstring(xml_output)
        except SyntaxError as err:
            print(f"SyntaxError: {err}. {xml_output}")
            return None

        return tree

    def get_hosts(self, tree):
        """
        @return items A list of Host instances
        """
        if tree.find('niktoscan'):
            for host_node in tree.find('niktoscan').findall('scandetails'):
                yield Host(host_node)
        else:
            for host_node in tree.findall('scandetails'):
                yield Host(host_node)


def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name):
    """
    Finds a subnode in the item node and the retrieves a value from it

    @return An attribute value
    """

    node = xml_node.find(subnode_xpath_expr)

    if node is not None:
        return node.get(attrib_name)

    return None


class Item:
    """
    An abstract representation of a Item

    TODO: Consider evaluating the attributes lazily
    TODO: Write what's expected to be present in the nodes
    TODO: Refactor both Host and the Port clases?

    @param item_node A item_node taken from an nikto xml tree
    """

    def __init__(self, item_node):

        self.node = item_node

        self.osvdbid = [
            "OSVDB-ID: " + self.node.get('osvdbid')] if self.node.get('osvdbid', "0") != "0" else []

        self.namelink = self.get_text_from_subnode('namelink')
        self.iplink = self.get_text_from_subnode('iplink')

        self.id_nikto = self.node.get('id')
        self.osvdblink = self.node.get('osvdbidlink')
        self.method = self.node.get('method')

        self.uri = self.get_uri()
        self.desc = self.get_desc()
        self.params = self.get_params(self.uri)
        self.CVE = self.get_cve(self.desc)

    def get_uri(self):

        try:

            uri = self.get_text_from_subnode('uri')
            h = HTMLParser.HTMLParser()
            return h.unescape(uri)

        except Exception as e:
            return uri

    def get_desc(self):

        desc = self.get_text_from_subnode('description')

        try:

            uri_present = desc.split(': ', 1)[0]
            h = HTMLParser.HTMLParser()
            if uri_present == h.unescape(self.uri):

                name = desc.split(': ', 1)[1]
                if name is not None and name != '':
                    return name

            return desc

        except Exception as e:
            return desc

    def get_params(self, uri):
        """Return the paramethers as a string"""
        try:
            params = uri.split('?')[1].replace('&', ',')
        except Exception as e:
            params = ''

        return params

    def get_text_from_subnode(self, subnode_xpath_expr):
        """
        Finds a subnode in the host node and the retrieves a value from it.

        @return An attribute value
        """
        sub_node = self.node.find(subnode_xpath_expr)
        if sub_node is not None:
            return sub_node.text

        return None

    def get_cve(self, desc):
        match = plugins_utils.CVE_regex.search(desc)
        if match:
            return [match.group()]
        return []


class Host:
    """
    An abstract representation of a Host

    @param host_node A host_node taken from an nmap xml tree
    """

    def __init__(self, host_node):

        self.node = host_node
        self.targetip = self.node.get('targetip')
        self.targethostname = self.node.get('targethostname')
        self.port = self.node.get('targetport')
        self.targetbanner = self.node.get('targetbanner')
        self.starttime = self.node.get('starttime')
        self.sitename = self.node.get('sitename')
        self.siteip = self.node.get('hostheader')
        self.items = self.get_items()

    def get_items(self):
        """
        @return items A list of Host instances
        """
        for item_node in self.node.findall('item'):
            yield Item(item_node)



class NiktoPlugin(PluginXMLFormat):
    """
    Example plugin to parse nikto output.
    """

    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
        self.identifier_tag = ["niktoscan", "niktoscans"]
        self.id = "Nikto"
        self.name = "Nikto XML Output Plugin"
        self.plugin_version = "0.0.2"
        self.version = "2.1.5"
        self.options = None
        self.parent = None
        self._use_temp_file = True
        self._temp_file_extension = "xml"
        self.xml_arg_re = re.compile(r"^.*(-output\s*[^\s]+).*$")
        self._command_regex = re.compile(
            r'^(sudo nikto|nikto|sudo nikto\.pl|nikto\.pl|perl nikto\.pl|\.\/nikto\.pl|\.\/nikto)\s+.*?')
        self._completition = {
            "": "",
            "-ask+": "Whether to ask about submitting updates",
            "-Cgidirs+": 'Scan these CGI dirs: "none", "all", or values like "/cgi/ /cgi-a/"',
            "-config+": "Use this config file",
            "-Display+": "Turn on/off display outputs:",
            "-dbcheck": "Check database and other key files for syntax errors",
            "-evasion+": "Encoding technique:",
            "-Format+": "Save file (-o) format:",
            "-Help": "Extended help information",
            "-host+": "Target host",
            "-IgnoreCode": "Ignore Codes--treat as negative responses",
            "-id+": "Host authentication to use, format is id:pass or id:pass:realm",
            "-key+": "Client certificate key file",
            "-list-plugins": "List all available plugins, perform no testing",
            "-maxtime+": "Maximum testing time per host",
            "-mutate+": "Guess additional file names:",
            "-mutate-options": "Provide information for mutates",
            "-nointeractive": "Disables interactive features",
            "-nolookup": "Disables DNS lookups",
            "-nossl": "Disables the use of SSL",
            "-no404": "Disables nikto attempting to guess a 404 page",
            "-output+": "Write output to this file ('.' for auto-name)",
            "-Pause+": "Pause between tests (seconds, integer or float)",
            "-Plugins+": "List of plugins to run (default: ALL)",
            "-port+": "Port to use (default 80)",
            "-RSAcert+": "Client certificate file",
            "-root+": "Prepend root value to all requests, format is /directory",
            "-Save": "Save positive responses to this directory ('.' for auto-name)",
            "-ssl": "Force ssl mode on port",
            "-Tuning+": "Scan tuning:",
            "-timeout+": "Timeout for requests (default 10 seconds)",
            "-Userdbs": "Load only user databases, not the standard databases",
            "-until": "Run until the specified time or duration",
            "-update": "Update databases and plugins from CIRT.net",
            "-useproxy": "Use the proxy defined in nikto.conf",
            "-Version": "Print plugin and database versions",
            "-vhost+": "Virtual host (for Host header)",
        }

    def parseOutputString(self, output):
        """
        This method will discard the output the shell sends, it will read it from
        the xml where it expects it to be present.

        NOTE: if 'debug' is true then it is being run from a test case and the
        output being sent is valid.
        """

        parser = NiktoXmlParser(output)

        for host in parser.hosts:

            h_id = self.createAndAddHost(
                host.targetip,
                hostnames=[host.targethostname]
            )

            s_id = self.createAndAddServiceToHost(
                h_id,
                "http",
                "tcp",
                ports=[host.port],
                status="open"
            )

            for item in host.items:
                self.createAndAddVulnWebToService(
                    h_id,
                    s_id,
                    name=item.desc,
                    ref=item.osvdbid,
                    method=item.method,
                    params=', '.join(item.params),
                    cve=item.CVE,
                    **plugins_utils.get_vulnweb_url_fields(item.namelink)
                )

        del parser

    def processCommandString(self, username, current_path, command_string):
        """
        Adds the -oX parameter to get xml output to the command string that the
        user has set.
        """
        super().processCommandString(username, current_path, command_string)

        arg_match = self.xml_arg_re.match(command_string)

        if arg_match is None:
            return re.sub(r"(^.*?nikto(\.pl)?)", r"\1 -output %s -Format XML" % self._output_file_path, command_string)
        else:
            data = re.sub(r" \-Format XML", "", command_string)
            return re.sub(arg_match.group(1), r"-output %s -Format XML" % self._output_file_path, data)


def createPlugin(*args, **kwargs):
    return NiktoPlugin(*args, **kwargs)