#!/usr/bin/env ruby # frozen_string_literal: false require 'pwn' require 'optparse' require 'uri' require 'cgi' require 'htmlentities' require 'faker' require 'json' opts = {} OptionParser.new do |options| options.banner = "USAGE: #{$PROGRAM_NAME} [opts] " options.on('-uURL', '--target-url=URL', '') do |t| opts[:target_url] = t end options.on('-wFILE', '--word-list=FILE', '') do |w| opts[:wordlist] = w end options.on('-aPATTERN', '--append=PATTERN', '') do |a| opts[:append_pattern] = a end options.on('-pPROXY', '--proxy=PROXY', '') do |p| opts[:proxy] = p end options.on('-tTHREADS', '--max-threads=THREADS', '') do |t| opts[:max_threads] = t end options.on('-rHEADERS', '--request-headers=HEADERS', '') do |h| opts[:http_request_headers] = h end options.on('-ICODES', '--include-response-codes=CODES', '') do |i| opts[:include_http_response_codes] = i end options.on('-ECODES', '--exclude-response-codes=CODES', '') do |e| opts[:exclude_http_response_codes] = e end options.on('-dDIR', '--dir-path=DIR', '') do |w| opts[:wordlist] = w end options.on('-nREPORTNAME', '--report-name=REPORTNAME', '--")>') do |n| opts[:report_name] = n end options.on('-s', '--[no-]start-reporting-server', '') do |s| opts[:start_reporting_server] = s end end.parse! if opts.empty? puts `#{$PROGRAM_NAME} --help` exit 1 end def request_path(opts = {}) target_url = opts[:target_url] proxy = opts[:proxy] http_request_headers = opts[:http_request_headers] wordlist_line = opts[:wordlist_line] http_method = opts[:http_method] rest_client_resp_hash = {} begin print '.' http_uri = "#{target_url}/#{wordlist_line}" if proxy browser_obj = PWN::Plugins::TransparentBrowser.open( browser_type: :rest, proxy: proxy ) else browser_obj = PWN::Plugins::TransparentBrowser.open( browser_type: :rest ) end rest_client = browser_obj[:browser]::Request headers = { user_agent: Faker::Internet.user_agent } if http_request_headers headers = JSON.parse( http_request_headers, symbolize_names: true ) end response = rest_client.execute( method: http_method, url: http_uri, headers: headers, verify_ssl: false ) rest_client_resp_hash = { request_timestamp: Time.now.strftime('%Y-%m-%d_%H-%M-%S'), http_uri: http_uri, http_method: http_method, http_resp_code: response.code, http_resp_length: response.body.length, http_resp_headers: JSON.pretty_generate(response.headers), http_resp: "#{response.body[0..300]}..." } rescue Errno::ECONNREFUSED raise 'ERROR: Connection(s) Refused. Try lowering the --max-threads value.' rescue Errno::ECONNRESET, NoMethodError, OpenSSL::SSL::SSLError, RestClient::Exceptions::ReadTimeout, RestClient::Exceptions::OpenTimeout, RestClient::ServerBrokeConnection, SOCKSError => e # May be best to switch Tor channel if SOCKSError is rescued rest_client_resp_hash = { request_timestamp: Time.now.strftime('%Y-%m-%d_%H-%M-%S'), http_uri: http_uri, http_method: http_method, http_resp_code: e.class, http_resp_length: 'N/A', http_resp_headers: 'N/A', http_resp: "ERROR: #{e.message}" } rescue RestClient::ExceptionWithResponse => e if e.respond_to?(:response) rest_client_resp_hash = { request_timestamp: Time.now.strftime('%Y-%m-%d_%H-%M-%S'), http_uri: http_uri, http_method: http_method, http_resp_code: e.response.code, http_resp_length: e.response.body.length, http_resp_headers: JSON.pretty_generate(e.response.headers), http_resp: "#{e.response.body[0..300]}..." } else resp_client_resp_hash = { request_timestamp: Time.now.strftime('%Y-%m-%d_%H-%M-%S'), http_uri: http_uri, http_method: http_method, http_resp_code: 'N/A', http_resp_length: 'N/A', http_resp_headers: 'N/A', http_resp: 'N/A' } end rescue URI::InvalidURIError url_encoded_wordlist_arr = [] wordlist_line.split('/').each do |path| url_encoded_wordlist_arr.push(CGI.escape(path)) end wordlist_line = url_encoded_wordlist_arr.join('/') retry rescue RestClient::TooManyRequests sleep 60 ensure browser_obj = PWN::Plugins::TransparentBrowser.close( browser_obj: browser_obj ) end rest_client_resp_hash end begin pwn_provider = 'ruby-gem' # pwn_provider = ENV.fetch('PWN_PROVIDER') if ENV.keys.select { |s| s == 'PWN_PROVIDER' }.any? pwn_provider = ENV.fetch('PWN_PROVIDER') if ENV.keys.any? { |s| s == 'PWN_PROVIDER' } $stdout.sync = true target_url = opts[:target_url] parsed_target_url = URI.parse(target_url) wordlist = opts[:wordlist] raise "ERROR: #{wordlist} Does Not Exist." unless File.exist?(wordlist) append_pattern = opts[:append_pattern] proxy = opts[:proxy] max_threads = opts[:max_threads] max_threads ||= 25 http_request_headers = opts[:http_request_headers] include_http_response_codes = opts[:include_http_response_codes] include_http_response_codes = include_http_response_codes.delete("\s").split(',') if include_http_response_codes exclude_http_response_codes = opts[:exclude_http_response_codes] exclude_http_response_codes = exclude_http_response_codes.delete("\s").split(',') if exclude_http_response_codes raise 'ERROR: Flags --include-response-codes and --exclude-response-codes cannot be used together.' if include_http_response_codes && exclude_http_response_codes dir_path = opts[:dir_path] dir_path ||= '.' report_name = opts[:report_name] report_name ||= "#{parsed_target_url.host}-#{File.basename(wordlist)}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}" start_reporting_server = opts[:start_reporting_server] mutex = Mutex.new results_hash = { report_name: HTMLEntities.new.encode( report_name.to_s.scrub.strip.chomp ), data: [] } wordlist_arr = File.readlines(wordlist) PWN::Plugins::ThreadPool.fill( enumerable_array: wordlist_arr, max_threads: max_threads ) do |this_wl_line| wordlist_line = this_wl_line.to_s.scrub.strip.chomp next if wordlist_line.match?(/^#/) http_methods = %i[DELETE GET HEAD OPTIONS PATCH POST PUT TRACE].shuffle http_methods.each do |http_method| rest_client_resp_hash = request_path( target_url: target_url, proxy: proxy, http_request_headers: http_request_headers, wordlist_line: "#{wordlist_line}#{append_pattern}", http_method: http_method ) mutex.synchronize do if include_http_response_codes ret_http_resp_code = rest_client_resp_hash[:http_resp_code].to_s results_hash[:data].push(rest_client_resp_hash) if include_http_response_codes.include?(ret_http_resp_code) elsif exclude_http_response_codes ret_http_resp_code = rest_client_resp_hash[:http_resp_code].to_s results_hash[:data].push(rest_client_resp_hash) unless exclude_http_response_codes.include?(ret_http_resp_code) else results_hash[:data].push(rest_client_resp_hash) end end end end # Generate HTML Report print "#{$PROGRAM_NAME} Generating Report..." PWN::Reports::URIBuster.generate( dir_path: dir_path, results_hash: results_hash ) puts 'complete.' # Start Simple HTTP Server (If Requested) if start_reporting_server listen_port = PWN::Plugins::Sock.get_random_unused_port.to_s if pwn_provider == 'docker' listen_ip = '0.0.0.0' else listen_ip = '127.0.0.1' end puts "For Scan Results Navigate to: http://127.0.0.1:#{listen_port}/#{report_name}.html" Dir.chdir(dir_path) system( 'pwn_simple_http_server', '-i', listen_ip, '-p', listen_port ) end rescue SystemExit, Interrupt puts "\nGoodbye." rescue StandardError => e puts e.backtrace raise e end