# frozen_string_literal: true require 'base64' require 'json' require 'socket' require 'uri' module PWN module Plugins # This plugin was created to interact w/ Burp Suite Pro in headless mode to kick off spidering/live scanning module BurpSuite # Supported Method Parameters:: # burp_obj = PWN::Plugins::BurpSuite.start( # burp_jar_path: 'required - path of burp suite pro jar file', # headless: 'optional - run burp headless if set to true', # browser_type: 'optional - defaults to :firefox. See PWN::Plugins::TransparentBrowser.help for a list of types', # target_config: 'optional - path to burp suite pro target config JSON file' # ) public_class_method def self.start(opts = {}) burp_jar_path = opts[:burp_jar_path] raise 'Invalid path to burp jar file. Please check your spelling and try again.' unless File.exist?(burp_jar_path) burp_root = File.dirname(burp_jar_path) browser_type = if opts[:browser_type].nil? :firefox else opts[:browser_type] end target_config = opts[:target_config] if opts[:headless] # burp_cmd_string = "java -Xmx4G -Djava.awt.headless=true -classpath #{burp_root}/burpbuddy.jar:#{burp_jar_path} burp.StartBurp" burp_cmd_string = "java -Xmx4G -Djava.awt.headless=true -classpath #{burp_root}/burpbuddy.jar -jar #{burp_jar_path}" else # burp_cmd_string = "java -Xmx4G -classpath #{burp_root}/burpbuddy.jar:#{burp_jar_path} burp.StartBurp" burp_cmd_string = "java -Xmx4G -classpath #{burp_root}/burpbuddy.jar -jar #{burp_jar_path}" end burp_cmd_string = "#{burp_cmd_string} --config-file=#{target_config}" if target_config && File.exist?(target_config) # Construct burp_obj burp_obj = {} burp_obj[:pid] = Process.spawn(burp_cmd_string) browser_obj1 = PWN::Plugins::TransparentBrowser.open(browser_type: :rest) rest_browser = browser_obj1[:browser] # random_mitm_port = PWN::Plugins::Sock.get_random_unused_port # random_bb_port = random_mitm_port # random_bb_port = PWN::Plugins::Sock.get_random_unused_port while random_bb_port == random_mitm_port burp_obj[:mitm_proxy] = '127.0.0.1:8080' burp_obj[:burpbuddy_api] = '127.0.0.1:8001' burp_obj[:rest_browser] = rest_browser # Proxy always listens on localhost...use SSH tunneling if remote access is required browser_obj2 = PWN::Plugins::TransparentBrowser.open( browser_type: browser_type, proxy: "http://#{burp_obj[:mitm_proxy]}" ) burp_obj[:burp_browser] = browser_obj2 # Wait for TCP 8001 to open prior to returning burp_obj loop do s = TCPSocket.new('127.0.0.1', 8001) s.close break rescue Errno::ECONNREFUSED print '.' sleep 3 next end burp_obj rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # uri_in_scope_bool = PWN::Plugins::BurpSuite.uri_in_scope( # target_config: 'required - path to burp suite pro target config JSON file', # uri: 'required - URI to determine if in scope' # ) public_class_method def self.uri_in_scope(opts = {}) target_config = opts[:target_config] raise 'ERROR: target_config does not exist' unless File.exist?(target_config) uri = opts[:uri] raise 'ERROR: uri parameter is required' if uri.nil? target_config_json = JSON.parse( File.read(target_config), symbolize_names: true ) out_of_scope = target_config_json[:target][:scope][:exclude] out_of_scope_arr = out_of_scope.select do |os| URI.parse(uri).scheme =~ /#{os[:protocol]}/ && URI.parse(uri).host =~ /#{os[:host]}/ && (URI.parse(uri).path =~ /#{os[:file]}/ || URI.parse(uri).path == '') end return false unless out_of_scope_arr.empty? in_scope = target_config_json[:target][:scope][:include] in_scope_arr = in_scope.select do |is| URI.parse(uri).scheme =~ /#{is[:protocol]}/ && URI.parse(uri).host =~ /#{is[:host]}/ && (URI.parse(uri).path =~ /#{is[:file]}/ || URI.parse(uri).path == '') end return false if in_scope_arr.empty? true rescue StandardError => e raise e end # Supported Method Parameters:: # PWN::Plugins::BurpSuite.enable_proxy( # burp_obj: 'required - burp_obj returned by #start method' # ) public_class_method def self.enable_proxy(opts = {}) burp_obj = opts[:burp_obj] rest_browser = burp_obj[:rest_browser] burpbuddy_api = burp_obj[:burpbuddy_api] enable_resp = rest_browser.post("http://#{burpbuddy_api}/proxy/intercept/enable", nil) rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # PWN::Plugins::BurpSuite.disable_proxy( # burp_obj: 'required - burp_obj returned by #start method' # ) public_class_method def self.disable_proxy(opts = {}) burp_obj = opts[:burp_obj] rest_browser = burp_obj[:rest_browser] burpbuddy_api = burp_obj[:burpbuddy_api] disable_resp = rest_browser.post("http://#{burpbuddy_api}/proxy/intercept/disable", nil) rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # json_sitemap = PWN::Plugins::BurpSuite.get_current_sitemap( # burp_obj: 'required - burp_obj returned by #start method' # ) public_class_method def self.get_current_sitemap(opts = {}) burp_obj = opts[:burp_obj] rest_browser = burp_obj[:rest_browser] burpbuddy_api = burp_obj[:burpbuddy_api] sitemap = rest_browser.get("http://#{burpbuddy_api}/sitemap", content_type: 'application/json; charset=UTF8') JSON.parse(sitemap) rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # active_scan_url_arr = PWN::Plugins::BurpSuite.invoke_active_scan( # burp_obj: 'required - burp_obj returned by #start method', # target_url: 'required - target url to scan in sitemap (should be loaded & authenticated w/ burp_obj[:burp_browser])' # ) public_class_method def self.invoke_active_scan(opts = {}) burp_obj = opts[:burp_obj] rest_browser = burp_obj[:rest_browser] burpbuddy_api = burp_obj[:burpbuddy_api] target_url = opts[:target_url].to_s.scrub.strip.chomp target_scheme = URI.parse(target_url).scheme target_domain_name = URI.parse(target_url).host target_port = URI.parse(target_url).port.to_i if target_scheme == 'http' use_https = false else use_https = true end active_scan_url_arr = [] json_sitemap = get_current_sitemap(burp_obj: burp_obj) json_sitemap.each do |site| json_http_svc = site['http_service'] json_req = site['request'] json_protocol = json_http_svc['protocol'] json_host = json_http_svc['host'].to_s.scrub.strip.chomp json_port = json_http_svc['port'].to_i json_path = json_req['path'] implicit_http_ports_arr = [ 80, 443 ] if implicit_http_ports_arr.include?(json_port) json_uri = "#{json_protocol}//#{json_host}#{json_path}" else json_uri = "#{json_protocol}//#{json_host}:#{json_port}#{json_path}" end next unless json_host == target_domain_name && json_port == target_port puts "Adding #{json_uri} to Active Scan" active_scan_url_arr.push(json_uri) post_body = "{ \"host\": \"#{json_host}\", \"port\": \"#{json_port}\", \"useHttps\": #{use_https}, \"request\": \"#{json_req['raw']}\" }" # Kick off an active scan for each given page in the json_sitemap results rest_browser.post("http://#{burpbuddy_api}/scan/active", post_body, content_type: 'application/json') end # Wait for scan completion scan_queue = rest_browser.get("http://#{burpbuddy_api}/scan/active") json_scan_queue = JSON.parse(scan_queue) scan_queue_total = json_scan_queue.count json_scan_queue.each do |scan_item| this_scan_item_id = scan_item['id'] until scan_item['status'] == 'finished' scan_item_resp = rest_browser.get("http://#{burpbuddy_api}/scan/active/#{this_scan_item_id}") scan_item = JSON.parse(scan_item_resp) scan_status = scan_item['status'] puts "Target ID ##{this_scan_item_id} of ##{scan_queue_total}| #{scan_status}" sleep 3 end puts "Target ID ##{this_scan_item_id} of ##{scan_queue_total}| 100% complete\n" end active_scan_url_arr # Return array of targeted URIs to pass to #generate_scan_report method rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # json_scan_issues = PWN::Plugins::BurpSuite.get_scan_issues( # burp_obj: 'required - burp_obj returned by #start method' # ) public_class_method def self.get_scan_issues(opts = {}) burp_obj = opts[:burp_obj] rest_browser = burp_obj[:rest_browser] burpbuddy_api = burp_obj[:burpbuddy_api] scan_issues = rest_browser.get("http://#{burpbuddy_api}/scanissues") JSON.parse(scan_issues) rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # PWN::Plugins::BurpSuite.generate_scan_report( # burp_obj: 'required - burp_obj returned by #start method', # target_url: 'required - target_url passed to #invoke_active_scan method', # report_type: :html|:xml|:both, # output_path: 'required - path to save report results' # ) public_class_method def self.generate_scan_report(opts = {}) burp_obj = opts[:burp_obj] target_url = opts[:target_url] rest_browser = burp_obj[:rest_browser] burpbuddy_api = burp_obj[:burpbuddy_api] report_type = opts[:report_type] # When burpbuddy begins to support XML report generation valid_report_types_arr = %i[ html xml ] raise 'INVALID Report Type' unless valid_report_types_arr.include?(report_type) output_path = opts[:output_path].to_s.scrub scheme = URI.parse(target_url).scheme host = URI.parse(target_url).host port = URI.parse(target_url).port implicit_http_ports_arr = [ 80, 443 ] if implicit_http_ports_arr.include?(port) target_domain = "#{scheme}://#{host}" else target_domain = "#{scheme}://#{host}:#{port}" end report_url = Base64.strict_encode64(target_domain) # Ready scanreport API call in burpbuddy to support HTML & XML report generation report_resp = rest_browser.get( "http://#{burpbuddy_api}/scanreport/#{report_type.to_s.upcase}/#{report_url}" ) # report_resp = rest_browser.get( # "http://#{burpbuddy_api}/scanreport/#{report_url}" # ) File.open(output_path, 'w') do |f| f.puts(report_resp.body.gsub("\r\n", "\n")) end rescue StandardError => e stop(burp_obj: burp_obj) unless burp_obj.nil? raise e end # Supported Method Parameters:: # PWN::Plugins::BurpSuite.update_burp_jar( # ) public_class_method def self.update_burp_jar # TODO: Do this if PortSwigger ever decides to includes this functionality as a CLI argument. end # Supported Method Parameters:: # PWN::Plugins::BurpSuite.stop( # burp_obj: 'required - burp_obj returned by #start method' # ) public_class_method def self.stop(opts = {}) burp_obj = opts[:burp_obj] browser_obj = burp_obj[:burp_browser] burp_pid = burp_obj[:pid] browser_obj = PWN::Plugins::TransparentBrowser.close(browser_obj: browser_obj) Process.kill('TERM', burp_pid) burp_obj = nil rescue StandardError => e raise e end # Author(s):: 0day Inc. public_class_method def self.authors "AUTHOR(S): 0day Inc. " end # Display Usage for this Module public_class_method def self.help puts "USAGE: burp_obj = #{self}.start( burp_jar_path: 'required - path of burp suite pro jar file', headless: 'optional - run headless if set to true', browser_type: 'optional - defaults to :firefox. See PWN::Plugins::TransparentBrowser.help for a list of types', target_config: 'optional - path to burp suite pro target config JSON file' ) uri_in_scope_bool = #{self}.uri_in_scope( target_config: 'required - path to burp suite pro target config JSON file', uri: 'required - URI to determine if in scope' ) #{self}.enable_proxy( burp_obj: 'required - burp_obj returned by #start method' ) #{self}.disable_proxy( burp_obj: 'required - burp_obj returned by #start method' ) json_sitemap = #{self}.get_current_sitemap( burp_obj: 'required - burp_obj returned by #start method' ) active_scan_url_arr = #{self}.invoke_active_scan( burp_obj: 'required - burp_obj returned by #start method', target_url: 'required - target url to scan in sitemap (should be loaded & authenticated w/ burp_obj[:burp_browser])' ) json_scan_issues = #{self}.get_scan_issues( burp_obj: 'required - burp_obj returned by #start method' ).to_json #{self}.generate_scan_report( burp_obj: 'required - burp_obj returned by #start method', active_scan_url_arr: 'required - active_scan_url_arr returned by #invoke_active_scan method', report_type: :html|:xml, output_path: 'required - path to save report results' ) #{self}.stop( burp_obj: 'required - burp_obj returned by #start method' ) #{self}.authors " end end end end