dradis-burp 3.12.0 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,3 +6,5 @@ require 'dradis/plugins/burp'
6
6
 
7
7
  # Load supporting Burp classes
8
8
  require 'burp/issue'
9
+ require 'burp/html/issue'
10
+ require 'burp/xml/issue'
@@ -7,5 +7,6 @@ end
7
7
 
8
8
  require 'dradis/plugins/burp/engine'
9
9
  require 'dradis/plugins/burp/field_processor'
10
- require 'dradis/plugins/burp/importer'
10
+ require 'dradis/plugins/burp/html/importer'
11
11
  require 'dradis/plugins/burp/version'
12
+ require 'dradis/plugins/burp/xml/importer'
@@ -5,10 +5,21 @@ module Dradis
5
5
  isolate_namespace Dradis::Plugins::Burp
6
6
 
7
7
  include ::Dradis::Plugins::Base
8
- description 'Processes Burp Scanner XML output'
8
+ description 'Processes Burp Scanner output'
9
9
  provides :upload
10
+
11
+ # Because this plugin provides two export modules, we have to overwrite
12
+ # the default .uploaders() method.
13
+ #
14
+ # See:
15
+ # Dradis::Plugins::Upload::Base in dradis-plugins
16
+ def self.uploaders
17
+ [
18
+ Dradis::Plugins::Burp::Html,
19
+ Dradis::Plugins::Burp::Xml
20
+ ]
21
+ end
10
22
  end
11
23
  end
12
24
  end
13
25
  end
14
-
@@ -4,7 +4,12 @@ module Dradis
4
4
  class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
5
5
 
6
6
  def post_initialize(args={})
7
- @burp_object = ::Burp::Issue.new(data)
7
+ @burp_object =
8
+ if data.is_a?(Nokogiri::XML::Element)
9
+ ::Burp::Xml::Issue.new(data)
10
+ elsif data.is_a?(Nokogiri::XML::NodeSet)
11
+ ::Burp::Html::Issue.new(data)
12
+ end
8
13
  end
9
14
 
10
15
  def value(args={})
@@ -20,4 +25,3 @@ module Dradis
20
25
  end
21
26
  end
22
27
  end
23
-
@@ -8,7 +8,7 @@ module Dradis
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 3
11
- MINOR = 12
11
+ MINOR = 13
12
12
  TINY = 0
13
13
  PRE = nil
14
14
 
