Skip to content

Commit

Permalink
Nessus parser for gowitness (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
randomactsofsecurity committed Feb 20, 2022
1 parent e79214e commit e1923bb
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 0 deletions.
268 changes: 268 additions & 0 deletions cmd/nessus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package cmd

import (
"fmt"
"net/url"
"encoding/xml"
"os"

"github.com/remeh/sizedwaitgroup"
"github.com/sensepost/gowitness/lib"
"github.com/spf13/cobra"
)

// nessusCmd represents the nessus command
var nessusCmd = &cobra.Command{
Use: "nessus",
Short: "Screenshot services from a Nessus XML file",
Long: `Screenshot services from a Nessus XML file.
Export the Nessus results as a .Nessus XML file from the console.
By default, this parser will search for the following match:
Plugin Name Contains: "Service Detection"
Then it will attempt to identify the web server by:
Plugin Service Name Contains: "www","https"
OR
Plugin Output Value Contains: "web server"
This parser needs a default plugin title to search for. Running this
command without specifying any --nessus-plugin-contains flags means it
will automatically attempt to find the 'Service Detection' plugin.
This default plugin appears to be the best plugin for web servers.
You can you can specify --port (multiple times) to only scan specific ports
flag which accepts multiple ports. If you scan by ports, you still need to
use the default --nessus-plugin-contains flag (or override it with your own value)
to identify a plugin to retrieve data out of.
Additionally, you can adjust the --nessus-plugin-output value to search the
plugin output for additional text to search through. The default value is
'web server'.
You can also adjust the --nessus-service value to include additional service
descriptors. The default values are 'www' and 'https', but perhaps using
'tcp' could be useful if nessus failed to identify a web server.
Optionally, you may choose to scan the FQDN hostnames with --scan-hostnames.
This will include both IP address and hostnames into the target list.`,
Example: `
$ gowitness nessus -file output.nessus
$ gowitness nessus -file output.nessus --scan-hostnames
# These options filter services from the nessus file
$ gowitness nessus -file output.nessus --nessus-plugin-output server
$ gowitness nessus -file output.nessus --nessus-service www --nessus-service tcp --nessus-service https
$ gowitness nessus -file output.nessus --no-http
$ gowitness nessus -file output.nessus --no-http --port 8888
$ gowitness nessus -file output.nessus --no-https
$ gowitness nessus -file output.nessus --port 80 --port 8080`,
Run: func(cmd *cobra.Command, args []string) {
log := options.Logger

// prepare targets
targets, err := getNessusURLs()
if err != nil {
log.Fatal().Err(err).Msg("could not process nessus .nessus xml file")
}
log.Debug().Int("targets", len(targets)).Msg("number of targets")

// screeny path
if err = options.PrepareScreenshotPath(); err != nil {
log.Fatal().Err(err).Msg("failed to prepare the screenshot path")
}

// prepare db
db, err := db.Get()
if err != nil {
log.Fatal().Err(err).Msg("failed to get a db handle")
}

// prepare swg
log.Debug().Int("threads", options.Threads).Msg("thread count to use with goroutines")
swg := sizedwaitgroup.New(options.Threads)

// process!
for _, target := range targets {
u, err := url.Parse(target)
if err != nil {
log.Warn().Str("url", u.String()).Msg("skipping invalid url")
continue
}

swg.Add()

log.Debug().Str("url", u.String()).Msg("queueing goroutine for url")
go func(url *url.URL) {
defer swg.Done()

p := &lib.Processor{
Logger: log,
Db: db,
Chrome: chrm,
URL: url,
ScreenshotPath: options.ScreenshotPath,
}

if err := p.Gowitness(); err != nil {
log.Debug().Err(err).Str("url", url.String()).Msg("failed to witness url")
}
}(u)
}

swg.Wait()
log.Info().Msg("processing complete")
},
}



func init() {
rootCmd.AddCommand(nessusCmd)

nessusCmd.Flags().StringVarP(&options.File, "file", "f", "", "Nessus .nessus XML file")
nessusCmd.Flags().StringSliceVar(&options.NessusServiceNames, "nessus-service", []string{"www","https"}, "service name contains filter. supports multiple --service flags")
nessusCmd.Flags().StringSliceVar(&options.NessusPluginOutput, "nessus-plugin-output", []string{"web server"}, "nessus plugin output contains filter. supports multiple --pluginoutput flags")
nessusCmd.Flags().StringSliceVar(&options.NessusPluginContains, "nessus-plugin-contains", []string{"Service Detection"}, "nessus plugin name contains filer. supports multiple --plugin-contains flags")
nessusCmd.Flags().IntSliceVar(&options.NessusPorts, "port", []int{}, "ports filter. supports multiple --port flags")
nessusCmd.Flags().BoolVarP(&options.NmapScanHostanmes, "scan-hostnames", "N", false, "scan hostnames (useful for virtual hosting)")
nessusCmd.Flags().BoolVarP(&options.NoHTTP, "no-http", "s", false, "do not try using http://")
nessusCmd.Flags().BoolVarP(&options.NoHTTPS, "no-https", "S", false, "do not try using https://")
nessusCmd.Flags().IntVarP(&options.Threads, "threads", "t", 4, "threads used to run")

cobra.MarkFlagRequired(nessusCmd.Flags(), "file")
}

// structure for XML parsing


type reportHost struct {
HostName string `xml:"name,attr"`
ReportItems []reportItem `xml:"ReportItem"`
Tags []tag `xml:"HostProperties>tag"`
}

type tag struct {
Key string `xml:"name,attr"`
Value string `xml:",chardata"`
}

type reportItem struct {
PluginName string `xml:"pluginName,attr"`
ServiceName string `xml:"svc_name,attr"`
Port int `xml:"port,attr"`
PluginOutput string `xml:"plugin_output"`
}


// getNessusURLs generates url's from a nessus .nessus xml file based on options
// this function considers many of the flag combinations
func getNessusURLs() (urls []string, err error) {
log := options.Logger
// using os.open due to large files
nessusFile, err := os.Open(options.File)
if err != nil {
return
}

defer nessusFile.Close()

decoder := xml.NewDecoder(nessusFile)

// Unique maps to cut down on dupliation within nessus files
var nessusIPsMap = make(map[string]int)
var nessusHostsMap = make(map[string]int)

for {

token, err := decoder.Token()

if err != nil {
break
}

if token == nil {
break
}

switch element := token.(type) {
case xml.StartElement:
tagName := element.Name.Local

// Read the ReportHosts from the XML
if tagName == "ReportHost" {

var host reportHost
decoder.DecodeElement(&host, &element)

// This could be replaced with a map for a quicker retrieval in the future
// pulling from the tags is a bit annoying
var fqdn, ip string
for _, v := range host.Tags{
if(v.Key == "host-fqdn"){
fqdn = v.Value
}
if(v.Key == "host-ip"){
ip = v.Value
}
}

// iterate across the ReportItems XML
for _, item := range host.ReportItems {
log.Debug().Str("IP,Port", ip + " | " + fqdn).Msg("ReportItem: ")
log.Debug().Str("Service,PluginName", item.PluginName + " | " + item.ServiceName).Msg("Details: ")

// skip port if the port does not match the provided ports to filter
if len(options.NessusPorts) > 0 && !lib.SliceContainsInt(options.NessusPorts, item.Port) {
continue
}
// check the plugin name contains a given string. Contains should work, though startsWith may be useful.
// A valid plugin name must be given here, otherwise we'll be iterating across too many pointless plugins
if !lib.SliceContainsString(options.NessusPluginContains, item.PluginName) {
continue
}
// identify that the service is a web server
if lib.SliceContainsString(options.NessusServiceNames, item.ServiceName) || lib.SliceContainsString(options.NessusPluginOutput, item.PluginOutput) {
// add the hostnames if the option has been set
if options.NmapScanHostanmes {
if fqdn != ""{
nessusHostsMap[fqdn] = item.Port
}
}
// checking for empty ip. It should always be set, but you never know
if ip != "" {
nessusIPsMap[ip] = item.Port
}
} else {
continue
}
}
}
}
}

// Build the URL list for unique IPs and Hostnames
for k, v := range nessusIPsMap {
urls = append(urls, buildURL(k,v)...)
}

for k, v := range nessusHostsMap {
urls = append(urls, buildURL(k,v)...)
}

return
}

// buildURI will build urls taking the http/https options int account
func buildURL(hostname string, port int) (r []string) {

if !options.NoHTTP {
r = append(r, fmt.Sprintf(`http://%s:%d`, hostname, port))
}

if !options.NoHTTPS {
r = append(r, fmt.Sprintf(`https://%s:%d`, hostname, port))
}

return r
}
6 changes: 6 additions & 0 deletions lib/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ type Options struct {
// Single
ScreenshotFileName string

// Nessus
NessusPluginContains []string
NessusServiceNames []string
NessusPluginOutput []string
NessusPorts []int

// Nmap
NmapFile string
NmapService []string
Expand Down

0 comments on commit e1923bb

Please sign in to comment.