Cover V11, I10

Article
Figure 1

oct2002.tar

NSRXREF -- A Python Script for NSR Files

Paul Trout

I've been using Nessus as a network and host-auditing tool for the past six months. As the person running the audits, I'm more than happy with the tool. When I change hats and become the person who has to fix all the holes, I have a small problem -- the reports that Nessus produces fall into two categories. There is a summary-type report that shows the hosts with the most holes, or a detailed list, by host, of the vulnerabilities found for that host. To build a security remediation plan, I've found a cross-reference chart, with the hosts across the top, and the vulnerabilities down the left side to be the best starting point. Because Nessus does not provide such a report, and because I was unable to find one on the Web, I decided to write my own.

NSRXREF is a Python script that reads Nessus results files (NSR files), and produces the aforementioned cross-reference chart as a set of HTML tables (see Figure 1). This article describes the NSRXREF script and how to use it. All code for this article is available for download at:

http://www.sysadminmag.com/code/
If you are not familiar with Nessus and want to begin with an introduction to Nessus as an auditing tool, see "Using Freeware Vulnerability Scanners" by Gary Bahadur and Yen-ming Chen (Sys Admin, April 2001).

NSR Files

When a Nessus scan is complete, the default option for saving the results is a Nessus Scan Results (NSR) file. This file is a pipe-delimited (|) file with one record per line. Each record has either two or five fields, depending whether a specific vulnerability was found on a particular port/service for a specific host. Briefly, here are the fields:

1. IP address of the scanned host

2. Service Name -- TCP/UDP Port or application protocol

3. Nessus Plugin ID

4. One of: NOTE, INFO, REPORT. REPORT indicates that a vulnerability was found (security hole). INFO indicates a potential vulnerability was found (security warning).

5. The Nessus plug-in description. Semi-colons in the description are used as line-break indicators in the Nessus output.

Having only two fields on the line means Nessus discovered some process or application was listening on the port, but found no vulnerabilities. Although a port that is open for receiving information is a potential gateway into the host, I kept the focus of NSRXREF on the actual vulnerabilities found on the various ports. For that reason, the ports that are identified simply as open are ignored during processing. The summary item "Ignored" tabulates the number of these lines.

NOTE records indicate some piece of information that was retrieved by Nessus, which, in and of itself, does not constitute a security risk. Typical NOTE items are the version of the Web server running on the host, DCE service IDs, and the operating system name, version, and patch level. NSRXREF ignores NOTE records. It simply counts them so the summary will account for all of the lines in the file. The description field presents a few problems for automatic processing. Some of the attack scripts add a list of items, such as daemons or services, to the vulnerability description. Some separate the generic information about the vulnerability from the host-specific information with two consecutive line breaks (represented as semi-colons in the actual description strings).

I experimented with a table of Nessus Plugin IDs and specific search-and-replace rules and decided that setup was too cumbersome to maintain. Since I was looking for the most verbose and non-host specific description without doing a custom extraction for each rule, I wound up searching for ";;" from the left end of the description, and ":" from the right end. I used whichever was the closest to the left side of the description as the end of my extracted description. I've had very good results with this method. I still get some descriptions containing host-specific information. The saving grace is that, in these cases, I also get enough of the generic description to apply it correctly to all affected hosts.

How the Script Works

I will not attempt to introduce Python beyond this brief description of its built-in list and dictionary data types. A list is just that -- a list of items. Python's list is similar to arrays in other languages. While the list has several methods, the two applicable to NSRXREF are "append" and "sort". Append simply appends an item to an existing list. Sort reorders the items, in the list, in ascending order. While a comparison function can be passed, I used the default function, resulting in a straight ASCII sort. Brackets ([]) are used to declare lists. Here is a brief summary of the list type, as it applies to NSRXREF:

newlist=[] -- Declare a new, empty list named newlist

newlist.append('One') -- Append a string item to the list

newlist.append('Four') -- Append another string item to the list. At this point, newlist has the items "One" and "Four" stored like this: ['One', 'Four']

