Skip to content

Commit 43ac2c3

Browse files
author
Brent Cook
committed
Land rapid7#8291, Acunetix XML import improvements
2 parents c8984d8 + c4f1130 commit 43ac2c3

File tree

1 file changed

+197
-22
lines changed

1 file changed

+197
-22
lines changed

lib/rex/parser/acunetix_nokogiri.rb

Lines changed: 197 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module Parser
2424
def start_document
2525
@parse_warnings = []
2626
@resolv_cache = {}
27+
@host_object = nil
2728
end
2829

2930
def start_element(name=nil,attrs=[])
@@ -32,9 +33,12 @@ def start_element(name=nil,attrs=[])
3233
@state[:current_tag][name] = true
3334
case name
3435
when "Scan" # Start of the thing.
35-
when "Name", "StartURL", "Banner", "Os"
36+
@state[:report_item] = {}
37+
when "Name", "StartURL", "StartTime", "Banner", "Os", "Text", "Severity", "CWE", "URL", "Parameter"
3638
@state[:has_text] = true
3739
when "LoginSequence" # Skipping for now
40+
when "ReportItem"
41+
@state[:report_item] = {}
3842
when "Crawler"
3943
record_crawler(attrs)
4044
when "FullURL"
@@ -62,14 +66,56 @@ def end_element(name=nil)
6266
# StartURL does not always include the scheme
6367
@text.prepend("http://") unless URI.parse(@text).scheme
6468
collect_host
65-
collect_service
69+
collect_service_from_url
6670
@text = nil
6771
handle_parse_warnings &block
68-
host_object = report_host &block
69-
if host_object
70-
report_starturl_service(host_object,&block)
71-
db.report_import_note(@args[:wspace],host_object)
72+
@host_object = report_host &block
73+
if @host_object
74+
report_starturl_service(&block)
75+
db.report_import_note(@args[:wspace],@host_object)
7276
end
77+
when "StartTime"
78+
@state[:has_text] = false
79+
@state[:timestamp] = @text.to_s.tr!(',','').tr!('/','-')
80+
@text = nil
81+
when "Text"
82+
@state[:has_text] = false
83+
service = collect_service_from_kbitem_text
84+
@text = nil
85+
return unless service
86+
handle_parse_warnings &block
87+
if @host_object
88+
report_kbitem_service(service,&block)
89+
end
90+
when "Severity"
91+
@state[:has_text] = false
92+
collect_report_item_severity
93+
@text = nil
94+
when "CWE"
95+
@state[:has_text] = false
96+
collect_report_item_cwe
97+
@text = nil
98+
when "URL"
99+
@state[:has_text] = false
100+
collect_report_item_reference_url
101+
@text = nil
102+
when "Parameter"
103+
@state[:has_text] = false
104+
collect_report_item_parameter
105+
@text = nil
106+
when "ReportItem"
107+
vuln = collect_vuln_from_report_item
108+
if vuln.nil?
109+
@state[:page_request] = @state[:page_response] = nil
110+
return
111+
end
112+
handle_parse_warnings &block
113+
if @state[:vuln_info][:refs].nil?
114+
report_web_vuln(&block)
115+
else
116+
report_other_vuln(&block)
117+
end
118+
@state[:page_request] = @state[:page_response] = nil
73119
when "Banner"
74120
@state[:has_text] = false
75121
collect_and_report_banner
@@ -134,7 +180,7 @@ def collect_host
134180
@report_data[:state] = Msf::HostState::Alive
135181
end
136182

137-
def collect_service
183+
def collect_service_from_url
138184
return unless @report_data[:host]
139185
return unless in_tag("Scan")
140186
return unless @text
@@ -146,6 +192,44 @@ def collect_service
146192
@report_data[:ports] << @state[:starturl_port]
147193
end
148194

