-
Notifications
You must be signed in to change notification settings - Fork 324
/
nessus.go
275 lines (220 loc) · 8.69 KB
/
nessus.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
package cmd
import (
"encoding/xml"
"fmt"
"net/url"
"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.
To start, 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.
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
from.
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 unique targets")
// screeny path
if err = options.PrepareScreenshotPath(); err != nil {
log.Fatal().Err(err).Msg("failed to prepare the screenshot path")
}
// parse headers
chrm.PrepareHeaderMap()
// 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.NmapScanHostnames, "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
}
log.Debug().Str("file", options.File).Msg("reading file")
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 {
// leaving this debugging here in case the parser needs to be debugged.
// dont think this is useful in normal cases
// 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.NmapScanHostnames {
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
}
log.Debug().Str("target", ip).Str("service", item.ServiceName).Int("port", item.Port).
Msg("adding target")
}
}
}
}
}
// 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
}