dradis-burp 3.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +36 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +57 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +23 -0
- data/LICENSE +339 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/dradis-burp.gemspec +34 -0
- data/lib/burp/html/issue.rb +157 -0
- data/lib/burp/issue.rb +43 -0
- data/lib/burp/xml/issue.rb +127 -0
- data/lib/dradis-burp.rb +10 -0
- data/lib/dradis/plugins/burp.rb +12 -0
- data/lib/dradis/plugins/burp/engine.rb +25 -0
- data/lib/dradis/plugins/burp/field_processor.rb +27 -0
- data/lib/dradis/plugins/burp/gem_version.rb +19 -0
- data/lib/dradis/plugins/burp/html/importer.rb +144 -0
- data/lib/dradis/plugins/burp/version.rb +13 -0
- data/lib/dradis/plugins/burp/xml/importer.rb +144 -0
- data/lib/tasks/thorfile.rb +30 -0
- data/spec/burp_upload_spec.rb +220 -0
- data/spec/fixtures/files/burp.html +229 -0
- data/spec/fixtures/files/burp.xml +100 -0
- data/spec/fixtures/files/burp_issue_severity.xml +118 -0
- data/spec/fixtures/files/invalid-utf-issue.xml +21 -0
- data/spec/fixtures/files/without-base64.xml +709 -0
- data/spec/spec_helper.rb +9 -0
- data/templates/evidence.fields +8 -0
- data/templates/evidence.sample +76 -0
- data/templates/evidence.template +20 -0
- data/templates/html_evidence.fields +13 -0
- data/templates/html_evidence.sample +36 -0
- data/templates/html_evidence.template +50 -0
- data/templates/issue.fields +8 -0
- data/templates/issue.sample +23 -0
- data/templates/issue.template +30 -0
- metadata +174 -0
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Burp Scanner plugin for Dradis
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/dradis/dradis-burp.png?branch=master)](http://travis-ci.org/dradis/dradis-burp) [![Code Climate](https://codeclimate.com/github/dradis/dradis-burp.png)](https://codeclimate.com/github/dradis/dradis-burp.png)
|
4
|
+
|
5
|
+
|
6
|
+
Upload Burp Scanner XML export files into Dradis.
|
7
|
+
|
8
|
+
The add-on requires [Dradis CE](https://dradisframework.org/) > 3.0, or [Dradis Pro](https://dradisframework.com/pro/).
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
## More information
|
13
|
+
|
14
|
+
See the Dradis Framework's [README.md](https://github.com/dradis/dradisframework/blob/master/README.md)
|
15
|
+
|
16
|
+
|
17
|
+
## Contributing
|
18
|
+
|
19
|
+
See the Dradis Framework's [CONTRIBUTING.md](https://github.com/dradis/dradisframework/blob/master/CONTRIBUTING.md)
|
20
|
+
|
21
|
+
|
22
|
+
## License
|
23
|
+
|
24
|
+
Dradis Framework and all its components are released under [GNU General Public License version 2.0](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html) as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.
|
25
|
+
|
26
|
+
|
27
|
+
## Feature requests and bugs
|
28
|
+
|
29
|
+
Please use the [Dradis Framework issue tracker](https://github.com/dradis/dradis-ce/issues) for add-on improvements and bug reports.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/dradis-burp.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'dradis/plugins/burp/version'
|
3
|
+
version = Dradis::Plugins::Burp::VERSION::STRING
|
4
|
+
|
5
|
+
# Describe your gem and declare its dependencies:
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.platform = Gem::Platform::RUBY
|
8
|
+
spec.name = 'dradis-burp'
|
9
|
+
spec.version = version
|
10
|
+
spec.summary = 'Burp Scanner upload plugin for the Dradis Framework.'
|
11
|
+
spec.description = 'This plugin allows you to upload and parse output produced from Portswigger\'s Burp Scanner into Dradis.'
|
12
|
+
|
13
|
+
spec.license = 'GPL-2'
|
14
|
+
|
15
|
+
spec.authors = ['Daniel Martin']
|
16
|
+
spec.email = ['etd@nomejortu.com']
|
17
|
+
spec.homepage = 'http://dradisframework.org'
|
18
|
+
|
19
|
+
spec.files = `git ls-files`.split($\)
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
|
23
|
+
# By not including Rails as a dependency, we can use the gem with different
|
24
|
+
# versions of Rails (a sure recipe for disaster, I'm sure), which is needed
|
25
|
+
# until we bump Dradis Pro to 4.1.
|
26
|
+
# s.add_dependency 'rails', '~> 4.1.1'
|
27
|
+
spec.add_dependency 'dradis-plugins', '~> 3.6'
|
28
|
+
spec.add_dependency 'nokogiri', '~> 1.3'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'bundler'
|
31
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
32
|
+
spec.add_development_dependency 'rspec-rails'
|
33
|
+
spec.add_development_dependency 'combustion', '~> 0.5.2'
|
34
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Burp
|
2
|
+
module Html
|
3
|
+
# This class represents each of the issue elements in the Burp
|
4
|
+
# Scanner HTML document: all elemennts from a span.BODH0 until the next
|
5
|
+
# span.BODH0 (the next one excluded).
|
6
|
+
#
|
7
|
+
# It provides a convenient way to access the information scattered all over
|
8
|
+
# the HTML.
|
9
|
+
class Issue < ::Burp::Issue
|
10
|
+
# Accepts a Nokogiri::XML::NodeSet
|
11
|
+
def initialize(html)
|
12
|
+
@html = Nokogiri::HTML(html.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
# List of supported tags
|
16
|
+
def supported_tags
|
17
|
+
[
|
18
|
+
# tags with contents retrieved from inside the span header
|
19
|
+
:name, :type,
|
20
|
+
|
21
|
+
# tags with contents retrieved following the span header
|
22
|
+
:background, :detail,
|
23
|
+
:references, :remediation_background, :remediation_detail,
|
24
|
+
:request, :request_1, :request_2, :request_3,
|
25
|
+
:response, :response_1, :response_2, :response_3,
|
26
|
+
:vulnerability_classifications
|
27
|
+
] + summary_table_tags
|
28
|
+
end
|
29
|
+
|
30
|
+
def header
|
31
|
+
@header ||= @html.at_css('span')
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
@name ||= header.text.gsub(/^\d+\.\S/, '')
|
36
|
+
end
|
37
|
+
|
38
|
+
# Link looks like: https://portswigger.net/kb/issues/00200400_flash-cross-domain-policy
|
39
|
+
# We use that 00200400 as type since in that page it calls it 'Type index'
|
40
|
+
def type
|
41
|
+
@type ||=
|
42
|
+
if header_link = header.at_css('a')
|
43
|
+
header_link.attr('href').to_s[/\/([0-9a-f]+)_.*/, 1].to_i(16)
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# This method is invoked by Ruby when a method that is not defined in this
|
50
|
+
# instance is called.
|
51
|
+
#
|
52
|
+
# In our case we inspect the @method@ parameter and try to find the
|
53
|
+
# corresponding header in our HTML, then return the following text.
|
54
|
+
def method_missing(method, *args)
|
55
|
+
# We could remove this check and return nil for any non-recognized tag.
|
56
|
+
# The problem would be that it would make tricky to debug problems with
|
57
|
+
# typos. For instance: <>.potr would return nil instead of raising an
|
58
|
+
# exception
|
59
|
+
unless supported_tags.include?(method)
|
60
|
+
super
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
# First we try the h2 headers.
|
65
|
+
translations_table = {
|
66
|
+
background: ['Issue background', 'Issue description'],
|
67
|
+
detail: 'Issue detail',
|
68
|
+
references: 'References',
|
69
|
+
remediation_background: ['Remediation background', 'Issue remediation'],
|
70
|
+
remediation_detail: 'Remediation detail',
|
71
|
+
request: 'Request',
|
72
|
+
request_1: 'Request 1',
|
73
|
+
request_2: 'Request 2',
|
74
|
+
request_3: 'Request 3',
|
75
|
+
response: 'Response',
|
76
|
+
response_1: 'Response 1',
|
77
|
+
response_2: 'Response 2',
|
78
|
+
response_3: 'Response 3',
|
79
|
+
serial_number: 'Serial number',
|
80
|
+
vulnerability_classifications: 'Vulnerability classifications'
|
81
|
+
}
|
82
|
+
|
83
|
+
# look for the h2 headers in the html fragment
|
84
|
+
method_names = translations_table.fetch(method, method.to_s)
|
85
|
+
method_names = [method_names].flatten
|
86
|
+
|
87
|
+
h2 = nil
|
88
|
+
method_names.each do |method_name|
|
89
|
+
h2 = @html.xpath("//h2[text()='#{method_name}']").first
|
90
|
+
break if h2
|
91
|
+
end
|
92
|
+
|
93
|
+
if h2
|
94
|
+
content =
|
95
|
+
if h2.text =~ /^(Request|Response)/
|
96
|
+
cleanup_request_response_html(h2.next_element.inner_html)
|
97
|
+
else
|
98
|
+
cleanup_html(h2.next_element.inner_html)
|
99
|
+
end
|
100
|
+
|
101
|
+
return content
|
102
|
+
end
|
103
|
+
|
104
|
+
# look inside the summary table in the html fragment
|
105
|
+
summary[method]
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# In Request/Response html snippets we don't want to cleanup the whole
|
111
|
+
# html as we ususally do. The snippets may contain html code to be displayed,
|
112
|
+
# and we don't want to convert that to textile.
|
113
|
+
def cleanup_request_response_html(source)
|
114
|
+
result = source.dup
|
115
|
+
|
116
|
+
result.gsub!(/<b>(.*?)<\/b>/, '\1')
|
117
|
+
result.gsub!(/<br>|<\/br>/){"\n"}
|
118
|
+
result.gsub!(/<span.*?>/, '')
|
119
|
+
result.gsub!(/<\/span>/, '')
|
120
|
+
|
121
|
+
result.gsub!(/"/, '"')
|
122
|
+
result.gsub!(/&/, '&')
|
123
|
+
result.gsub!(/</, '<')
|
124
|
+
result.gsub!(/>/, '>')
|
125
|
+
result.gsub!(/ /, ' ')
|
126
|
+
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the summary table in the HTML fragment as a Hash
|
131
|
+
def summary
|
132
|
+
@summary ||= begin
|
133
|
+
@summary = {}
|
134
|
+
h2 = @html.search("h2[text()='Summary']").first
|
135
|
+
return @summary if h2.nil?
|
136
|
+
|
137
|
+
table = h2.next_element
|
138
|
+
|
139
|
+
summary_table_tags.each do |tag|
|
140
|
+
td = table.search("td:starts-with('#{tag.to_s.capitalize}:')").first
|
141
|
+
@summary[tag] = td.next_element.text
|
142
|
+
end
|
143
|
+
|
144
|
+
@summary
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# List of supported tags to obtain from the summary html table
|
149
|
+
def summary_table_tags
|
150
|
+
[
|
151
|
+
:confidence, :host, :path, :severity
|
152
|
+
]
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/burp/issue.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Burp
|
2
|
+
# We use this string to replace invalid UTF-8 bytes with.
|
3
|
+
INVALID_UTF_REPLACE = '<?>'
|
4
|
+
|
5
|
+
class Issue
|
6
|
+
|
7
|
+
# This allows external callers (and specs) to check for implemented
|
8
|
+
# properties
|
9
|
+
def respond_to?(method, include_private=false)
|
10
|
+
return true if supported_tags.include?(method.to_sym)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def cleanup_html(source)
|
17
|
+
result = source.dup
|
18
|
+
result.gsub!(/"/, '"')
|
19
|
+
result.gsub!(/&/, '&')
|
20
|
+
result.gsub!(/</, '<')
|
21
|
+
result.gsub!(/>/, '>')
|
22
|
+
result.gsub!(/ /, ' ')
|
23
|
+
|
24
|
+
result.gsub!(/<b>(.*?)<\/b>/, '*\1*')
|
25
|
+
result.gsub!(/<br>|<\/br>/){"\n"}
|
26
|
+
result.gsub!(/<font.*?>(.*?)<\/font>/m, '\1')
|
27
|
+
result.gsub!(/<h\d?>(.*?)<\/h\d?>/, '*\1*')
|
28
|
+
result.gsub!(/<i>(.*?)<\/i>/, '\1')
|
29
|
+
result.gsub!(/<p>|<\/p>/){"\n"}
|
30
|
+
result.gsub!(/<pre.*?>(.*?)<\/pre>/m){|m| "\n\nbc.. #{ $1 }\n\np. \n" }
|
31
|
+
|
32
|
+
result.gsub!(/<ul>(.*?)<\/ul>/m){|m| "#{ $1 }\n"}
|
33
|
+
result.gsub!(/<li>(.*?)<\/li>/m){|m| "\n* #{ $1 }"}
|
34
|
+
result.gsub!(/<a href=\"(.*?)\">(.*?)<\/a>/im) { "\"#{$2.strip}\":#{$1.strip}" }
|
35
|
+
|
36
|
+
result.gsub!(/<table>(.*?)<\/table>/m){|m| "\n\n#{ $1 }\n\n" }
|
37
|
+
result.gsub!(/<tr>(.*?)<\/tr>/m){|m| "|#{ $1 }\n" }
|
38
|
+
result.gsub!(/<td>(.*?)<\/td>/, '\1|')
|
39
|
+
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Burp
|
2
|
+
module Xml
|
3
|
+
|
4
|
+
# This class represents each of the /issues/issue elements in the Burp
|
5
|
+
# Scanner XML document.
|
6
|
+
#
|
7
|
+
# It provides a convenient way to access the information scattered all over
|
8
|
+
# the XML in attributes and nested tags.
|
9
|
+
#
|
10
|
+
# Instead of providing separate methods for each supported property we rely
|
11
|
+
# on Ruby's #method_missing to do most of the work.
|
12
|
+
class Issue < ::Burp::Issue
|
13
|
+
|
14
|
+
# Accepts an XML node from Nokogiri::XML.
|
15
|
+
def initialize(xml_node)
|
16
|
+
@xml = xml_node
|
17
|
+
end
|
18
|
+
|
19
|
+
# List of supported tags. They can be attributes, simple descendants or
|
20
|
+
# collections (e.g. <references/>, <tags/>)
|
21
|
+
def supported_tags
|
22
|
+
[
|
23
|
+
# attributes
|
24
|
+
|
25
|
+
# simple tags
|
26
|
+
:background, :confidence, :detail, :host, :location, :name, :path,
|
27
|
+
:references, :remediation_background, :remediation_detail,
|
28
|
+
:serial_number, :severity, :type,
|
29
|
+
:vulnerability_classifications,
|
30
|
+
|
31
|
+
# nested tags
|
32
|
+
:request, :response
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
# This method is invoked by Ruby when a method that is not defined in this
|
37
|
+
# instance is called.
|
38
|
+
#
|
39
|
+
# In our case we inspect the @method@ parameter and try to find the
|
40
|
+
# attribute, simple descendent or collection that it maps to in the XML
|
41
|
+
# tree.
|
42
|
+
def method_missing(method, *args)
|
43
|
+
|
44
|
+
# We could remove this check and return nil for any non-recognized tag.
|
45
|
+
# The problem would be that it would make tricky to debug problems with
|
46
|
+
# typos. For instance: <>.potr would return nil instead of raising an
|
47
|
+
# exception
|
48
|
+
unless supported_tags.include?(method)
|
49
|
+
super
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
# First we try the attributes. In Ruby we use snake_case, but in XML
|
54
|
+
# CamelCase is used for some attributes
|
55
|
+
translations_table = {
|
56
|
+
background: 'issueBackground',
|
57
|
+
detail: 'issueDetail',
|
58
|
+
remediation_background: 'remediationBackground',
|
59
|
+
remediation_detail: 'remediationDetail',
|
60
|
+
vulnerability_classifications: 'vulnerabilityClassifications',
|
61
|
+
serial_number: 'serialNumber'
|
62
|
+
}
|
63
|
+
|
64
|
+
method_name = translations_table.fetch(method, method.to_s)
|
65
|
+
|
66
|
+
# no attributes in the <issue> node
|
67
|
+
# return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
|
68
|
+
|
69
|
+
# Then we try simple children tags: name, type, ...
|
70
|
+
tag = @xml.xpath("./#{method_name}").first
|
71
|
+
if tag && !tag.text.blank?
|
72
|
+
if tags_with_html_content.include?(method)
|
73
|
+
return cleanup_html(tag.text)
|
74
|
+
else
|
75
|
+
return tag.text
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if (['request', 'response'].include?(method_name))
|
80
|
+
requestresponse_child(method_name)
|
81
|
+
else
|
82
|
+
# nothing found, the tag is valid but not present in this ReportItem
|
83
|
+
return nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Some of the values have embedded HTML content that we need to strip
|
90
|
+
def tags_with_html_content
|
91
|
+
[:background, :detail, :remediation_background, :remediation_detail, :references, :vulnerability_classifications]
|
92
|
+
end
|
93
|
+
|
94
|
+
def requestresponse_child(field)
|
95
|
+
return 'n/a' unless @xml.at('requestresponse') && @xml.at("requestresponse/#{field}")
|
96
|
+
|
97
|
+
xml_node = @xml.at("requestresponse/#{field}")
|
98
|
+
result = "[unprocessable #{field}]"
|
99
|
+
|
100
|
+
if xml_node['base64'] == 'true'
|
101
|
+
result = Base64::strict_decode64(xml_node.text)
|
102
|
+
|
103
|
+
# don't pass binary data to the DB.
|
104
|
+
if result =~ /\0/
|
105
|
+
header, _ = result.split("\r\n\r\n")
|
106
|
+
result = header << "\r\n\r\n" << '[Binary Data Not Displayed]'
|
107
|
+
end
|
108
|
+
else
|
109
|
+
result = xml_node.text
|
110
|
+
end
|
111
|
+
|
112
|
+
# Just in case a null byte was left by Burp
|
113
|
+
result.gsub!(/\0/,'�')
|
114
|
+
|
115
|
+
# We truncate the request/response because it can be pretty big.
|
116
|
+
# If it is > 1M MySQL will die when trying to INSERT
|
117
|
+
#
|
118
|
+
# TODO: maybe add a reference to this node's XPATH so the user can go
|
119
|
+
# back to the burp scanner file and look up the original request/response
|
120
|
+
result.truncate(50000, omission: '... (truncated)')
|
121
|
+
|
122
|
+
# Encode the string to UTF-8 to catch invalid bytes.
|
123
|
+
result.encode('utf-8', invalid: :replace, undef: :replace, replace: ::Burp::INVALID_UTF_REPLACE)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/dradis-burp.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Burp
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'dradis/plugins/burp/engine'
|
9
|
+
require 'dradis/plugins/burp/field_processor'
|
10
|
+
require 'dradis/plugins/burp/html/importer'
|
11
|
+
require 'dradis/plugins/burp/version'
|
12
|
+
require 'dradis/plugins/burp/xml/importer'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Burp
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Dradis::Plugins::Burp
|
6
|
+
|
7
|
+
include ::Dradis::Plugins::Base
|
8
|
+
description 'Processes Burp Scanner output'
|
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
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|