195+
def collect_service_from_kbitem_text
196+
return unless @host_object
197+
return unless in_tag("Scan")
198+
return unless in_tag("KBase")
199+
return unless in_tag("KBItem")
200+
return unless @text
201+
return if @text.strip.empty?
202+
return unless @text =~ /server is running/
203+
matched = / (?<name>\w+) server is running on (?<proto>\w+) port (?<portnum>\d+)\./.match(@text)
204+
@report_data[:ports] ||= []
205+
@report_data[:ports] << matched[:portnum]
206+
return matched
207+
end
208+
209+
def collect_vuln_from_report_item
210+
@state[:vuln_info] = nil
211+
return unless @host_object
212+
return unless in_tag("Scan")
213+
return unless in_tag("ReportItems")
214+
return unless in_tag("ReportItem")
215+
return unless @state[:report_item][:name]
216+
return unless @state[:report_item][:severity]
217+
return unless @state[:report_item][:severity].downcase == "high"
218+
219+
@state[:vuln_info] = {}
220+
@state[:vuln_info][:name] = @state[:report_item][:name]
221+
if @state[:page_request_verb].nil? && @state[:report_item][:name] =~ /deprecated/
222+
# Treating this as a regular vuln, not web-specific
223+
@state[:vuln_info][:refs] = ["ACX-#{@state[:report_item][:reference_url]}"]
224+
unless @state[:report_item_cwe].nil?
225+
@state[:vuln_info][:refs][0] << ",#{@state[:report_item][:cwe]}"
226+
end
227+
end
228+
@state[:vuln_info][:severity] = @state[:report_item][:severity].downcase
229+
@state[:vuln_info][:cwe] = @state[:report_item][:cwe]
230+
return @state[:vuln_info]
231+
end
232+
149233
def collect_and_report_banner
150234
return unless (svc = @state[:starturl_service_object]) # Yes i want assignment
151235
return unless @text
@@ -165,7 +249,37 @@ def collect_report_item_name
165249
return unless in_tag("ReportItem")
166250
return unless @text
167251
return if @text.strip.empty?
168-
@state[:report_item] = @text
252+
@state[:report_item][:name] = @text
253+
end
254+
255+
def collect_report_item_severity
256+
return unless in_tag("ReportItem")
257+
return unless @text
258+
return if @text.strip.empty?
259+
@state[:report_item][:severity] = @text
260+
end
261+
262+
def collect_report_item_cwe
263+
return unless in_tag("ReportItem")
264+
return unless @text
265+
return if @text.strip.empty?
266+
@state[:report_item][:cwe] = @text
267+
end
268+
269+
def collect_report_item_reference_url
270+
return unless in_tag("ReportItem")
271+
return unless in_tag("References")
272+
return unless in_tag("Reference")
273+
return unless @text
274+
return if @text.strip.empty?
275+
@state[:report_item][:reference_url] = @text
276+
end
277+
278+
def collect_report_item_parameter
279+
return unless in_tag("ReportItem")
280+
return unless @text
281+
return if @text.strip.empty?
282+
@state[:report_item][:parameter] = @text
169283
end
170284

171285
# @state[:fullurl] is set by report_web_site
@@ -211,20 +325,26 @@ def report_web_form(&block)
211325
def report_web_page(&block)
212326
return if should_skip_this_page
213327
return unless @state[:web_site]
328+
@state[:page_request_verb] = nil
214329
return unless @state[:page_request]
215330
return if @state[:page_request].strip.empty?
216-
return unless @state[:page_response]
217-
return if @state[:page_response].strip.empty?
218-
path,query_string = parse_request(@state[:page_request])
331+
verb,path,query_string = parse_request(@state[:page_request])
219332
return unless path
220-
parsed_response = parse_response(@state[:page_response])
221-
return unless parsed_response
333+
@state[:page_request_verb] = verb
222334
web_page_info = {}
335+
if @state[:page_response].strip.blank?
336+
web_page_info[:code] = ""
337+
web_page_info[:headers] = {}
338+
web_page_info[:body] = ""
339+
else
340+
parsed_response = parse_response(@state[:page_response])
341+
return unless parsed_response
342+
web_page_info[:code] = parsed_response[:code].to_i
343+
web_page_info[:headers] = parsed_response[:headers]
344+
web_page_info[:body] = parsed_response[:body]
345+
end
223346
web_page_info[:web_site] = @state[:web_site]
224347
web_page_info[:path] = path
225-
web_page_info[:code] = parsed_response[:code].to_i
226-
web_page_info[:headers] = parsed_response[:headers]
227-
web_page_info[:body] = parsed_response[:body]
228348
web_page_info[:query] = query_string || ""
229349
url = ""
230350
url << @state[:web_site].service.name.to_s << "://"
@@ -234,13 +354,51 @@ def report_web_page(&block)
234354
return unless uri # Sanity checker
235355
db.emit(:web_page, url, &block) if block
236356
web_page_object = db_report(:web_page,web_page_info)
237-
@state[:page_request] = @state[:page_response] = nil
238357
@state[:web_page] = web_page_object
239358
end
240359