newlist.sort() -- Sort list, in place, in ascending ASCII order. Now newlist has the same items, but they are in this order: ['Four', 'One']

The dictionary is an associative array. It's made up of key value pairs. Entries are set with dict[key]=value, and retrieved with a statement like: x=dict[key]. Like the list type, dictionary types have a several built-in methods. The two most useful, in this context, are has_key, and keys. The has_key method returns 1 if the specified key is already in the dictionary. The keys method returns a list of keys in the dictionary. The keys method is especially useful when you take the list of keys, sort it, and use it as a rudimentary index for stepping through the dictionary in a specific order. Braces ({}) are used to declare dictionaries. As it applies to NSRXREF, here is a summary of the dictionary type:

newdict={} -- Create a new, empty, dictionary

newdict['PATH']='/home/ptrout' -- Add a new key, PATH with a value of /home/ptrout

newdict['FILE']='NSRXREF.PY' -- Add another key, FILE with a value of NSRXREF.PY

print newdict['FILE'] -- This displays /home/ptrout since that is the value of the key, PATH.

Because PATH is a key, in newdict, the following expression returns 1:

newdict.has_key('PATH')

The following returns 0, because AUTHOR is not a key:

newdict.has_key('AUTHOR')

newdict.keys() returns the list ['PATH','FILE'].

Because NSRXREF expects the NSR filename to be passed (on the command line) as the first argument, the first thing it does is check whether an argument was passed. If there is no argument, then it displays a usage message and exits. If an argument is present, it assumes it's a filename and attempts to open the file in read mode. If the open fails, an error message (different from the usage message) is displayed, and execution terminates. Obviously, if there are no errors, it's time for processing.

The file-processing loop is the main body of NSRXREF. Each line is processed, and, at the end, there are three populated dictionaries:

vulninfo (vulnerability information) -- Keyed by the Nessus Plugin ID number, this stores the vulnerability descriptions. In the cross-reference chart, this dictionary provides the vulnerability information down the left-hand side.

host_addr (host addresses) -- Keyed by the IP address of each host in the NSR file. This stores a 1 for each entry. In truth, this could be a straight list, but I like using the has_key method of a dictionary, rather than the index method of a list for testing to see whether, in this case, an IP address has already been added. Has_key returns 1 if it's present, and 0 if it's not. The index method (list data type) raises an exception to indicate the value could not be found. In the chart, this provides the IP addresses across the top of the chart.

vulnbyhost (vulnerability by host) -- Keyed by the concatenation of the host IP address, and the Nessus Plugin ID (e.g., if 172.22.16.2 is vulnerable to whatever is tested by Plugin 94567, the key for the dictionary is 172.22.16.294567). As with the host_addr dictionary, I simply store a 1 for each entry. This dictionary is used to determine whether a particular combination of host and vulnerability is present, and what should be displayed in the appropriate cell in the chart.

In addition to these dictionaries, there are five counters I use for the summary table at the end of the report:

Linecounter -- A straight count of the number of lines in the NSR file.

Twoflds -- The number of records (lines) in the NSR file describing open ports, but no found vulnerability. These records are ignored during processing.

Notecounter -- The number of NOTEs ignored during the processing.

Warncounter -- The number of INFO records processed.

Holecounter -- The number of REPORT records processed.

If you add twoflds, notecounter, warncounter, and holecounter together, you should get linecounter.

Briefly, here is the process for populating the dictionaries, and setting the counters:

1. Read a line.

2. Count the number of fields -- if 2, increment twoflds, and continue processing on the next line.

3. If there are 5 fields, check the type of record -- if NOTE, increment notecounter, and continue processing on the next line.

4. Increment either warncounter or holecounter, extract the description and add an entry to vulninfo (if the vulnerability is not already present).

5. If the IP address is not already in host_addr, add it.

6. Concatenate the IP address, and the Plugin ID, and add an entry in vulnbyhost.

7. Get the next line.