@@ -0,0 +1,144 @@
1
+ module Dradis::Plugins::Burp
2
+ # This module knows how to parse Burp HTML format.
3
+ module Html
4
+ def self.meta
5
+ package = Dradis::Plugins::Burp
6
+ {
7
+ name: package::Engine::plugin_name,
8
+ description: 'Upload Burp Scanner output file (.html)',
9
+ version: package.version
10
+ }
11
+ end
12
+
13
+ class Importer < Dradis::Plugins::Upload::Importer
14
+ def initialize(args={})
15
+ args[:plugin] = Dradis::Plugins::Burp
16
+ super(args)
17
+ end
18
+
19
+ def import(params = {})
20
+ logger.info { 'Parsing Burp Scanner HTML output file...' }
21
+ @doc = Nokogiri::HTML(File.read(params[:file]))
22
+ logger.info { 'Done.' }
23
+
24
+ # Issue headers are like: <span class="BODH0" id="X">
25
+ issue_headers = @doc.xpath("//span[contains(@class, 'BODH0')]")
26
+
27
+ if issue_headers.count.zero?
28
+ error = "Document doesn't seem to be in the Burp Scanner HTML format."
29
+ logger.fatal { error }
30
+ content_service.create_note text: error
31
+ return false
32
+ end
33
+
34
+ issue_headers.each do |header|
35
+ issue_id = header.attr('id')
36
+ html = extract_html_fragment_for(issue_id)
37
+ process_html_issue(html)
38
+ end
39
+
40
+ logger.info { 'Burp Scanner results successfully imported' }
41
+ true
42
+ end
43
+
44
+ def process_html_issue(html_issue)
45
+ header = html_issue.first
46
+ title = header.text.gsub(/^\d+\.\S/, '')
47
+ burp_id =
48
+ if (link = header.css('a').first)
49
+ link.attr('href')[/\/([0-9a-f]+)_.*/, 1].to_i(16)
50
+ else
51
+ title
52
+ end
53
+ issue_id = html_issue.attr('id').value
54
+ issue_text =
55
+ template_service.process_template(
56
+ template: 'issue',
57
+ data: html_issue
58
+ )
59
+
60
+ logger.info { "Processing issue #{issue_id}: #{title}" }
61
+ issue = content_service.create_issue(text: issue_text, id: burp_id)
62
+
63
+ # Evidence headers are like:
64
+ # <span class="BODH1" id="X.Y">
65
+ # where:
66
+ # X is the issue index
67
+ # Y is the evidence index
68
+ evidence_headers = html_issue.xpath(
69
+ "//span[contains(@class, 'BODH1') and starts-with(@id, '#{issue_id}.')]"
70
+ )
71
+
72
+ # If there are no evidence headers inside this issue, this is a
73
+ # "single evidence" case: our evidence html is the issue html itself
74
+ if evidence_headers.count.zero?
75
+ process_html_evidence(html_issue, issue)
76
+ else
77
+ evidence_headers.each do |header|
78
+ evidence_id = header.attr('id')
79
+ html = extract_html_fragment_for(evidence_id)
80
+ process_html_evidence(html, issue)
81
+ end
82
+ end
83
+ end
84
+
85
+ def process_html_evidence(html_evidence, issue)
86
+ evidence_id = html_evidence.attr('id').value
87
+ logger.info { "Processing evidence #{evidence_id}" }
88
+
89
+ host_td = html_evidence.search("td:starts-with('Host:')").first
90
+ host_label = host_td.next_element.text.split('//').last
91
+ host = content_service.create_node(label: host_label, type: :host)
92
+
93
+ evidence_text =
94
+ template_service.process_template(
95
+ template: 'html_evidence',
96
+ data: html_evidence
97
+ )
98
+
99
+ content_service.create_evidence(
100
+ issue: issue,
101
+ node: host,
102
+ content: evidence_text
103
+ )
104
+ end
105
+
106
+ # Html for an issue and evidence is not nested inside an html element.
107
+ #
108
+ # An issue is the html fragment from <span id="X"> (where X is a single
109
+ # integer number: 1, 2, 3...) until the next span like that or the end of
110
+ # the file.
111
+ #
112
+ # An evidence is the html fragment from <span id="X.Y"> (where X is the
113
+ # issue index and Y the evidence index: 1.1, 1.2,...,2.1, 2.2,...) until
114
+ # the next evidence span (id="X.Z"), the next issue span (id="Y"), or the
115
+ # end of the file.
116
+ #
117
+ # This method extracts all the html related to as specific issue id or
118
+ # evidence id.
119
+ def extract_html_fragment_for(id)
120
+ next_id = if /\d+\.\d+/ =~ id
121
+ id_parts = id.split('.')
122
+ "#{id_parts[0]}.#{id_parts[1].to_i + 1}"
123
+ else
124
+ id.to_i + 1
125
+ end
126
+
127
+ start_element = @doc.xpath("//span[@id='#{id}']")
128
+ return nil if start_element.empty?
129
+
130
+ ending_element = @doc.xpath("//span[@id='#{next_id}']")
131
+ if ending_element.empty? && /\d+\.\d+/ =~ id
132
+ next_id = id.split('.')[0].to_i + 1
133
+ ending_element = @doc.xpath("//span[@id='#{next_id}']")
134
+ end
135
+
136
+ xpath = "//*[preceding-sibling::span[@id='#{id}']"
137
+ xpath += " and following-sibling::span[@id='#{next_id}']" unless ending_element.empty?
138
+ xpath += ']'
139
+
140
+ start_element + @doc.xpath(xpath)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,150 @@
1
+ module Dradis::Plugins::Burp
2
+
3
+ # This module knows how to parse Burp XML format.
4
+ module Xml
5
+ def self.meta
6
+ package = Dradis::Plugins::Burp
7
+ {
8
+ name: package::Engine::plugin_name,
9
+ description: 'Upload Burp Scanner output file (.xml)',
10
+ version: package.version
11
+ }
12
+ end
13
+
14
+ class Importer < Dradis::Plugins::Upload::Importer
15
+ def initialize(args={})
16
+ args[:plugin] = Dradis::Plugins::Burp
17
+ super(args)
18
+ end
19
+ def import(params = {})
20
+ file_content = File.read(params[:file])
21
+
22
+ if file_content =~ /base64="false"/
23
+ error = "Burp input contains HTTP request / response data that hasn't been Base64-encoded.\n"
24
+ error << 'Please re-export your scanner results making sure the Base-64 encode option is selected.'
25
+
26
+ logger.fatal{ error }
27
+ content_service.create_note text: error
28
+ return false
29
+ end
30
+
31
+ logger.info { 'Parsing Burp Scanner XML output file...' }
32
+ doc = Nokogiri::XML(file_content)
33
+ logger.info { 'Done.' }
34
+
35
+ if doc.root.name != 'issues'
36
+ error = "Document doesn't seem to be in the Burp Scanner XML format."
37
+ logger.fatal { error }
38
+ content_service.create_note text: error
39
+ return false
40
+ end
41
+
42
+ # This will be filled in by the Processor while iterating over the issues
43
+ @hosts = []
44
+ @affected_host = nil
45
+ @issue_text = nil
46
+ @evidence_text = nil
47
+
48
+ doc.xpath('issues/issue').each do |xml_issue|
49
+ process_issue(xml_issue)
50
+ end
51
+
52
+ logger.info { 'Burp Scanner results successfully imported' }
53
+ true
54
+ end
55
+
56
+ # Creates the Nodes/properties
57
+ def process_issue(xml_issue)
58
+ host_label = xml_issue.at('host')['ip']
59
+ host_label = xml_issue.at('host').text if host_label.empty?
60
+ affected_host = content_service.create_node(label: host_label, type: :host)
61
+ logger.info { "\taffects: #{host_label}" }
62
+
63
+ unless @hosts.include?(affected_host.label)
64
+ @hosts << affected_host.label
65
+ url = xml_issue.at('host').text
66
+ affected_host.set_property(:hostname, url)
67
+ affected_host.save
68
+ end
69
+
70
+ # Burp extensions don't follow the "unique type for every Issue" logic
71
+ # so we have to deal with them separately
72
+ burp_extension_type = '134217728'.freeze
73
+ if xml_issue.at('type').text.to_str == burp_extension_type
74
+ process_extension_issues(affected_host, xml_issue)
75
+ else
76
+ process_burp_issues(affected_host, xml_issue)
77
+ end
78
+ end
79
+
80
+ # If the Issues come from the Burp app, use the type as the plugin_ic
81
+ def process_burp_issues(affected_host, xml_issue)
82
+ issue_name = xml_issue.at('name').text
83
+ issue_type = xml_issue.at('type').text.to_i
84
+
85
+ logger.info { "Adding #{issue_name} (#{issue_type})" }
86
+
87
+ create_issue(
88
+ affected_host: affected_host,
89
+ id: issue_type,
90
+ xml_issue: xml_issue
91
+ )
92
+ end
93
+
94
+ # If the Issues come from a Burp extension (type = 134217728), then
95
+ # use the name (spaces removed) as the plugin_id
96
+ def process_extension_issues(affected_host, xml_issue)
97
+ ext_name = xml_issue.at('name').text
98
+ ext_name = ext_name.gsub!(" ", "")
99
+
100
+ logger.info { "Adding #{ext_name}" }
101
+
102
+ create_issue(
103
+ affected_host: affected_host,
104
+ id: ext_name,
105
+ xml_issue: xml_issue
106
+ )
107
+ end
108
+
109
+ def create_issue(affected_host:, id:, xml_issue:)
110
+ issue_text =
111
+ template_service.process_template(
112
+ template: 'issue',
113
+ data: xml_issue
114
+ )
115
+
116
+ if issue_text.include?(::Burp::INVALID_UTF_REPLACE)
117
+ logger.info do
118
+ "\tdetected invalid UTF-8 bytes in your issue. " \
119
+ "Replacing them with '#{::Burp::INVALID_UTF_REPLACE}'."
120
+ end
121
+ end
122
+
123
+ issue = content_service.create_issue(text: issue_text, id: id)
124
+
125
+ logger.info do
126
+ "\tadding evidence for this instance to #{affected_host.label}."
127
+ end
128
+
129
+ evidence_text =
130
+ template_service.process_template(
131
+ template: 'evidence',
132
+ data: xml_issue
133
+ )
134
+
135
+ if evidence_text.include?(::Burp::INVALID_UTF_REPLACE)
136
+ logger.info do
137
+ "\tdetected invalid UTF-8 bytes in your evidence. " \
138
+ "Replacing them with '#{::Burp::INVALID_UTF_REPLACE}'."
139
+ end
140
+ end
141
+
142
+ content_service.create_evidence(
143
+ issue: issue,
144
+ node: affected_host,
145
+ content: evidence_text
146
+ )
147
+ end
148
+ end
149
+ end
150
+ end
@@ -3,18 +3,27 @@ class BurpTasks < Thor
3
3
 
