pwn 0.5.331 → 0.5.333
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -5
- data/Gemfile +5 -4
- data/bin/pwn_burp_suite_pro_active_scan +1 -1
- data/lib/pwn/plugins/burp_suite.rb +439 -93
- data/lib/pwn/plugins/open_api.rb +879 -0
- data/lib/pwn/plugins/transparent_browser.rb +48 -16
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/plugins/open_api_spec.rb +15 -0
- data/third_party/pwn_rdoc.jsonl +19 -2
- metadata +25 -9
@@ -56,12 +56,7 @@ module PWN
|
|
56
56
|
|
57
57
|
burp_root = File.dirname(burp_jar_path)
|
58
58
|
|
59
|
-
browser_type =
|
60
|
-
:firefox
|
61
|
-
else
|
62
|
-
opts[:browser_type]
|
63
|
-
end
|
64
|
-
|
59
|
+
browser_type = opts[:browser_type] ||= :firefox
|
65
60
|
burp_ip = opts[:burp_ip] ||= '127.0.0.1'
|
66
61
|
burp_port = opts[:burp_port] ||= 8080
|
67
62
|
# burp_port = opts[:burp_port] ||= PWN::Plugins::Sock.get_random_unused_port
|
@@ -90,7 +85,8 @@ module PWN
|
|
90
85
|
# Proxy always listens on localhost...use SSH tunneling if remote access is required
|
91
86
|
browser_obj2 = PWN::Plugins::TransparentBrowser.open(
|
92
87
|
browser_type: browser_type,
|
93
|
-
proxy: "http://#{burp_obj[:mitm_proxy]}"
|
88
|
+
proxy: "http://#{burp_obj[:mitm_proxy]}",
|
89
|
+
devtools: true
|
94
90
|
)
|
95
91
|
|
96
92
|
burp_obj[:burp_browser] = browser_obj2
|
@@ -122,41 +118,51 @@ module PWN
|
|
122
118
|
end
|
123
119
|
|
124
120
|
# Supported Method Parameters::
|
125
|
-
#
|
126
|
-
#
|
121
|
+
# uri_in_scope = PWN::Plugins::BurpSuite.in_scope(
|
122
|
+
# burp_obj: 'required - burp_obj returned by #start method',
|
127
123
|
# uri: 'required - URI to determine if in scope'
|
128
124
|
# )
|
129
125
|
|
130
|
-
public_class_method def self.
|
131
|
-
|
132
|
-
raise 'ERROR:
|
126
|
+
public_class_method def self.in_scope(opts = {})
|
127
|
+
burp_obj = opts[:burp_obj]
|
128
|
+
raise 'ERROR: burp_obj parameter is required' unless burp_obj.is_a?(Hash)
|
133
129
|
|
134
130
|
uri = opts[:uri]
|
135
131
|
raise 'ERROR: uri parameter is required' if uri.nil?
|
136
132
|
|
137
|
-
|
138
|
-
|
139
|
-
|
133
|
+
rest_browser = burp_obj[:rest_browser]
|
134
|
+
pwn_burp_api = burp_obj[:pwn_burp_api]
|
135
|
+
base64_encoded_uri = Base64.strict_encode64(uri.to_s.scrub.strip.chomp)
|
136
|
+
|
137
|
+
in_scope_resp = rest_browser.get(
|
138
|
+
"http://#{pwn_burp_api}/scope/#{base64_encoded_uri}",
|
139
|
+
content_type: 'application/json; charset=UTF8'
|
140
140
|
)
|
141
|
+
json_in_scope = JSON.parse(in_scope_resp, symbolize_names: true)
|
142
|
+
json_in_scope[:value]
|
143
|
+
rescue StandardError => e
|
144
|
+
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
145
|
+
raise e
|
146
|
+
end
|
141
147
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
148
|
-
return false unless out_of_scope_arr.empty?
|
148
|
+
# Supported Method Parameters::
|
149
|
+
# json_in_scope = PWN::Plugins::BurpSuite.add_to_scope(
|
150
|
+
# burp_obj: 'required - burp_obj returned by #start method',
|
151
|
+
# target_url: 'required - target url to add to scope'
|
152
|
+
# )
|
149
153
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
154
|
+
public_class_method def self.add_to_scope(opts = {})
|
155
|
+
burp_obj = opts[:burp_obj]
|
156
|
+
target_url = opts[:target_url]
|
157
|
+
rest_browser = burp_obj[:rest_browser]
|
158
|
+
pwn_burp_api = burp_obj[:pwn_burp_api]
|
159
|
+
|
160
|
+
post_body = { url: target_url }.to_json
|
157
161
|
|
158
|
-
|
162
|
+
in_scope = rest_browser.post("http://#{pwn_burp_api}/scope", post_body, content_type: 'application/json; charset=UTF8')
|
163
|
+
JSON.parse(in_scope, symbolize_names: true)
|
159
164
|
rescue StandardError => e
|
165
|
+
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
160
166
|
raise e
|
161
167
|
end
|
162
168
|
|
@@ -171,6 +177,7 @@ module PWN
|
|
171
177
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
172
178
|
|
173
179
|
enable_resp = rest_browser.post("http://#{pwn_burp_api}/proxy/intercept/enable", nil)
|
180
|
+
JSON.parse(enable_resp, symbolize_names: true)
|
174
181
|
rescue StandardError => e
|
175
182
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
176
183
|
raise e
|
@@ -187,55 +194,379 @@ module PWN
|
|
187
194
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
188
195
|
|
189
196
|
disable_resp = rest_browser.post("http://#{pwn_burp_api}/proxy/intercept/disable", nil)
|
197
|
+
JSON.parse(disable_resp, symbolize_names: true)
|
190
198
|
rescue StandardError => e
|
191
199
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
192
200
|
raise e
|
193
201
|
end
|
194
202
|
|
195
203
|
# Supported Method Parameters::
|
196
|
-
# json_sitemap = PWN::Plugins::BurpSuite.
|
197
|
-
# burp_obj: 'required - burp_obj returned by #start method'
|
204
|
+
# json_sitemap = PWN::Plugins::BurpSuite.get_sitemap(
|
205
|
+
# burp_obj: 'required - burp_obj returned by #start method',
|
206
|
+
# target_url: 'optional - target URL to filter sitemap results (defaults to entire sitemap)'
|
198
207
|
# )
|
199
208
|
|
200
|
-
public_class_method def self.
|
209
|
+
public_class_method def self.get_sitemap(opts = {})
|
201
210
|
burp_obj = opts[:burp_obj]
|
202
211
|
rest_browser = burp_obj[:rest_browser]
|
203
212
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
213
|
+
target_url = opts[:target_url]
|
214
|
+
|
215
|
+
base64_encoded_target_url = Base64.strict_encode64(target_url.to_s.scrub.strip.chomp) if target_url
|
216
|
+
|
217
|
+
rest_call = "http://#{pwn_burp_api}/sitemap"
|
218
|
+
rest_call = "#{rest_call}/#{base64_encoded_target_url}" if target_url
|
219
|
+
|
220
|
+
sitemap = rest_browser.get(
|
221
|
+
rest_call,
|
222
|
+
content_type: 'application/json; charset=UTF8'
|
223
|
+
)
|
204
224
|
|
205
|
-
sitemap = rest_browser.get("http://#{pwn_burp_api}/sitemap", content_type: 'application/json; charset=UTF8')
|
206
|
-
# json_sitemap = JSON.parse(sitemap, symbolize_names: true)
|
207
|
-
# json_sitemap is an array of hashes.
|
208
|
-
# each hash contains a :request and :response key.
|
209
|
-
# both of these values are Base64 encoded strings.
|
210
|
-
# We want to decode them in an array of hashes.
|
211
|
-
# json_sitemap.map do |site|
|
212
|
-
# site[:request] = Base64.decode64(site[:request]) if site[:request]
|
213
|
-
# site[:response] = Base64.decode64(site[:response]) if site[:response]
|
214
|
-
# end
|
215
|
-
|
216
|
-
# json_sitemap
|
217
225
|
JSON.parse(sitemap, symbolize_names: true)
|
218
226
|
rescue StandardError => e
|
219
227
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
220
228
|
raise e
|
221
229
|
end
|
222
230
|
|
223
|
-
# Supported Method Parameters
|
224
|
-
#
|
231
|
+
# Supported Method Parameters:
|
232
|
+
# json_sitemap = PWN::Plugins::BurpSuite.add_to_sitemap(
|
225
233
|
# burp_obj: 'required - burp_obj returned by #start method',
|
226
|
-
#
|
234
|
+
# sitemap: 'required - sitemap hash to add',
|
235
|
+
# debug: 'optional - boolean to enable sitemap debugging (default: false)'
|
227
236
|
# )
|
228
|
-
|
229
|
-
|
237
|
+
#
|
238
|
+
# Example:
|
239
|
+
# json_sitemap = PWN::Plugins::BurpSuite.add_to_sitemap(
|
240
|
+
# burp_obj: burp_obj,
|
241
|
+
# sitemap: {
|
242
|
+
# request: 'base64_encoded_request_string',
|
243
|
+
# response: 'base64_encoded_response_string',
|
244
|
+
# highlight: 'NONE'||'RED'||'ORANGE'||'YELLOW'||'GREEN'||'CYAN'||'BLUE'||'PINK'||'MAGENTA'||'GRAY',
|
245
|
+
# comment: 'optional comment for the sitemap entry',
|
246
|
+
# http_service: {
|
247
|
+
# host: 'example.com',
|
248
|
+
# port: 80,
|
249
|
+
# protocol: 'http'
|
250
|
+
# }
|
251
|
+
# }
|
252
|
+
|
253
|
+
public_class_method def self.add_to_sitemap(opts = {})
|
230
254
|
burp_obj = opts[:burp_obj]
|
231
|
-
target_url = opts[:target_url]
|
232
255
|
rest_browser = burp_obj[:rest_browser]
|
233
256
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
257
|
+
sitemap = opts[:sitemap] ||= {}
|
258
|
+
debug = opts[:debug] || false
|
259
|
+
|
260
|
+
# Send POST request to /sitemap
|
261
|
+
response = RestClient.post(
|
262
|
+
"#{pwn_burp_api}/sitemap",
|
263
|
+
sitemap.to_json,
|
264
|
+
content_type: 'application/json; charset=UTF-8'
|
265
|
+
)
|
234
266
|
|
235
|
-
|
267
|
+
if debug
|
268
|
+
puts "\nSubmitted:"
|
269
|
+
puts sitemap.inspect
|
270
|
+
print 'Press Enter to continue...'
|
271
|
+
gets
|
272
|
+
end
|
273
|
+
# Return response body (assumed to be JSON)
|
274
|
+
JSON.parse(response.body, symbolize_names: true)
|
275
|
+
rescue RestClient::ExceptionWithResponse => e
|
276
|
+
raise StandardError, "HTTP error adding to sitemap: Status #{e.response.code}, Response: #{e.response.body}"
|
277
|
+
rescue StandardError => e
|
278
|
+
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
279
|
+
raise e
|
280
|
+
end
|
236
281
|
|
237
|
-
|
238
|
-
|
282
|
+
# Supported Method Parameters:
|
283
|
+
# json_sitemap = PWN::Plugins::BurpSuite.import_openapi_to_sitemap(
|
284
|
+
# burp_obj: 'required - burp_obj returned by #start method',
|
285
|
+
# openapi_spec: 'required - path to OpenAPI JSON specification file',
|
286
|
+
# additional_http_headers: 'optional - hash of additional HTTP headers to include in requests (default: {})',
|
287
|
+
# highlight: 'optional - highlight color for the sitemap entry (default: "NONE")',
|
288
|
+
# comment: 'optional - comment for the sitemap entry (default: "")',
|
289
|
+
# debug: 'optional - boolean to enable debug logging (default: false)'
|
290
|
+
# )
|
291
|
+
public_class_method def self.import_openapi_to_sitemap(opts = {})
|
292
|
+
burp_obj = opts[:burp_obj]
|
293
|
+
raise 'ERROR: burp_obj parameter is required' unless burp_obj.is_a?(Hash)
|
294
|
+
|
295
|
+
openapi_spec = opts[:openapi_spec]
|
296
|
+
raise 'ERROR: openapi_spec parameter not found' unless File.exist?(openapi_spec)
|
297
|
+
|
298
|
+
additional_http_headers = opts[:additional_http_headers] || {}
|
299
|
+
raise 'ERROR: additional_http_headers must be a Hash' unless additional_http_headers.is_a?(Hash)
|
300
|
+
|
301
|
+
highlight = opts[:highlight] ||= 'NONE'
|
302
|
+
comment = opts[:comment].to_s.scrub
|
303
|
+
|
304
|
+
debug = opts[:debug] || false
|
305
|
+
|
306
|
+
# Parse the OpenAPI JSON
|
307
|
+
openapi = JSON.parse(File.read(openapi_spec), symbolize_names: true)
|
308
|
+
|
309
|
+
# Initialize result array
|
310
|
+
sitemap_arr = []
|
311
|
+
|
312
|
+
# Get servers; default to empty array if not present
|
313
|
+
servers = openapi[:servers].is_a?(Array) ? openapi[:servers] : []
|
314
|
+
if servers.empty?
|
315
|
+
warn("No servers defined in #{openapi_spec}. Using default server 'http://localhost'.")
|
316
|
+
servers = [{ url: 'http://localhost', description: 'Default server' }]
|
317
|
+
end
|
318
|
+
|
319
|
+
# Valid HTTP methods for validation
|
320
|
+
valid_methods = %w[get post put patch delete head options trace connect]
|
321
|
+
|
322
|
+
# Iterate through each server
|
323
|
+
servers.each do |server|
|
324
|
+
server_url = server[:url]
|
325
|
+
unless server_url.is_a?(String)
|
326
|
+
warn("[ERROR] Invalid server URL type '#{server_url.class}' in #{openapi_spec}: Expected String, got #{server_url.inspect}")
|
327
|
+
next
|
328
|
+
end
|
329
|
+
|
330
|
+
begin
|
331
|
+
uri = URI.parse(server_url)
|
332
|
+
host = uri.host
|
333
|
+
port = uri.port
|
334
|
+
protocol = uri.scheme
|
335
|
+
server_path = uri.path&.sub(%r{^/+}, '')&.sub(%r{/+$}, '') || ''
|
336
|
+
|
337
|
+
warn("[DEBUG] Processing server: #{server_url}, host: #{host}, port: #{port}, protocol: #{protocol}, server_path: #{server_path}") if debug
|
338
|
+
|
339
|
+
# Iterate through each path and its methods
|
340
|
+
openapi[:paths]&.each do |path, methods|
|
341
|
+
# Convert path to string, handling different types
|
342
|
+
path_str = case path
|
343
|
+
when Symbol, String
|
344
|
+
path.to_s
|
345
|
+
else
|
346
|
+
warn("[ERROR] Invalid path type '#{path.class}' in #{openapi_spec}: Expected Symbol or String, got #{path.inspect}")
|
347
|
+
'/' # Fallback to root path
|
348
|
+
end
|
349
|
+
|
350
|
+
# Construct full path by prepending server path if present
|
351
|
+
full_path = server_path.empty? ? path_str : "/#{server_path}/#{path_str.sub(%r{^/+}, '')}".gsub(%r{/+}, '/')
|
352
|
+
|
353
|
+
# Initialize path-level parameters
|
354
|
+
path_parameters = []
|
355
|
+
|
356
|
+
# Process methods based on type
|
357
|
+
operations = []
|
358
|
+
if methods.is_a?(Hash)
|
359
|
+
# Extract path-level parameters
|
360
|
+
path_parameters = methods[:parameters].is_a?(Array) ? methods[:parameters] : []
|
361
|
+
warn("[DEBUG] Path-level parameters for #{full_path}: #{path_parameters.inspect}") if debug && !path_parameters.empty?
|
362
|
+
|
363
|
+
# Collect operations for valid HTTP methods
|
364
|
+
methods.each do |method, details|
|
365
|
+
method_str = case method
|
366
|
+
when Symbol, String
|
367
|
+
method.to_s.downcase
|
368
|
+
else
|
369
|
+
warn("[ERROR] Invalid method type '#{method.class}' for path '#{full_path}' in #{openapi_spec}: Expected Symbol or String, got #{method.inspect}")
|
370
|
+
nil
|
371
|
+
end
|
372
|
+
|
373
|
+
next unless method_str && valid_methods.include?(method_str)
|
374
|
+
|
375
|
+
operations << { method: method_str, details: details }
|
376
|
+
end
|
377
|
+
elsif methods.is_a?(Array)
|
378
|
+
warn("[DEBUG] Methods is an array for path '#{full_path}' in #{openapi_spec}: #{methods.inspect}") if debug
|
379
|
+
|
380
|
+
# Look for parameters in the array
|
381
|
+
param_entry = methods.find { |m| m.is_a?(Hash) && m[:parameters].is_a?(Array) }
|
382
|
+
path_parameters = param_entry[:parameters] if param_entry
|
383
|
+
warn("[DEBUG] Path-level parameters for #{full_path}: #{path_parameters.inspect}") if debug && !path_parameters.empty?
|
384
|
+
|
385
|
+
# Collect operations from array elements
|
386
|
+
methods.each do |op|
|
387
|
+
next unless op.is_a?(Hash)
|
388
|
+
|
389
|
+
# Infer method from operationId or other indicators
|
390
|
+
method_str = if op[:operationId].is_a?(String)
|
391
|
+
op_id = op[:operationId].downcase
|
392
|
+
valid_methods.find { |m| op_id.start_with?(m) }
|
393
|
+
elsif op[:method].is_a?(String) || op[:method].is_a?(Symbol)
|
394
|
+
op[:method].to_s.downcase if valid_methods.include?(op[:method].to_s.downcase)
|
395
|
+
end
|
396
|
+
|
397
|
+
if method_str
|
398
|
+
operations << { method: method_str, details: op }
|
399
|
+
else
|
400
|
+
warn("[ERROR] Could not infer valid HTTP method for operation #{op.inspect} in path '#{full_path}' in #{openapi_spec}")
|
401
|
+
end
|
402
|
+
end
|
403
|
+
else
|
404
|
+
warn("[ERROR] Invalid methods type '#{methods.class}' for path '#{full_path}' in #{openapi_spec}: Expected Hash or Array, got #{methods.inspect}")
|
405
|
+
end
|
406
|
+
|
407
|
+
# Process each operation
|
408
|
+
operations.each do |op|
|
409
|
+
method_str = op[:method]
|
410
|
+
details = op[:details]
|
411
|
+
|
412
|
+
# Handle details based on type
|
413
|
+
operation = case details
|
414
|
+
when Hash
|
415
|
+
details
|
416
|
+
when Array
|
417
|
+
# Find the first hash with responses, or use empty hash
|
418
|
+
selected = details.find { |d| d.is_a?(Hash) && d[:responses].is_a?(Hash) }
|
419
|
+
if selected
|
420
|
+
selected
|
421
|
+
else
|
422
|
+
warn("[ERROR] No valid operation hash found in array for #{method_str.upcase} #{full_path} in #{openapi_spec}: Got #{details.inspect}")
|
423
|
+
{}
|
424
|
+
end
|
425
|
+
else
|
426
|
+
warn("[ERROR] Invalid details type '#{details.class}' for #{method_str.upcase} #{full_path} in #{openapi_spec}: Expected Hash or Array, got #{details.inspect}")
|
427
|
+
{}
|
428
|
+
end
|
429
|
+
|
430
|
+
# Skip if operation is empty (indicating invalid details)
|
431
|
+
if operation.empty?
|
432
|
+
warn("[DEBUG] Skipping #{method_str.upcase} #{full_path} due to invalid operation data") if debug
|
433
|
+
next
|
434
|
+
end
|
435
|
+
|
436
|
+
# Skip if no valid responses
|
437
|
+
unless operation[:responses].is_a?(Hash)
|
438
|
+
warn("[ERROR] No valid responses for #{method_str.upcase} #{full_path} in #{openapi_spec}: Expected Hash, got #{operation[:responses].inspect}")
|
439
|
+
next
|
440
|
+
end
|
441
|
+
|
442
|
+
begin
|
443
|
+
# Combine path-level and operation-level parameters
|
444
|
+
operation_parameters = operation[:parameters].is_a?(Array) ? operation[:parameters] : []
|
445
|
+
all_parameters = path_parameters + operation_parameters
|
446
|
+
warn("[DEBUG] All parameters for #{method_str.upcase} #{full_path}: #{all_parameters.inspect}") if debug && !all_parameters.empty?
|
447
|
+
|
448
|
+
# Process path parameters for substitution
|
449
|
+
request_path = full_path.dup
|
450
|
+
query_params = []
|
451
|
+
|
452
|
+
all_parameters.each do |param|
|
453
|
+
next unless param.is_a?(Hash) && param[:name] && param[:in]
|
454
|
+
|
455
|
+
param_name = param[:name].to_s
|
456
|
+
case param[:in]
|
457
|
+
when 'path'
|
458
|
+
# Substitute path parameter with a default value (e.g., 'example')
|
459
|
+
param_value = param[:schema]&.dig(:example) || 'example'
|
460
|
+
request_path.gsub!("{#{param_name}}", param_value.to_s)
|
461
|
+
when 'query'
|
462
|
+
# Collect query parameters
|
463
|
+
param_value = param[:schema]&.dig(:example) || 'example'
|
464
|
+
query_params << "#{URI.encode_www_form_component(param_name)}=#{URI.encode_www_form_component(param_value.to_s)}"
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Append query parameters to path if any
|
469
|
+
request_path += "?#{query_params.join('&')}" if query_params.any?
|
470
|
+
|
471
|
+
# Construct HTTP request headers
|
472
|
+
request_headers = {
|
473
|
+
host: host
|
474
|
+
}
|
475
|
+
request_headers.merge!(additional_http_headers)
|
476
|
+
|
477
|
+
# Construct request lines, including all headers
|
478
|
+
request_lines = [
|
479
|
+
"#{method_str.upcase} #{request_path} HTTP/1.1"
|
480
|
+
]
|
481
|
+
request_headers.each do |key, value|
|
482
|
+
# Capitalize header keys (e.g., 'host' to 'Host', 'authorization' to 'Authorization')
|
483
|
+
header_key = key.to_s.split('-').map(&:capitalize).join('-')
|
484
|
+
request_lines << "#{header_key}: #{value}"
|
485
|
+
end
|
486
|
+
request_lines << '' << '' # Add blank lines for HTTP request body separation
|
487
|
+
|
488
|
+
request = request_lines.join("\r\n")
|
489
|
+
encoded_request = Base64.strict_encode64(request)
|
490
|
+
|
491
|
+
# Determine response code from operation[:responses].keys
|
492
|
+
fallback_response_code = 200
|
493
|
+
response_keys = operation[:responses].keys
|
494
|
+
response_code = response_keys.find { |key| key.to_s.to_i.between?(100, 599) }.to_s.to_i
|
495
|
+
response_code ||= fallback_response_code
|
496
|
+
|
497
|
+
response_status = case response_code
|
498
|
+
when 200 then '200 OK'
|
499
|
+
when 201 then '201 Created'
|
500
|
+
when 204 then '204 No Content'
|
501
|
+
when 301 then '301 Moved Permanently'
|
502
|
+
when 302 then '302 Found'
|
503
|
+
when 303 then '303 See Other'
|
504
|
+
when 304 then '304 Not Modified'
|
505
|
+
when 307 then '307 Temporary Redirect'
|
506
|
+
when 308 then '308 Permanent Redirect'
|
507
|
+
when 400 then '400 Bad Request'
|
508
|
+
when 401 then '401 Unauthorized'
|
509
|
+
when 403 then '403 Forbidden'
|
510
|
+
when 404 then '404 Not Found'
|
511
|
+
when 500 then '500 Internal Server Error'
|
512
|
+
when 502 then '502 Bad Gateway'
|
513
|
+
when 503 then '503 Service Unavailable'
|
514
|
+
when 504 then '504 Gateway Timeout'
|
515
|
+
else "#{fallback_response_code} OK"
|
516
|
+
end
|
517
|
+
|
518
|
+
# Construct response body
|
519
|
+
response_body = operation[:responses][response_code]&.dig(:description) ||
|
520
|
+
"Endpoint #{method_str.upcase} #{request_path} response"
|
521
|
+
|
522
|
+
# Safely determine Content-Type
|
523
|
+
content_type = if operation[:responses][response_code]
|
524
|
+
content = operation[:responses][response_code][:content]
|
525
|
+
content&.keys&.first || 'text/plain'
|
526
|
+
else
|
527
|
+
'text/plain'
|
528
|
+
end
|
529
|
+
|
530
|
+
response_lines = [
|
531
|
+
"HTTP/1.1 #{response_status}",
|
532
|
+
"Content-Type: #{content_type}",
|
533
|
+
"Content-Length: #{response_body.length}",
|
534
|
+
'',
|
535
|
+
response_body
|
536
|
+
]
|
537
|
+
response = response_lines.join("\r\n")
|
538
|
+
encoded_response = Base64.strict_encode64(response)
|
539
|
+
|
540
|
+
# Build the hash for this endpoint
|
541
|
+
sitemap_hash = {
|
542
|
+
request: encoded_request,
|
543
|
+
response: encoded_response,
|
544
|
+
highlight: highlight.to_s.upcase,
|
545
|
+
comment: comment,
|
546
|
+
http_service: {
|
547
|
+
host: host,
|
548
|
+
port: port,
|
549
|
+
protocol: protocol
|
550
|
+
}
|
551
|
+
}
|
552
|
+
|
553
|
+
# Add to the results array
|
554
|
+
sitemap_arr.push(sitemap_hash)
|
555
|
+
warn("[DEBUG] Added sitemap entry for #{method_str.upcase} #{request_path} on #{server_url} with headers #{request_headers.inspect}") if debug
|
556
|
+
rescue StandardError => e
|
557
|
+
warn("[ERROR] Failed to process #{method_str.upcase} #{full_path} on #{server_url}: #{e.message}")
|
558
|
+
warn("[DEBUG] Operation: #{operation.inspect}, Parameters: #{all_parameters.inspect}, Headers: #{request_headers.inspect}") if debug
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
rescue URI::InvalidURIError => e
|
563
|
+
warn("[ERROR] Invalid server URL '#{server_url}' in #{openapi_spec}: #{e.message}")
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
sitemap_arr.each { |sitemap| add_to_sitemap(burp_obj: burp_obj, sitemap: sitemap) }
|
568
|
+
|
569
|
+
sitemap_arr
|
239
570
|
rescue StandardError => e
|
240
571
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
241
572
|
raise e
|
@@ -255,18 +586,14 @@ module PWN
|
|
255
586
|
target_scheme = URI.parse(target_url).scheme
|
256
587
|
target_host = URI.parse(target_url).host
|
257
588
|
target_port = URI.parse(target_url).port.to_i
|
258
|
-
# if target_scheme == 'http'
|
259
|
-
# use_https = false
|
260
|
-
# else
|
261
|
-
# use_https = true
|
262
|
-
# end
|
263
|
-
|
264
589
|
active_scan_url_arr = []
|
265
|
-
|
590
|
+
|
591
|
+
json_sitemap = get_sitemap(burp_obj: burp_obj, target_url: target_url)
|
266
592
|
json_sitemap.each do |site|
|
267
593
|
json_req = site[:request]
|
268
|
-
|
269
|
-
|
594
|
+
b64_decoded_req = Base64.strict_decode64(json_req)
|
595
|
+
json_path = b64_decoded_req.split[1].to_s.scrub.strip.chomp
|
596
|
+
|
270
597
|
json_http_svc = site[:http_service]
|
271
598
|
json_protocol = json_http_svc[:protocol]
|
272
599
|
json_host = json_http_svc[:host].to_s.scrub.strip.chomp
|
@@ -279,10 +606,16 @@ module PWN
|
|
279
606
|
path: json_path
|
280
607
|
)
|
281
608
|
|
282
|
-
|
283
|
-
|
284
|
-
|
609
|
+
uri_in_scope = in_scope(
|
610
|
+
burp_obj: burp_obj,
|
611
|
+
uri: json_uri
|
612
|
+
)
|
613
|
+
|
614
|
+
puts "Skipping #{json_uri} - not in scope. Check out #{self}.help >> #add_to_scope method" unless uri_in_scope
|
615
|
+
next unless uri_in_scope
|
285
616
|
|
617
|
+
# If the protocol is HTTPS, set use_https to true
|
618
|
+
use_https = false
|
286
619
|
use_https = true if json_protocol == 'https'
|
287
620
|
|
288
621
|
puts "Adding #{json_uri} to Active Scan"
|
@@ -291,7 +624,7 @@ module PWN
|
|
291
624
|
host: json_host,
|
292
625
|
port: json_port,
|
293
626
|
use_https: use_https,
|
294
|
-
request:
|
627
|
+
request: json_req
|
295
628
|
}.to_json
|
296
629
|
# Kick off an active scan for each given page in the json_sitemap results
|
297
630
|
rest_browser.post("http://#{pwn_burp_api}/scan/active", post_body, content_type: 'application/json')
|
@@ -299,14 +632,14 @@ module PWN
|
|
299
632
|
|
300
633
|
# Wait for scan completion
|
301
634
|
scan_queue = rest_browser.get("http://#{pwn_burp_api}/scan/active")
|
302
|
-
json_scan_queue = JSON.parse(scan_queue)
|
635
|
+
json_scan_queue = JSON.parse(scan_queue, symbolize_names: true)
|
303
636
|
scan_queue_total = json_scan_queue.count
|
304
637
|
json_scan_queue.each do |scan_item|
|
305
|
-
this_scan_item_id = scan_item[
|
306
|
-
until scan_item[
|
638
|
+
this_scan_item_id = scan_item[:id]
|
639
|
+
until scan_item[:status] == 'finished'
|
307
640
|
scan_item_resp = rest_browser.get("http://#{pwn_burp_api}/scan/active/#{this_scan_item_id}")
|
308
|
-
scan_item = JSON.parse(scan_item_resp)
|
309
|
-
scan_status = scan_item[
|
641
|
+
scan_item = JSON.parse(scan_item_resp, symbolize_names: true)
|
642
|
+
scan_status = scan_item[:status]
|
310
643
|
puts "Target ID ##{this_scan_item_id} of ##{scan_queue_total}| #{scan_status}"
|
311
644
|
sleep 3
|
312
645
|
end
|
@@ -330,7 +663,7 @@ module PWN
|
|
330
663
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
331
664
|
|
332
665
|
scan_issues = rest_browser.get("http://#{pwn_burp_api}/scanissues")
|
333
|
-
JSON.parse(scan_issues)
|
666
|
+
JSON.parse(scan_issues, symbolize_names: true)
|
334
667
|
rescue StandardError => e
|
335
668
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
336
669
|
raise e
|
@@ -385,7 +718,7 @@ module PWN
|
|
385
718
|
rescue RestClient::BadRequest => e
|
386
719
|
puts e.response
|
387
720
|
rescue StandardError => e
|
388
|
-
|
721
|
+
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
389
722
|
raise e
|
390
723
|
end
|
391
724
|
|
@@ -536,45 +869,58 @@ module PWN
|
|
536
869
|
browser_type: 'optional - defaults to :firefox. See PWN::Plugins::TransparentBrowser.help for a list of types'
|
537
870
|
)
|
538
871
|
|
539
|
-
|
540
|
-
|
872
|
+
uri_in_scope = #{self}.in_scope(
|
873
|
+
burp_obj: 'required - burp_obj returned by #start method',
|
541
874
|
uri: 'required - URI to determine if in scope'
|
542
875
|
)
|
543
876
|
|
544
|
-
#{self}.
|
545
|
-
burp_obj: 'required - burp_obj returned by #start method'
|
877
|
+
json_in_scope = #{self}.add_to_scope(
|
878
|
+
burp_obj: 'required - burp_obj returned by #start method',
|
879
|
+
target_url: 'required - target url to add to scope'
|
546
880
|
)
|
547
881
|
|
548
|
-
#{self}.
|
882
|
+
#{self}.enable_proxy(
|
549
883
|
burp_obj: 'required - burp_obj returned by #start method'
|
550
884
|
)
|
551
885
|
|
552
|
-
|
886
|
+
#{self}.disable_proxy(
|
553
887
|
burp_obj: 'required - burp_obj returned by #start method'
|
554
888
|
)
|
555
889
|
|
556
|
-
|
557
|
-
burp_obj: 'required - burp_obj returned by #start method'
|
890
|
+
json_sitemap = #{self}.get_sitemap(
|
891
|
+
burp_obj: 'required - burp_obj returned by #start method',
|
892
|
+
target_url: 'optional - target URL to filter sitemap results (defaults to entire sitemap)'
|
558
893
|
)
|
559
894
|
|
560
|
-
|
895
|
+
json_sitemap = #{self}.add_to_sitemap(
|
561
896
|
burp_obj: 'required - burp_obj returned by #start method',
|
562
|
-
|
563
|
-
|
564
|
-
enabled: 'optional - enable the listener (defaults to true)'
|
897
|
+
sitemap: 'required - sitemap hash to add',
|
898
|
+
debug: 'optional - boolean to enable sitemap debugging (default: false)'
|
565
899
|
)
|
566
900
|
|
567
|
-
|
901
|
+
Example:
|
902
|
+
json_sitemap = #{self}.add_to_sitemap(
|
568
903
|
burp_obj: 'required - burp_obj returned by #start method',
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
904
|
+
sitemap: {
|
905
|
+
request: 'base64_encoded_request_string',
|
906
|
+
response: 'base64_encoded_response_string',
|
907
|
+
highlight: 'NONE'||'RED'||'ORANGE'||'YELLOW'||'GREEN'||'CYAN'||'BLUE'||'PINK'||'MAGENTA'||'GRAY',
|
908
|
+
comment: 'optional comment for the sitemap entry',
|
909
|
+
http_service: {
|
910
|
+
host: 'example.com',
|
911
|
+
port: 80,
|
912
|
+
protocol: 'http'
|
913
|
+
}
|
914
|
+
}
|
573
915
|
)
|
574
916
|
|
575
|
-
#{self}.
|
917
|
+
json_sitemap = #{self}.import_openapi_to_sitemap(
|
576
918
|
burp_obj: 'required - burp_obj returned by #start method',
|
577
|
-
|
919
|
+
openapi_spec: 'required - path to OpenAPI JSON specification file',
|
920
|
+
additional_http_headers: 'optional - hash of additional HTTP headers to include in requests (default: {})',
|
921
|
+
debug: 'optional - boolean to enable debug logging (default: false)',
|
922
|
+
highlight: 'optional - highlight color for the sitemap entry (default: \"NONE\")',
|
923
|
+
comment: 'optional - comment for the sitemap entry (default: \"\")',
|
578
924
|
)
|
579
925
|
|
580
926
|
active_scan_url_arr = #{self}.invoke_active_scan(
|