Once the last line has been processed, and the NSR file closed, we can do some housekeeping chores. Nessus Plugin IDs always appear to be a string of five digits. Since they are all the same length, an ASCII sort works very nicely to put them in order. A list called vulnid holds the sorted list of keys from the vulninfo dictionary. Getting the IP addresses sorted properly is a little more involved. For this I created a function, sortip.

The sortip function accepts a list of IP addresses, and returns a list of those same IP addresses sorted in correct order. Briefly, sortip does this:

1. Each address in the source list is split into its respective four octets.

2. Each octet is left-padded with 0s to three characters, and then these padded octets are combined back into an address. A dictionary entry is created with the padded address as the key, and the original, unpadded address as the value.

3. When all of the source addresses have been added to the dictionary, the keys method returns a list of just the padded addresses.

4. Sort this list with the list type's sort method, and use it as an index to step through the dictionary in the correct order.

5. As each address is extracted, it's appended to the list that sortip will return.

6. Return the list of IP addresses, in their original format (no left-padded octets), but sorted in the correct order.

Once the NSR file has been read, and all of the scanned host's IP addresses have been sorted, it's time to create the report. I chose to emit HTML because it's relatively platform-independent, and easy to create. After some trial and error, I empirically chose to display five hosts at a time, which seemed to give the most usable tables on a variety of screen resolutions and printed well, too.

The HTML output is split into two parts -- the cross-reference table(s), and a summary. The summary is primarily a quick sanity check of the results. Remember, Ignored+NOTES+WARNINGS+HOLES should equal the number of lines in the NSR file. Whenever you're working with a security audit, sanity checks can prevent you from having to explain to your boss or client why you missed the gaping vulnerability in XYZ server that just shut the company down.

To generate the cross-reference tables, I do the following:

  • Pull the next five host IP addresses into a list.

  • Emit the table header, and the first row of cells for each vulnerability that was found; print the Nessus plugin ID, and the plugin's description text.

  • Concatenate the host IP and the plugin ID, and look up that key in the dictionary of vulnerabilities by host. If it's found, the cell gets a red background with the word FOUND displayed. If it's not found, the background of the cell is green, and there is no text. This process is repeated for each group of host addresses until all of the cross-reference tables have been generated.

Using NSRXREF

NSRXREF accepts the name of a Nessus NSR file as its command-line argument. It emits HTML to STDOUT, where it can be captured by redirection. I typically run it with a command line similar to this:

Python nsrxref.py network.nsr > netxref.html
Of course, you can execute it directly by making it executable, and adding #! /path_to_python/python to the top of the file. Then, you could invoke it with:

Nsrxref.py network.nsr > netxref.html
As with any security auditing tool, Nessus returns a certain number of false positives, depending on the environment and which plugins were enabled during the scan. An example is the SSH-version plugin that mistakenly reports that OpenSSH 3.2.2 is a lesser version that 3.0.2. While NSRXREF does nothing to eliminate these, it does allow them to be visually grouped together and therefore easier to ignore, or explain, while creating the remediation plan or conducting the post-remediation debriefing.

I've tested NSRXREF with Python 1.5.2, 2.0, 2.1, and 2.1.2 on Windows 2000, and OpenBSD 2.9, 3.0, and Red Hat Linux 7.1. As far as I know, there is nothing that should prevent it from running on any version of Python subsequent to 1.5.2 on any operating system.

Conclusion

I've found the reports built into Nessus to be very good for showing a user, manager, or senior executive where there are holes in their networked systems. However, those same reports required a lot of manual work to turn them into even a rough remediation plan. NSRXREF provides the starting point in the remediation process by allowing administrators to quickly discover which groups of machines have the same holes.

Using Nessus has helped me to be a more thorough security auditor. I wrote NSRXREF to help me control the post-audit remediation process. Since then, I've been able to spend more time fixing holes, and less time working up the remediation plan. I hope this tool is also useful to you.

Paul Trout is a systems engineer for an Internet advertising company. A systems generalist, and relative *nix rookie, he's been sucked into the vortex of systems and network security. He can be reached at: ptrout@usa.net.