4
4
  namespace "dradis:plugins:burp"
5
5
 
6
- desc "upload FILE", "upload Burp XML results"
6
+ desc "upload FILE", "upload Burp XML or HTML results"
7
7
  def upload(file_path)
8
8
  require 'config/environment'
9
9
 
10
10
  unless File.exists?(file_path)
11
11
  $stderr.puts "** the file [#{file_path}] does not exist"
12
- exit -1
12
+ exit(-1)
13
13
  end
14
14
 
15
15
  detect_and_set_project_scope
16
16
 
17
- importer = Dradis::Plugins::Burp::Importer.new(task_options)
17
+ importer =
18
+ if File.extname(file_path) == '.xml'
19
+ Dradis::Plugins::Burp::Xml::Importer.new(task_options)
20
+ elsif File.extname(file_path) == '.html'
21
+ Dradis::Plugins::Burp::Html::Importer.new(task_options)
22
+ else
23
+ $stderr.puts "** Unsupported file. Must be .xml or .html"
24
+ exit(-2)
25
+ end
26
+
18
27
  importer.import(file: file_path)
19
28
  end
20
29
 
@@ -3,17 +3,17 @@ require 'ostruct'
3
3
 
4
4
  describe 'Burp upload plugin' do
5
5
 
6
- describe Burp::Issue do
6
+ describe Burp::Xml::Issue do
7
7
  it "handles invalid utf-8 bytes" do
