pwn 0.5.332 → 0.5.334
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 +3 -2
- data/bin/pwn_burp_suite_pro_active_scan +1 -1
- data/lib/pwn/plugins/burp_suite.rb +429 -143
- data/lib/pwn/plugins/open_api.rb +653 -184
- data/lib/pwn/plugins/transparent_browser.rb +48 -16
- data/lib/pwn/version.rb +1 -1
- data/third_party/pwn_rdoc.jsonl +13 -1
- metadata +19 -5
@@ -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,25 +194,31 @@ 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.
|
204
|
+
# json_sitemap = PWN::Plugins::BurpSuite.get_sitemap(
|
197
205
|
# burp_obj: 'required - burp_obj returned by #start method',
|
198
206
|
# target_url: 'optional - target URL to filter sitemap results (defaults to entire sitemap)'
|
199
207
|
# )
|
200
208
|
|
201
|
-
public_class_method def self.
|
209
|
+
public_class_method def self.get_sitemap(opts = {})
|
202
210
|
burp_obj = opts[:burp_obj]
|
203
211
|
rest_browser = burp_obj[:rest_browser]
|
204
212
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
205
213
|
target_url = opts[:target_url]
|
206
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
|
+
|
207
220
|
sitemap = rest_browser.get(
|
208
|
-
|
221
|
+
rest_call,
|
209
222
|
content_type: 'application/json; charset=UTF8'
|
210
223
|
)
|
211
224
|
|
@@ -215,95 +228,345 @@ module PWN
|
|
215
228
|
raise e
|
216
229
|
end
|
217
230
|
|
218
|
-
# Supported Method Parameters
|
231
|
+
# Supported Method Parameters:
|
219
232
|
# json_sitemap = PWN::Plugins::BurpSuite.add_to_sitemap(
|
220
233
|
# burp_obj: 'required - burp_obj returned by #start method',
|
221
|
-
#
|
222
|
-
#
|
223
|
-
# req_headers: 'optional - array of headers to include in the request (defaults to [])',
|
224
|
-
# req_body: 'optional - body of the request (defaults to empty string)',
|
225
|
-
# highlight: 'optional - highlight color :none|:red|:orange|:yellow|:green|:cyan|:blue|:pink|:magenta|:gray (defaults to :none)',
|
226
|
-
# status_code: 'optional - HTTP status code to use in the response (defaults to 200)',
|
227
|
-
# resp_headers: 'optional - array of response headers to include in the response (defaults to [])',
|
228
|
-
# resp_body: 'optional - body of the response (defaults to empty JSON object "{}")',
|
229
|
-
# comment: 'optional - comment to add to the sitemap entry (defaults to empty string)'
|
234
|
+
# sitemap: 'required - sitemap hash to add',
|
235
|
+
# debug: 'optional - boolean to enable sitemap debugging (default: false)'
|
230
236
|
# )
|
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
|
+
# }
|
231
252
|
|
232
253
|
public_class_method def self.add_to_sitemap(opts = {})
|
233
254
|
burp_obj = opts[:burp_obj]
|
234
255
|
rest_browser = burp_obj[:rest_browser]
|
235
256
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
raise ArgumentError, "Invalid HTTP method: #{method}" unless valid_methods.include?(method)
|
242
|
-
|
243
|
-
req_headers = opts[:req_headers] ||= []
|
244
|
-
req_body = opts[:body].to_s
|
245
|
-
valid_highlights = %i[none red orange yellow green cyan blue pink magenta gray]
|
246
|
-
highlight = opts[:highlight] ||= :none
|
247
|
-
raise ArgumentError, "Invalid highlight color: #{highlight}" unless valid_highlights.include?(highlight)
|
248
|
-
|
249
|
-
status_code = opts[:status_code] ||= 200
|
250
|
-
resp_headers = opts[:resp_headers] ||= []
|
251
|
-
resp_body = opts[:resp_body] ||= '{}'
|
252
|
-
comment = opts[:comment].to_s
|
253
|
-
|
254
|
-
protocol = URI.parse(url).scheme
|
255
|
-
host = URI.parse(url).host
|
256
|
-
path = URI.parse(url).path
|
257
|
-
port = URI.parse(url).port || (url.start_with?('https') ? 443 : 80)
|
258
|
-
|
259
|
-
sitemap_message = {
|
260
|
-
request: {
|
261
|
-
method: method,
|
262
|
-
url: url,
|
263
|
-
path: path,
|
264
|
-
headers: req_headers,
|
265
|
-
body: req_body
|
266
|
-
},
|
267
|
-
response: {
|
268
|
-
statusCode: status_code.to_i,
|
269
|
-
headers: resp_headers,
|
270
|
-
body: resp_body
|
271
|
-
},
|
272
|
-
http_service: {
|
273
|
-
protocol: protocol,
|
274
|
-
host: host,
|
275
|
-
port: port
|
276
|
-
},
|
277
|
-
highlight: highlight,
|
278
|
-
comment: comment
|
279
|
-
}
|
280
|
-
|
281
|
-
RestClient.post(
|
257
|
+
sitemap = opts[:sitemap] ||= {}
|
258
|
+
debug = opts[:debug] || false
|
259
|
+
|
260
|
+
# Send POST request to /sitemap
|
261
|
+
response = RestClient.post(
|
282
262
|
"#{pwn_burp_api}/sitemap",
|
283
|
-
|
284
|
-
content_type: 'application/json; charset=
|
263
|
+
sitemap.to_json,
|
264
|
+
content_type: 'application/json; charset=UTF-8'
|
285
265
|
)
|
266
|
+
|
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}"
|
286
277
|
rescue StandardError => e
|
287
|
-
|
278
|
+
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
288
279
|
raise e
|
289
280
|
end
|
290
281
|
|
291
|
-
# Supported Method Parameters
|
292
|
-
#
|
282
|
+
# Supported Method Parameters:
|
283
|
+
# json_sitemap = PWN::Plugins::BurpSuite.import_openapi_to_sitemap(
|
293
284
|
# burp_obj: 'required - burp_obj returned by #start method',
|
294
|
-
#
|
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)'
|
295
290
|
# )
|
296
|
-
|
297
|
-
public_class_method def self.add_to_scope(opts = {})
|
291
|
+
public_class_method def self.import_openapi_to_sitemap(opts = {})
|
298
292
|
burp_obj = opts[:burp_obj]
|
299
|
-
|
300
|
-
rest_browser = burp_obj[:rest_browser]
|
301
|
-
pwn_burp_api = burp_obj[:pwn_burp_api]
|
293
|
+
raise 'ERROR: burp_obj parameter is required' unless burp_obj.is_a?(Hash)
|
302
294
|
|
303
|
-
|
295
|
+
openapi_spec = opts[:openapi_spec]
|
296
|
+
raise 'ERROR: openapi_spec parameter not found' unless File.exist?(openapi_spec)
|
304
297
|
|
305
|
-
|
306
|
-
|
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
|
307
570
|
rescue StandardError => e
|
308
571
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
309
572
|
raise e
|
@@ -323,18 +586,14 @@ module PWN
|
|
323
586
|
target_scheme = URI.parse(target_url).scheme
|
324
587
|
target_host = URI.parse(target_url).host
|
325
588
|
target_port = URI.parse(target_url).port.to_i
|
326
|
-
# if target_scheme == 'http'
|
327
|
-
# use_https = false
|
328
|
-
# else
|
329
|
-
# use_https = true
|
330
|
-
# end
|
331
|
-
|
332
589
|
active_scan_url_arr = []
|
333
|
-
|
590
|
+
|
591
|
+
json_sitemap = get_sitemap(burp_obj: burp_obj, target_url: target_url)
|
334
592
|
json_sitemap.each do |site|
|
335
593
|
json_req = site[:request]
|
336
|
-
|
337
|
-
|
594
|
+
b64_decoded_req = Base64.strict_decode64(json_req)
|
595
|
+
json_path = b64_decoded_req.split[1].to_s.scrub.strip.chomp
|
596
|
+
|
338
597
|
json_http_svc = site[:http_service]
|
339
598
|
json_protocol = json_http_svc[:protocol]
|
340
599
|
json_host = json_http_svc[:host].to_s.scrub.strip.chomp
|
@@ -347,34 +606,48 @@ module PWN
|
|
347
606
|
path: json_path
|
348
607
|
)
|
349
608
|
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
353
616
|
|
617
|
+
# If the protocol is HTTPS, set use_https to true
|
618
|
+
use_https = false
|
354
619
|
use_https = true if json_protocol == 'https'
|
355
620
|
|
356
|
-
|
621
|
+
print "Adding #{json_uri} to Active Scan"
|
357
622
|
active_scan_url_arr.push(json_uri)
|
358
623
|
post_body = {
|
359
624
|
host: json_host,
|
360
625
|
port: json_port,
|
361
626
|
use_https: use_https,
|
362
|
-
request:
|
627
|
+
request: json_req
|
363
628
|
}.to_json
|
364
629
|
# Kick off an active scan for each given page in the json_sitemap results
|
365
|
-
rest_browser.post(
|
630
|
+
resp = rest_browser.post(
|
631
|
+
"http://#{pwn_burp_api}/scan/active",
|
632
|
+
post_body,
|
633
|
+
content_type: 'application/json'
|
634
|
+
)
|
635
|
+
puts " => #{resp.code}"
|
636
|
+
rescue RestClient::ExceptionWithResponse => e
|
637
|
+
puts " => #{e.response.code}"
|
638
|
+
next
|
366
639
|
end
|
367
640
|
|
368
641
|
# Wait for scan completion
|
369
642
|
scan_queue = rest_browser.get("http://#{pwn_burp_api}/scan/active")
|
370
|
-
json_scan_queue = JSON.parse(scan_queue)
|
643
|
+
json_scan_queue = JSON.parse(scan_queue, symbolize_names: true)
|
371
644
|
scan_queue_total = json_scan_queue.count
|
372
645
|
json_scan_queue.each do |scan_item|
|
373
|
-
this_scan_item_id = scan_item[
|
374
|
-
until scan_item[
|
646
|
+
this_scan_item_id = scan_item[:id]
|
647
|
+
until scan_item[:status] == 'finished'
|
375
648
|
scan_item_resp = rest_browser.get("http://#{pwn_burp_api}/scan/active/#{this_scan_item_id}")
|
376
|
-
scan_item = JSON.parse(scan_item_resp)
|
377
|
-
scan_status = scan_item[
|
649
|
+
scan_item = JSON.parse(scan_item_resp, symbolize_names: true)
|
650
|
+
scan_status = scan_item[:status]
|
378
651
|
puts "Target ID ##{this_scan_item_id} of ##{scan_queue_total}| #{scan_status}"
|
379
652
|
sleep 3
|
380
653
|
end
|
@@ -398,7 +671,7 @@ module PWN
|
|
398
671
|
pwn_burp_api = burp_obj[:pwn_burp_api]
|
399
672
|
|
400
673
|
scan_issues = rest_browser.get("http://#{pwn_burp_api}/scanissues")
|
401
|
-
JSON.parse(scan_issues)
|
674
|
+
JSON.parse(scan_issues, symbolize_names: true)
|
402
675
|
rescue StandardError => e
|
403
676
|
stop(burp_obj: burp_obj) unless burp_obj.nil?
|
404
677
|
raise e
|
@@ -604,45 +877,58 @@ module PWN
|
|
604
877
|
browser_type: 'optional - defaults to :firefox. See PWN::Plugins::TransparentBrowser.help for a list of types'
|
605
878
|
)
|
606
879
|
|
607
|
-
|
608
|
-
|
880
|
+
uri_in_scope = #{self}.in_scope(
|
881
|
+
burp_obj: 'required - burp_obj returned by #start method',
|
609
882
|
uri: 'required - URI to determine if in scope'
|
610
883
|
)
|
611
884
|
|
612
|
-
#{self}.
|
613
|
-
burp_obj: 'required - burp_obj returned by #start method'
|
885
|
+
json_in_scope = #{self}.add_to_scope(
|
886
|
+
burp_obj: 'required - burp_obj returned by #start method',
|
887
|
+
target_url: 'required - target url to add to scope'
|
614
888
|
)
|
615
889
|
|
616
|
-
#{self}.
|
890
|
+
#{self}.enable_proxy(
|
617
891
|
burp_obj: 'required - burp_obj returned by #start method'
|
618
892
|
)
|
619
893
|
|
620
|
-
|
894
|
+
#{self}.disable_proxy(
|
621
895
|
burp_obj: 'required - burp_obj returned by #start method'
|
622
896
|
)
|
623
897
|
|
624
|
-
|
625
|
-
burp_obj: 'required - burp_obj returned by #start method'
|
898
|
+
json_sitemap = #{self}.get_sitemap(
|
899
|
+
burp_obj: 'required - burp_obj returned by #start method',
|
900
|
+
target_url: 'optional - target URL to filter sitemap results (defaults to entire sitemap)'
|
626
901
|
)
|
627
902
|
|
628
|
-
|
903
|
+
json_sitemap = #{self}.add_to_sitemap(
|
629
904
|
burp_obj: 'required - burp_obj returned by #start method',
|
630
|
-
|
631
|
-
|
632
|
-
enabled: 'optional - enable the listener (defaults to true)'
|
905
|
+
sitemap: 'required - sitemap hash to add',
|
906
|
+
debug: 'optional - boolean to enable sitemap debugging (default: false)'
|
633
907
|
)
|
634
908
|
|
635
|
-
|
909
|
+
Example:
|
910
|
+
json_sitemap = #{self}.add_to_sitemap(
|
636
911
|
burp_obj: 'required - burp_obj returned by #start method',
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
912
|
+
sitemap: {
|
913
|
+
request: 'base64_encoded_request_string',
|
914
|
+
response: 'base64_encoded_response_string',
|
915
|
+
highlight: 'NONE'||'RED'||'ORANGE'||'YELLOW'||'GREEN'||'CYAN'||'BLUE'||'PINK'||'MAGENTA'||'GRAY',
|
916
|
+
comment: 'optional comment for the sitemap entry',
|
917
|
+
http_service: {
|
918
|
+
host: 'example.com',
|
919
|
+
port: 80,
|
920
|
+
protocol: 'http'
|
921
|
+
}
|
922
|
+
}
|
641
923
|
)
|
642
924
|
|
643
|
-
#{self}.
|
925
|
+
json_sitemap = #{self}.import_openapi_to_sitemap(
|
644
926
|
burp_obj: 'required - burp_obj returned by #start method',
|
645
|
-
|
927
|
+
openapi_spec: 'required - path to OpenAPI JSON specification file',
|
928
|
+
additional_http_headers: 'optional - hash of additional HTTP headers to include in requests (default: {})',
|
929
|
+
debug: 'optional - boolean to enable debug logging (default: false)',
|
930
|
+
highlight: 'optional - highlight color for the sitemap entry (default: \"NONE\")',
|
931
|
+
comment: 'optional - comment for the sitemap entry (default: \"\")',
|
646
932
|
)
|
647
933
|
|
648
934
|
active_scan_url_arr = #{self}.invoke_active_scan(
|