360+
def report_web_vuln(&block)
361+
return if should_skip_this_page
362+
return unless @state[:web_page]
363+
return unless @state[:web_site]
364+
return unless @state[:vuln_info]
365+
366+
web_vuln_info = {}
367+
web_vuln_info[:web_site] = @state[:web_site]
368+
web_vuln_info[:path] = @state[:web_page][:path]
369+
web_vuln_info[:query] = @state[:web_page][:query]
370+
web_vuln_info[:method] = @state[:page_request_verb]
371+
web_vuln_info[:pname] = ""
372+
if @state[:page_response].blank?
373+
web_vuln_info[:proof] = "<empty response>"
374+
else
375+
web_vuln_info[:proof] = @state[:page_response]
376+
end
377+
web_vuln_info[:risk] = 5
378+
web_vuln_info[:params] = []
379+
unless @state[:report_item][:parameter].blank?
380+
# Acunetix only lists a single paramter...
381+
web_vuln_info[:params] << [ @state[:report_item][:parameter].to_s, "" ]
382+
end
383+
web_vuln_info[:category] = "imported"
384+
web_vuln_info[:confidence] = 100
385+
web_vuln_info[:name] = @state[:vuln_info][:name]
386+
387+
db.emit(:web_vuln, web_vuln_info[:name], &block) if block
388+
vuln = db_report(:web_vuln, web_vuln_info)
389+
end
390+
391+
def report_other_vuln(&block)
392+
return if should_skip_this_page
393+
return unless @state[:vuln_info]
394+
395+
db.emit(:vuln, @state[:vuln_info][:name], &block) if block
396+
db_report(:vuln, @state[:vuln_info].merge(:host => @host_object))
397+
end
398+
241399
# Reasons why we shouldn't collect a particular web page.
242400
def should_skip_this_page
243-
if @state[:report_item] =~ /Unrestricted File Upload/
401+
if @state[:report_item][:name] =~ /Unrestricted File Upload/
244402
# This means that the page being collected is something the
245403
# auditor put there, so it's not useful to report on.
246404
return true
@@ -259,6 +417,7 @@ def parse_request(request)
259417
return unless verb
260418
return unless req
261419
path,query_string = req.split(/\?/)[0,2]
420+
return verb,path,query_string
262421
end
263422

264423
def parse_response(response)
@@ -302,14 +461,14 @@ def report_host(&block)
302461

303462
# The service is super important, so we hang on to it for the
304463
# rest of the scan.
305-
def report_starturl_service(host_object,&block)
306-
return unless host_object
464+
def report_starturl_service(&block)
465+
return unless @host_object
307466
return unless @state[:starturl_uri]
308467
name = @state[:starturl_uri].scheme
309468
port = @state[:starturl_uri].port
310-
addr = host_object.address
469+
addr = @host_object.address
311470
svc = {
312-
:host => host_object,
471+
:host => @host_object,
313472
:port => port,
314473
:name => name.dup,
315474
:proto => "tcp"
@@ -320,6 +479,22 @@ def report_starturl_service(host_object,&block)
320479
end
321480
end
322481

482+
def report_kbitem_service(service,&block)
483+
return unless @host_object
484+
return unless @state[:starturl_uri]
485+
addr = @host_object.address
486+
svc = {
487+
:host => @host_object,
488+
:port => service[:portnum].to_i,
489+
:name => service[:name].dup.downcase,
490+
:proto => service[:proto].dup.downcase
491+
}
492+
if service[:name] and service[:portnum]
493+
db.emit(:service,[addr,service[:portnum]].join(":"),&block) if block
494+
db_report(:service,svc)
495+
end
496+
end
497+
323498
def report_web_site(url,&block)
324499
return unless in_tag("Crawler")
325500
return unless url

0 commit comments

Comments
 (0)