8
8
  doc = Nokogiri::XML(File.read('spec/fixtures/files/invalid-utf-issue.xml'))
9
9
  xml_issue = doc.xpath('issues/issue').first
10
- issue = Burp::Issue.new(xml_issue)
10
+ issue = Burp::Xml::Issue.new(xml_issue)
11
11
 
12
12
  expect{ issue.request.encode('utf-8') }.to_not raise_error
13
13
  end
14
14
  end
15
15
 
16
- describe "Importer" do
16
+ describe Dradis::Plugins::Burp::Xml::Importer do
17
17
  before(:each) do
18
18
  # Stub template service
19
19
  templates_dir = File.expand_path('../../templates', __FILE__)
@@ -21,7 +21,7 @@ describe 'Burp upload plugin' do
21
21
  .to receive(:default_templates_dir).and_return(templates_dir)
22
22
 
23
23
  # Init services
24
- plugin = Dradis::Plugins::Burp
24
+ plugin = Dradis::Plugins::Burp::Xml
25
25
 
26
26
  @content_service = Dradis::Plugins::ContentService::Base.new(
27
27
  logger: Logger.new(STDOUT),
@@ -113,4 +113,68 @@ describe 'Burp upload plugin' do
113
113
  end
114
114
 
115
115
  end
116
+
117
+ describe Dradis::Plugins::Burp::Html::Importer do
118
+ before(:each) do
119
+ # Stub template service
120
+ templates_dir = File.expand_path('../../templates', __FILE__)
121
+ expect_any_instance_of(Dradis::Plugins::TemplateService)
122
+ .to receive(:default_templates_dir).and_return(templates_dir)
123
+
124
+ # Init services
125
+ plugin = Dradis::Plugins::Burp::Html
126
+
127
+ @content_service = Dradis::Plugins::ContentService::Base.new(
128
+ logger: Logger.new(STDOUT),
129
+ plugin: plugin
130
+ )
131
+
132
+ @importer = plugin::Importer.new(
133
+ content_service: @content_service,
134
+ )
135
+
136
+ # Stub dradis-plugins methods
137
+ #
138
+ # They return their argument hashes as objects mimicking
139
+ # Nodes, Issues, etc
140
+ allow(@content_service).to receive(:create_node) do |args|
141
+ obj = OpenStruct.new(args)
142
+ obj.define_singleton_method(:set_property) { |_, __| }
143
+ obj
144
+ end
145
+ allow(@content_service).to receive(:create_issue) do |args|
146
+ OpenStruct.new(args)
147
+ end
148
+ allow(@content_service).to receive(:create_evidence) do |args|
149
+ OpenStruct.new(args)
150
+ end
151
+ end
152
+
153
+ it "creates nodes, issues, and evidence as needed" do
154
+
155
+ # Host node
156
+ #
157
+ # create_node should be called once for each issue in the xml,
158
+ # but ContentService knows it's already created and NOOPs
159
+ expect(@content_service).to receive(:create_node)
160
+ .with(hash_including label: 'github.com/dradis/dradis-burp')
161
+ .exactly(1).times
162
+
163
+ # # create_issue should be called once for each issue in the xml
164
+ expect(@content_service).to receive(:create_issue) do |args|
165
+ expect(args[:text]).to include("#[Title]#\nStrict transport security not enforced")
166
+ expect(args[:id]).to eq(16777984)
167
+ OpenStruct.new(args)
168
+ end.once
169
+ expect(@content_service).to receive(:create_evidence) do |args|
170
+ expect(args[:content]).to include("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
171
+ expect(args[:issue].text).to include("#[Title]#\nStrict transport security not enforced")
172
+ expect(args[:node].label).to eq("github.com/dradis/dradis-burp")
173
+ end.once
174
+
175
+ # Run the import
176
+ @importer.import(file: 'spec/fixtures/files/burp.html')
177
+ end
178
+
179
+ end
116
180
  end