librex 0.0.35 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +1 -1
- data/lib/rex/exploitation/javascriptosdetect.rb +3 -22
- data/lib/rex/exploitation/jsobfu.rb +489 -0
- data/lib/rex/io/stream_abstraction.rb +4 -3
- data/lib/rex/parser/acunetix_nokogiri.rb +394 -0
- data/lib/rex/parser/appscan_nokogiri.rb +366 -0
- data/lib/rex/parser/burp_session_nokogiri.rb +290 -0
- data/lib/rex/parser/nokogiri_doc_mixin.rb +5 -3
- data/lib/rex/pescan/scanner.rb +2 -1
- data/lib/rex/post/meterpreter/extensions/stdapi/constants.rb +2 -0
- data/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb +5 -0
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/registry.rb +30 -1
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/registry_subsystem/remote_registry_key.rb +188 -0
- data/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb +1 -0
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb +84 -14
- data/lib/rex/proto/http/header.rb +3 -3
- data/lib/rex/ropbuilder.rb +7 -0
- data/lib/rex/ropbuilder/rop.rb +257 -0
- data/lib/rex/ui/text/table.rb +9 -0
- metadata +10 -3
@@ -148,7 +148,7 @@ protected
|
|
148
148
|
closed = true
|
149
149
|
wlog("monitor_rsock: closed remote socket due to nil read")
|
150
150
|
end
|
151
|
-
rescue ::Exception
|
151
|
+
rescue ::Exception => e
|
152
152
|
closed = true
|
153
153
|
wlog("monitor_rsock: exception during read: #{e.class} #{e}")
|
154
154
|
end
|
@@ -169,10 +169,11 @@ protected
|
|
169
169
|
# This way we naturally perform a resend if a failure occured.
|
170
170
|
# Catches an edge case with meterpreter TCP channels where remote send
|
171
171
|
# failes gracefully and a resend is required.
|
172
|
-
if (sent.nil?
|
172
|
+
if (sent.nil?)
|
173
|
+
closed = true
|
173
174
|
wlog("monitor_rsock: failed writing, socket must be dead")
|
174
175
|
break
|
175
|
-
|
176
|
+
elsif (sent > 0)
|
176
177
|
total_sent += sent
|
177
178
|
end
|
178
179
|
rescue ::IOError, ::EOFError => e
|
@@ -0,0 +1,394 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)),"nokogiri_doc_mixin")
|
2
|
+
require 'rex'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Rex
|
6
|
+
module Parser
|
7
|
+
|
8
|
+
# If Nokogiri is available, define the Acunetix document class.
|
9
|
+
load_nokogiri && class AcunetixDocument < Nokogiri::XML::SAX::Document
|
10
|
+
|
11
|
+
include NokogiriDocMixin
|
12
|
+
|
13
|
+
# The resolver prefers your local /etc/hosts (or windows equiv), but will
|
14
|
+
# fall back to regular DNS. It retains a cache for the import to avoid
|
15
|
+
# spamming your network with DNS requests.
|
16
|
+
attr_reader :resolv_cache
|
17
|
+
|
18
|
+
# If name resolution of the host fails out completely, you will not be
|
19
|
+
# able to import that Scan task. Other scan tasks in the same report
|
20
|
+
# should be unaffected.
|
21
|
+
attr_reader :parse_warnings
|
22
|
+
|
23
|
+
def start_document
|
24
|
+
@parse_warnings = []
|
25
|
+
@resolv_cache = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_element(name=nil,attrs=[])
|
29
|
+
attrs = normalize_attrs(attrs)
|
30
|
+
block = @block
|
31
|
+
@state[:current_tag][name] = true
|
32
|
+
case name
|
33
|
+
when "Scan" # Start of the thing.
|
34
|
+
when "Name", "StartURL", "Banner", "Os"
|
35
|
+
@state[:has_text] = true
|
36
|
+
when "LoginSequence" # Skipping for now
|
37
|
+
when "Crawler"
|
38
|
+
record_crawler(attrs)
|
39
|
+
when "FullURL"
|
40
|
+
@state[:has_text] = true
|
41
|
+
when "Variable"
|
42
|
+
record_variable(attrs)
|
43
|
+
when "Request", "Response"
|
44
|
+
@state[:has_text] = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def end_element(name=nil)
|
49
|
+
block = @block
|
50
|
+
case name
|
51
|
+
when "Scan"
|
52
|
+
# Clears most of the @state out, we're done with this web site.
|
53
|
+
@state.delete_if {|k| k != :current_tag}
|
54
|
+
when "Name"
|
55
|
+
@state[:has_text] = false
|
56
|
+
collect_scan_name
|
57
|
+
collect_report_item_name
|
58
|
+
@text = nil
|
59
|
+
when "StartURL" # Populates @state[:starturl_uri], we use this a lot
|
60
|
+
@state[:has_text] = false
|
61
|
+
collect_host
|
62
|
+
collect_service
|
63
|
+
@text = nil
|
64
|
+
handle_parse_warnings &block
|
65
|
+
host_object = report_host &block
|
66
|
+
if host_object
|
67
|
+
report_starturl_service(host_object,&block)
|
68
|
+
db.report_import_note(@args[:wspace],host_object)
|
69
|
+
end
|
70
|
+
when "Banner"
|
71
|
+
@state[:has_text] = false
|
72
|
+
collect_and_report_banner
|
73
|
+
when "Os"
|
74
|
+
@state[:has_text] = false
|
75
|
+
report_os_fingerprint
|
76
|
+
when "LoginSequence" # This comes up later in the report anyway
|
77
|
+
when "Crawler"
|
78
|
+
report_starturl_web_site(&block)
|
79
|
+
when "FullURL"
|
80
|
+
@state[:has_text] = false
|
81
|
+
report_web_site(@text,&block)
|
82
|
+
@text = nil
|
83
|
+
when "Inputs"
|
84
|
+
report_web_form(&block)
|
85
|
+
when "Request"
|
86
|
+
@state[:has_text] = false
|
87
|
+
collect_page_request
|
88
|
+
@text = nil
|
89
|
+
when "Response"
|
90
|
+
@state[:has_text] = false
|
91
|
+
collect_page_response
|
92
|
+
@text = nil
|
93
|
+
report_web_page(&block)
|
94
|
+
end
|
95
|
+
@state[:current_tag].delete name
|
96
|
+
end
|
97
|
+
|
98
|
+
def collect_page_response
|
99
|
+
return unless in_tag("TechnicalDetails")
|
100
|
+
return unless in_tag("ReportItem")
|
101
|
+
return unless @text
|
102
|
+
return if @text.to_s.empty?
|
103
|
+
@state[:page_response] = @text
|
104
|
+
end
|
105
|
+
|
106
|
+
def collect_page_request
|
107
|
+
return unless in_tag("TechnicalDetails")
|
108
|
+
return unless in_tag("ReportItem")
|
109
|
+
return unless @text
|
110
|
+
return if @text.to_s.empty?
|
111
|
+
@state[:page_request] = @text
|
112
|
+
end
|
113
|
+
|
114
|
+
def collect_scan_name
|
115
|
+
return unless in_tag("Scan")
|
116
|
+
return if in_tag("ReportItems")
|
117
|
+
return if in_tag("Crawler")
|
118
|
+
return unless @text
|
119
|
+
return if @text.strip.empty?
|
120
|
+
@state[:scan_name] = @text.strip
|
121
|
+
end
|
122
|
+
|
123
|
+
def collect_host
|
124
|
+
return unless in_tag("Scan")
|
125
|
+
return unless @text
|
126
|
+
return if @text.strip.empty?
|
127
|
+
uri = URI.parse(@text) rescue nil
|
128
|
+
return unless uri
|
129
|
+
address = resolve_scan_starturl_address(uri)
|
130
|
+
@report_data[:host] = address
|
131
|
+
@report_data[:state] = Msf::HostState::Alive
|
132
|
+
end
|
133
|
+
|
134
|
+
def collect_service
|
135
|
+
return unless @report_data[:host]
|
136
|
+
return unless in_tag("Scan")
|
137
|
+
return unless @text
|
138
|
+
return if @text.strip.empty?
|
139
|
+
uri = URI.parse(@text) rescue nil
|
140
|
+
return unless uri
|
141
|
+
@state[:starturl_uri] = uri
|
142
|
+
@report_data[:ports] ||= []
|
143
|
+
@report_data[:ports] << @state[:starturl_port]
|
144
|
+
end
|
145
|
+
|
146
|
+
def collect_and_report_banner
|
147
|
+
return unless (svc = @state[:starturl_service_object]) # Yes i want assignment
|
148
|
+
return unless @text
|
149
|
+
return if @text.strip.empty?
|
150
|
+
return unless in_tag("Scan")
|
151
|
+
svc_info = {
|
152
|
+
:host => svc.host,
|
153
|
+
:port => svc.port,
|
154
|
+
:proto => svc.proto,
|
155
|
+
:info => @text.strip
|
156
|
+
}
|
157
|
+
db_report(:service, svc_info)
|
158
|
+
@text = nil
|
159
|
+
end
|
160
|
+
|
161
|
+
def collect_report_item_name
|
162
|
+
return unless in_tag("ReportItem")
|
163
|
+
return unless @text
|
164
|
+
return if @text.strip.empty?
|
165
|
+
@state[:report_item] = @text
|
166
|
+
end
|
167
|
+
|
168
|
+
# @state[:fullurl] is set by report_web_site
|
169
|
+
def record_variable(attrs)
|
170
|
+
return unless in_tag("Inputs")
|
171
|
+
return unless @state[:fullurl].kind_of? URI
|
172
|
+
method = attr_hash(attrs)["Type"]
|
173
|
+
return unless method
|
174
|
+
return if method.strip.empty?
|
175
|
+
@state[:form_variables] ||= []
|
176
|
+
@state[:form_variables] << [attr_hash(attrs)["Name"],method]
|
177
|
+
end
|
178
|
+
|
179
|
+
def record_crawler(attrs)
|
180
|
+
return unless in_tag("Scan")
|
181
|
+
return unless @state[:starturl_service_object]
|
182
|
+
starturl = attr_hash(attrs)["StartUrl"]
|
183
|
+
return unless starturl
|
184
|
+
@state[:crawler_starturl] = starturl
|
185
|
+
end
|
186
|
+
|
187
|
+
def report_web_form(&block)
|
188
|
+
return unless in_tag("SiteFiles")
|
189
|
+
return unless @state[:web_site]
|
190
|
+
return unless @state[:fullurl].kind_of? URI
|
191
|
+
return unless @state[:form_variables].kind_of? Array
|
192
|
+
return if @state[:form_variables].empty?
|
193
|
+
method = @state[:form_variables].first[1]
|
194
|
+
vars = @state[:form_variables].map {|x| x[0]}
|
195
|
+
form_info = {}
|
196
|
+
form_info[:web_site] = @state[:web_site]
|
197
|
+
form_info[:path] = @state[:fullurl].path
|
198
|
+
form_info[:query] = @state[:fullurl].query
|
199
|
+
form_info[:method] = method
|
200
|
+
form_info[:params] = vars
|
201
|
+
url = @state[:fullurl].to_s
|
202
|
+
db.emit(:web_form,url,&block) if block
|
203
|
+
db_report(:web_form,form_info)
|
204
|
+
@state[:fullurl] = nil
|
205
|
+
@state[:form_variables] = nil
|
206
|
+
end
|
207
|
+
|
208
|
+
def report_web_page(&block)
|
209
|
+
return if should_skip_this_page
|
210
|
+
return unless @state[:web_site]
|
211
|
+
return unless @state[:page_request]
|
212
|
+
return if @state[:page_request].strip.empty?
|
213
|
+
return unless @state[:page_response]
|
214
|
+
return if @state[:page_response].strip.empty?
|
215
|
+
path,query_string = parse_request(@state[:page_request])
|
216
|
+
return unless path
|
217
|
+
parsed_response = parse_response(@state[:page_response])
|
218
|
+
return unless parsed_response
|
219
|
+
web_page_info = {}
|
220
|
+
web_page_info[:web_site] = @state[:web_site]
|
221
|
+
web_page_info[:path] = path
|
222
|
+
web_page_info[:code] = parsed_response[:code].to_i
|
223
|
+
web_page_info[:headers] = parsed_response[:headers]
|
224
|
+
web_page_info[:body] = parsed_response[:body]
|
225
|
+
web_page_info[:query] = query_string || ""
|
226
|
+
url = ""
|
227
|
+
url << @state[:web_site].service.name.to_s << "://"
|
228
|
+
url << @state[:web_site].vhost.to_s << ":"
|
229
|
+
url << path
|
230
|
+
uri = URI.parse(url) rescue nil
|
231
|
+
return unless uri # Sanity checker
|
232
|
+
db.emit(:web_page, url, &block) if block
|
233
|
+
web_page_object = db_report(:web_page,web_page_info)
|
234
|
+
@state[:page_request] = @state[:page_response] = nil
|
235
|
+
@state[:web_page] = web_page_object
|
236
|
+
end
|
237
|
+
|
238
|
+
# Reasons why we shouldn't collect a particular web page.
|
239
|
+
def should_skip_this_page
|
240
|
+
if @state[:report_item] =~ /Unrestricted File Upload/
|
241
|
+
# This means that the page being collected is something the
|
242
|
+
# auditor put there, so it's not useful to report on.
|
243
|
+
return true
|
244
|
+
end
|
245
|
+
return false
|
246
|
+
end
|
247
|
+
|
248
|
+
# XXX Rex::Proto::Http::Packet seems broken for
|
249
|
+
# actually parsing requests and responses, but all I
|
250
|
+
# need are the headers anyway
|
251
|
+
def parse_request(request)
|
252
|
+
headers = Rex::Proto::Http::Packet::Header.new
|
253
|
+
headers.from_s request.dup # It's destructive.
|
254
|
+
return unless headers.cmd_string
|
255
|
+
verb,req = headers.cmd_string.split(/\s+/)
|
256
|
+
return unless verb
|
257
|
+
return unless req
|
258
|
+
path,query_string = req.split(/\?/)[0,2]
|
259
|
+
end
|
260
|
+
|
261
|
+
def parse_response(response)
|
262
|
+
headers = Rex::Proto::Http::Packet::Header.new
|
263
|
+
headers.from_s response.dup # It's destructive.
|
264
|
+
return unless headers.cmd_string
|
265
|
+
http,code,msg = headers.cmd_string.split(/\s+/)
|
266
|
+
return unless code
|
267
|
+
return unless code.to_i.to_s == code
|
268
|
+
parsed = {}
|
269
|
+
parsed[:code] = code
|
270
|
+
parsed[:headers] = {}
|
271
|
+
headers.each do |k,v|
|
272
|
+
parsed[:headers][k.to_s.downcase] = []
|
273
|
+
parsed[:headers][k.to_s.downcase] << v
|
274
|
+
end
|
275
|
+
parsed[:body] = "" # We never seem to get this from Acunetix
|
276
|
+
parsed
|
277
|
+
end
|
278
|
+
|
279
|
+
def report_host(&block)
|
280
|
+
return unless @report_data[:host]
|
281
|
+
return unless in_tag("Scan")
|
282
|
+
if host_is_okay
|
283
|
+
db.emit(:address,@report_data[:host],&block) if block
|
284
|
+
host_info = @report_data.merge(:workspace => @args[:wspace])
|
285
|
+
db_report(:host,host_info)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# The service is super important, so we hang on to it for the
|
290
|
+
# rest of the scan.
|
291
|
+
def report_starturl_service(host_object,&block)
|
292
|
+
return unless host_object
|
293
|
+
return unless @state[:starturl_uri]
|
294
|
+
name = @state[:starturl_uri].scheme
|
295
|
+
port = @state[:starturl_uri].port
|
296
|
+
addr = host_object.address
|
297
|
+
svc = {
|
298
|
+
:host => host_object,
|
299
|
+
:port => port,
|
300
|
+
:name => name.dup,
|
301
|
+
:proto => "tcp"
|
302
|
+
}
|
303
|
+
if name and port
|
304
|
+
db.emit(:service,[addr,port].join(":"),&block) if block
|
305
|
+
@state[:starturl_service_object] = db_report(:service,svc)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def report_web_site(url,&block)
|
310
|
+
return unless in_tag("Crawler")
|
311
|
+
return unless url
|
312
|
+
return if url.strip.empty?
|
313
|
+
uri = URI.parse(url) rescue nil
|
314
|
+
return unless uri
|
315
|
+
host = uri.host
|
316
|
+
port = uri.port
|
317
|
+
scheme = uri.scheme
|
318
|
+
return unless scheme[/^https?/]
|
319
|
+
return unless (host && port && scheme)
|
320
|
+
address = resolve_address(host)
|
321
|
+
return unless address
|
322
|
+
service_info = [ @args[:wspace], address, "tcp", port ]
|
323
|
+
service_object = db.get_service(*service_info)
|
324
|
+
service_object = db_report(:service,service_info) unless service_object
|
325
|
+
web_site_info = {
|
326
|
+
:workspace => @args[:wspace],
|
327
|
+
:service => service_object,
|
328
|
+
:vhost => host,
|
329
|
+
:ssl => (scheme == "https")
|
330
|
+
}
|
331
|
+
@state[:web_site] = db_report(:web_site,web_site_info)
|
332
|
+
@state[:fullurl] = uri
|
333
|
+
end
|
334
|
+
|
335
|
+
def report_starturl_web_site(&block)
|
336
|
+
return unless @state[:crawler_starturl]
|
337
|
+
starturl = @state[:crawler_starturl].dup
|
338
|
+
report_web_site(starturl,&block)
|
339
|
+
end
|
340
|
+
|
341
|
+
def report_os_fingerprint
|
342
|
+
return unless @state[:starturl_service_object]
|
343
|
+
return unless @text
|
344
|
+
return if @text.strip.empty?
|
345
|
+
return unless in_tag("Scan")
|
346
|
+
host = @state[:starturl_service_object].host
|
347
|
+
fp_note = {
|
348
|
+
:workspace => host.workspace,
|
349
|
+
:host => host,
|
350
|
+
:type => 'host.os.acunetix_fingerprint',
|
351
|
+
:data => {:os => @text}
|
352
|
+
}
|
353
|
+
db_report(:note, fp_note)
|
354
|
+
@text = nil
|
355
|
+
end
|
356
|
+
|
357
|
+
def resolve_port(uri)
|
358
|
+
@state[:port] = uri.port
|
359
|
+
unless @state[:port]
|
360
|
+
@parse_warnings << "Could not determine a port for '#{@state[:scan_name]}'"
|
361
|
+
end
|
362
|
+
@state[:port] = uri.port
|
363
|
+
end
|
364
|
+
|
365
|
+
def resolve_address(host)
|
366
|
+
return @resolv_cache[host] if @resolv_cache[host]
|
367
|
+
address = Rex::Socket.resolv_to_dotted(host) rescue nil
|
368
|
+
@resolv_cache[host] = address
|
369
|
+
return address
|
370
|
+
end
|
371
|
+
|
372
|
+
def resolve_scan_starturl_address(uri)
|
373
|
+
if uri.host
|
374
|
+
address = resolve_address(uri.host)
|
375
|
+
unless address
|
376
|
+
@parse_warnings << "Could not resolve address for '#{uri.host}', skipping '#{@state[:scan_name]}'"
|
377
|
+
end
|
378
|
+
else
|
379
|
+
@parse_warnings << "Could not determine a host for '#{@state[:scan_name]}'"
|
380
|
+
end
|
381
|
+
address
|
382
|
+
end
|
383
|
+
|
384
|
+
def handle_parse_warnings(&block)
|
385
|
+
return if @parse_warnings.empty?
|
386
|
+
@parse_warnings.each do |pwarn|
|
387
|
+
db.emit(:warning, pwarn, &block) if block
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
@@ -0,0 +1,366 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)),"nokogiri_doc_mixin")
|
2
|
+
|
3
|
+
module Rex
|
4
|
+
module Parser
|
5
|
+
|
6
|
+
# If Nokogiri is available, define AppScan document class.
|
7
|
+
load_nokogiri && class AppscanDocument < Nokogiri::XML::SAX::Document
|
8
|
+
|
9
|
+
include NokogiriDocMixin
|
10
|
+
|
11
|
+
# The resolver prefers your local /etc/hosts (or windows equiv), but will
|
12
|
+
# fall back to regular DNS. It retains a cache for the import to avoid
|
13
|
+
# spamming your network with DNS requests.
|
14
|
+
attr_reader :resolv_cache
|
15
|
+
|
16
|
+
# If name resolution of the host fails out completely, you will not be
|
17
|
+
# able to import that Scan task. Other scan tasks in the same report
|
18
|
+
# should be unaffected.
|
19
|
+
attr_reader :parse_warning
|
20
|
+
|
21
|
+
def start_document
|
22
|
+
@parse_warnings = []
|
23
|
+
@resolv_cache = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def start_element(name=nil,attrs=[])
|
27
|
+
attrs = normalize_attrs(attrs)
|
28
|
+
block = @block
|
29
|
+
@state[:current_tag][name] = true
|
30
|
+
case name
|
31
|
+
when "Issue" # Start of the stuff we want
|
32
|
+
collect_issue(attrs)
|
33
|
+
when "Entity" # Start of the stuff we want
|
34
|
+
collect_entity(attrs)
|
35
|
+
when "Severity", "Url", "OriginalHttpTraffic"
|
36
|
+
@state[:has_text] = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def end_element(name=nil)
|
41
|
+
block = @block
|
42
|
+
case name
|
43
|
+
when "Issue" # Wrap it up
|
44
|
+
record_issue
|
45
|
+
# Reset the state once we close an issue
|
46
|
+
@state = @state.select do
|
47
|
+
|k| [:current_tag, :web_sites].include? k
|
48
|
+
end
|
49
|
+
when "Url" # Populates @state[:web_site]
|
50
|
+
@state[:has_text] = false
|
51
|
+
record_url
|
52
|
+
@text = nil
|
53
|
+
report_web_site(&block)
|
54
|
+
handle_parse_warnings(&block)
|
55
|
+
when "Severity"
|
56
|
+
@state[:has_text] = false
|
57
|
+
record_risk
|
58
|
+
@text = nil
|
59
|
+
when "OriginalHttpTraffic" # Request and response
|
60
|
+
@state[:has_text] = false
|
61
|
+
record_request_and_response
|
62
|
+
report_service_info
|
63
|
+
page_info = report_web_page(&block)
|
64
|
+
if page_info
|
65
|
+
form_info = report_web_form(page_info,&block)
|
66
|
+
if form_info
|
67
|
+
report_web_vuln(form_info,&block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@text = nil
|
71
|
+
end
|
72
|
+
@state[:current_tag].delete name
|
73
|
+
end
|
74
|
+
|
75
|
+
def report_web_vuln(form_info,&block)
|
76
|
+
return unless(in_issue && has_text)
|
77
|
+
return unless form_info.kind_of? Hash
|
78
|
+
return unless @state[:issue]
|
79
|
+
return unless @state[:issue]["Noise"]
|
80
|
+
return unless @state[:issue]["Noise"].to_s.downcase == "false"
|
81
|
+
return unless @state[:issue][:vuln_param]
|
82
|
+
web_vuln_info = {}
|
83
|
+
web_vuln_info[:web_site] = form_info[:web_site]
|
84
|
+
web_vuln_info[:path] = form_info[:path]
|
85
|
+
web_vuln_info[:query] = form_info[:query]
|
86
|
+
web_vuln_info[:method] = form_info[:method]
|
87
|
+
web_vuln_info[:params] = form_info[:params]
|
88
|
+
web_vuln_info[:pname] = @state[:issue][:vuln_param]
|
89
|
+
web_vuln_info[:proof] = "" # TODO: pick this up from <Difference> maybe?
|
90
|
+
web_vuln_info[:risk] = @state[:issue][:risk]
|
91
|
+
web_vuln_info[:name] = @state[:issue]["IssueTypeID"]
|
92
|
+
web_vuln_info[:category] = "imported"
|
93
|
+
web_vuln_info[:confidence] = 100 # Seems pretty binary, noise or not
|
94
|
+
db.emit(:web_vuln, web_vuln_info[:name], &block) if block
|
95
|
+
web_vuln = db_report(:web_vuln, web_vuln_info)
|
96
|
+
end
|
97
|
+
|
98
|
+
def collect_entity(attrs)
|
99
|
+
return unless in_issue
|
100
|
+
return unless @state[:issue].kind_of? Hash
|
101
|
+
ent_hash = attr_hash(attrs)
|
102
|
+
return unless ent_hash
|
103
|
+
return unless ent_hash["Type"].to_s.downcase == "parameter"
|
104
|
+
@state[:issue][:vuln_param] = ent_hash["Name"]
|
105
|
+
end
|
106
|
+
|
107
|
+
def report_web_form(page_info,&block)
|
108
|
+
return unless(in_issue && has_text)
|
109
|
+
return unless page_info.kind_of? Hash
|
110
|
+
return unless @state[:request_body]
|
111
|
+
return if @state[:request_body].strip.empty?
|
112
|
+
web_form_info = {}
|
113
|
+
web_form_info[:web_site] = page_info[:web_site]
|
114
|
+
web_form_info[:path] = page_info[:path]
|
115
|
+
web_form_info[:query] = page_info[:query]
|
116
|
+
web_form_info[:method] = @state[:request_headers].cmd_string.split(/\s+/)[0]
|
117
|
+
parsed_params = parse_params(@state[:request_body])
|
118
|
+
return unless parsed_params
|
119
|
+
return if parsed_params.empty?
|
120
|
+
web_form_info[:params] = parsed_params
|
121
|
+
web_form = db_report(:web_form, web_form_info)
|
122
|
+
@state[:web_forms] ||= []
|
123
|
+
unless @state[:web_forms].include? web_form
|
124
|
+
db.emit(:web_form, @state[:uri].to_s, &block) if block
|
125
|
+
@state[:web_forms] << web_form
|
126
|
+
end
|
127
|
+
web_form_info
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_params(request_body)
|
131
|
+
return unless request_body
|
132
|
+
pairs = request_body.split(/&/)
|
133
|
+
params = []
|
134
|
+
pairs.each do |pair|
|
135
|
+
param,value = pair.split("=",2)
|
136
|
+
params << [param,""] # Can't tell what's default
|
137
|
+
end
|
138
|
+
params
|
139
|
+
end
|
140
|
+
|
141
|
+
def report_web_page(&block)
|
142
|
+
return unless(in_issue && has_text)
|
143
|
+
return unless @state[:web_site]
|
144
|
+
return unless @state[:response_headers]
|
145
|
+
return unless @state[:uri]
|
146
|
+
web_page_info = {}
|
147
|
+
web_page_info[:web_site] = @state[:web_site]
|
148
|
+
web_page_info[:path] = @state[:uri].path
|
149
|
+
web_page_info[:body] = @state[:response_body].to_s
|
150
|
+
web_page_info[:query] = @state[:uri].query
|
151
|
+
code = @state[:response_headers].cmd_string.split(/\s+/)[1]
|
152
|
+
return unless code
|
153
|
+
web_page_info[:code] = code
|
154
|
+
parsed_headers = {}
|
155
|
+
@state[:response_headers].each do |k,v|
|
156
|
+
parsed_headers[k.to_s.downcase] ||= []
|
157
|
+
parsed_headers[k.to_s.downcase] << v
|
158
|
+
end
|
159
|
+
return if parsed_headers.empty?
|
160
|
+
web_page_info[:headers] = parsed_headers
|
161
|
+
web_page = db_report(:web_page, web_page_info)
|
162
|
+
@state[:web_pages] ||= []
|
163
|
+
unless @state[:web_pages].include? web_page
|
164
|
+
db.emit(:web_page, @state[:uri].to_s, &block) if block
|
165
|
+
@state[:web_pages] << web_page
|
166
|
+
end
|
167
|
+
web_page_info
|
168
|
+
end
|
169
|
+
|
170
|
+
def report_service_info
|
171
|
+
return unless(in_issue && has_text)
|
172
|
+
return unless @state[:web_site]
|
173
|
+
return unless @state[:response_headers]
|
174
|
+
banner = @state[:response_headers]["server"]
|
175
|
+
return unless banner
|
176
|
+
service = @state[:web_site].service
|
177
|
+
return unless service.info.to_s.empty?
|
178
|
+
service_info = {
|
179
|
+
:host => service.host,
|
180
|
+
:port => service.port,
|
181
|
+
:proto => service.proto,
|
182
|
+
:info => banner
|
183
|
+
}
|
184
|
+
db_report(:service, service_info)
|
185
|
+
end
|
186
|
+
|
187
|
+
def record_request_and_response
|
188
|
+
return unless(in_issue && has_text)
|
189
|
+
return unless @state[:web_site]
|
190
|
+
really_original_traffic = unindent_and_crlf(@text)
|
191
|
+
split_traffic = really_original_traffic.split(/\r\n\r\n/)
|
192
|
+
request_headers_text = split_traffic.first
|
193
|
+
content_length = 0
|
194
|
+
if request_headers_text =~ /\ncontent-length:\s+([0-9]+)/mni
|
195
|
+
content_length = $1.to_i
|
196
|
+
end
|
197
|
+
if(content_length > 0) and (split_traffic[1].to_s.size >= content_length)
|
198
|
+
request_body_text = split_traffic[1].to_s[0,content_length]
|
199
|
+
else
|
200
|
+
request_body_text = nil
|
201
|
+
end
|
202
|
+
response_headers_text = split_traffic[1].to_s[content_length,split_traffic[1].to_s.size].lstrip
|
203
|
+
request = request_headers_text
|
204
|
+
return unless(request && response_headers_text)
|
205
|
+
response_body_text = split_traffic[2]
|
206
|
+
req_header = Rex::Proto::Http::Packet::Header.new
|
207
|
+
res_header = Rex::Proto::Http::Packet::Header.new
|
208
|
+
req_header.from_s request_headers_text.dup
|
209
|
+
res_header.from_s response_headers_text.dup
|
210
|
+
@state[:request_headers] = req_header
|
211
|
+
@state[:request_body] = request_body_text
|
212
|
+
@state[:response_headers] = res_header
|
213
|
+
@state[:response_body] = response_body_text
|
214
|
+
end
|
215
|
+
|
216
|
+
# Appscan tab-indents which makes parsing a little difficult. They
|
217
|
+
# also don't record CRLFs, just LFs.
|
218
|
+
def unindent_and_crlf(text)
|
219
|
+
second_line = text.split(/\r*\n/)[1]
|
220
|
+
indent_level = second_line.size - second_line.lstrip.size
|
221
|
+
unindented_text_lines = []
|
222
|
+
text.split(/\r*\n/).each do |line|
|
223
|
+
if line =~ /^\t{#{indent_level}}/
|
224
|
+
unindented_line = line[indent_level,line.size]
|
225
|
+
unindented_text_lines << unindented_line
|
226
|
+
else
|
227
|
+
unindented_text_lines << line
|
228
|
+
end
|
229
|
+
end
|
230
|
+
unindented_text_lines.join("\r\n")
|
231
|
+
end
|
232
|
+
|
233
|
+
def record_risk
|
234
|
+
return unless(in_issue && has_text)
|
235
|
+
@state[:issue] ||= {}
|
236
|
+
@state[:issue][:risk] = map_severity_to_risk
|
237
|
+
end
|
238
|
+
|
239
|
+
def map_severity_to_risk
|
240
|
+
case @text.to_s.downcase
|
241
|
+
when "high" ; 5
|
242
|
+
when "medium" ; 3
|
243
|
+
when "low" ; 1
|
244
|
+
else ; 0
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# TODO
|
249
|
+
def record_issue
|
250
|
+
return unless in_issue
|
251
|
+
return unless @report_data[:issue].kind_of? Hash
|
252
|
+
return unless @state[:web_site]
|
253
|
+
return if @state[:issue]["Noise"].to_s.downcase == "true"
|
254
|
+
end
|
255
|
+
|
256
|
+
def collect_issue(attrs)
|
257
|
+
return unless in_issue
|
258
|
+
@state[:issue] = {}
|
259
|
+
@state[:issue].merge! attr_hash(attrs)
|
260
|
+
end
|
261
|
+
|
262
|
+
def report_web_site(&block)
|
263
|
+
return unless @state[:uri]
|
264
|
+
uri = @state[:uri]
|
265
|
+
hostname = uri.host # Assume the first one is the real hostname
|
266
|
+
address = resolve_issue_url_address(uri)
|
267
|
+
return unless address
|
268
|
+
unless @resolv_cache.values.include? address
|
269
|
+
db.emit(:address, address, &block) if block
|
270
|
+
end
|
271
|
+
port = resolve_port(uri)
|
272
|
+
return unless port
|
273
|
+
scheme = uri.scheme
|
274
|
+
return unless scheme
|
275
|
+
web_site_info = {:workspace => @args[:wspace]}
|
276
|
+
web_site_info[:vhost] = hostname
|
277
|
+
service_obj = check_for_existing_service(address,port)
|
278
|
+
if service_obj
|
279
|
+
web_site_info[:service] = service_obj
|
280
|
+
else
|
281
|
+
web_site_info[:host] = address
|
282
|
+
web_site_info[:port] = port
|
283
|
+
web_site_info[:ssl] = scheme == "https"
|
284
|
+
end
|
285
|
+
web_site_obj = db_report(:web_site, web_site_info)
|
286
|
+
@state[:web_sites] ||= []
|
287
|
+
unless @state[:web_sites].include? web_site_obj
|
288
|
+
url = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
289
|
+
db.emit(:web_site, url, &block) if block
|
290
|
+
db.report_import_note(@args[:wspace], web_site_obj.service.host)
|
291
|
+
@state[:web_sites] << web_site_obj
|
292
|
+
end
|
293
|
+
@state[:service] = service_obj || web_site_obj.service
|
294
|
+
@state[:host] = (service_obj || web_site_obj.service).host
|
295
|
+
@state[:web_site] = web_site_obj
|
296
|
+
end
|
297
|
+
|
298
|
+
def check_for_existing_service(address,port)
|
299
|
+
db.get_service(@args[:wspace],address,"tcp",port)
|
300
|
+
end
|
301
|
+
|
302
|
+
def resolve_port(uri)
|
303
|
+
@state[:port] = uri.port
|
304
|
+
unless @state[:port]
|
305
|
+
@parse_warnings << "Could not determine a port for '#{@state[:scan_name]}'"
|
306
|
+
end
|
307
|
+
return @state[:port]
|
308
|
+
end
|
309
|
+
|
310
|
+
def resolve_address(host)
|
311
|
+
return @resolv_cache[host] if @resolv_cache[host]
|
312
|
+
address = Rex::Socket.resolv_to_dotted(host) rescue nil
|
313
|
+
@resolv_cache[host] = address
|
314
|
+
if address
|
315
|
+
block = @block
|
316
|
+
db.emit(:address, address, &block) if block
|
317
|
+
end
|
318
|
+
return address
|
319
|
+
end
|
320
|
+
|
321
|
+
# Alias this
|
322
|
+
def resolve_issue_url_address(uri)
|
323
|
+
if uri.host
|
324
|
+
address = resolve_address(uri.host)
|
325
|
+
unless address
|
326
|
+
@parse_warnings << "Could not resolve address for '#{uri.host}', skipping."
|
327
|
+
end
|
328
|
+
else
|
329
|
+
@parse_warnings << "Could not determine a host for this import."
|
330
|
+
end
|
331
|
+
address
|
332
|
+
end
|
333
|
+
|
334
|
+
def handle_parse_warnings(&block)
|
335
|
+
return if @parse_warnings.empty?
|
336
|
+
@parse_warnings.each do |pwarn|
|
337
|
+
db.emit(:warning, pwarn, &block) if block
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def record_url
|
342
|
+
return unless in_issue
|
343
|
+
return unless has_text
|
344
|
+
uri = URI.parse(@text) rescue nil
|
345
|
+
return unless uri
|
346
|
+
@state[:uri] = uri
|
347
|
+
end
|
348
|
+
|
349
|
+
def in_issue
|
350
|
+
return false unless in_tag("Issue")
|
351
|
+
return false unless in_tag("Issues")
|
352
|
+
return false unless in_tag("XmlReport")
|
353
|
+
return true
|
354
|
+
end
|
355
|
+
|
356
|
+
def has_text
|
357
|
+
return false unless @text
|
358
|
+
return false if @text.strip.empty?
|
359
|
+
@text = @text.strip
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|