From b688c3251e589db61bf5a380e361bcff0fc4d5de Mon Sep 17 00:00:00 2001 From: Brandon Perry Date: Mon, 26 Jun 2017 18:49:05 -0500 Subject: [PATCH 1/2] arachni plugin --- lib/rex/proto/arachni.rb | 2 + lib/rex/proto/arachni/client.rb | 92 +++++++++++++++++++ lib/rex/proto/arachni/connection.rb | 128 ++++++++++++++++++++++++++ plugins/arachni.rb | 134 ++++++++++++++++++++++++++++ 4 files changed, 356 insertions(+) create mode 100644 lib/rex/proto/arachni.rb create mode 100644 lib/rex/proto/arachni/client.rb create mode 100644 lib/rex/proto/arachni/connection.rb create mode 100644 plugins/arachni.rb diff --git a/lib/rex/proto/arachni.rb b/lib/rex/proto/arachni.rb new file mode 100644 index 000000000000..64bae3b068ed --- /dev/null +++ b/lib/rex/proto/arachni.rb @@ -0,0 +1,2 @@ +require 'rex/proto/arachni/client' +require 'rex/proto/arachni/connection' diff --git a/lib/rex/proto/arachni/client.rb b/lib/rex/proto/arachni/client.rb new file mode 100644 index 000000000000..05111b900e38 --- /dev/null +++ b/lib/rex/proto/arachni/client.rb @@ -0,0 +1,92 @@ +=begin + + This file is part of the Arachni-RPC Pure project and may be subject to + redistribution and commercial restrictions. Please see the Arachni-RPC Pure + web site for more information on licensing and terms of use. + +=end +module Rex +module Proto +module Arachni + +# Very simple client, essentially establishes a {Connection} and performs +# requests. +# +# @author Tasos Laskos +class Client + + # @param [Hash] options + # @option options [String] :host + # Hostname/IP address. + # @option options [Integer] :port + # Port number. + # @option options [String] :token + # Optional authentication token. + # @option options [String] :ssl_ca + # SSL CA certificate. + # @option options [String] :ssl_pkey + # SSL private key. + # @option options [String] :ssl_cert + # SSL certificate. + def initialize( options ) + @options = options + end + + # @note Will establish a connection if none is available. + # + # Performs an RPC request and returns a response. + # + # @param [String] msg + # RPC message in the form of `handler.method`. + # @param [Array] args + # Collection of arguments to be passed to the method. + # + # @return [Object] + # Response object. + # + # @raise [RuntimeError] + # * If a connection error was encountered the relevant exception will be + # raised. + # * If the response object is a remote exception, one will also be raised + # locally. + def call( msg, *args ) + response = with_connection { |c| c.perform( request( msg, *args ) ) } + handle_exception( response ) + + response['obj'] + end + + private + + def with_connection( &block ) + c = Connection.new( @options ) + + begin + block.call c + ensure + c.close + end + end + + def handle_exception( response ) + return if !(data = response['exception']) + + exception = RuntimeError.new( "#{data['type']}: #{data['message']}" ) + exception.set_backtrace( data['backtrace'] ) + + raise exception + end + + def request( msg, *args ) + { + message: msg, + args: args, + token: @options[:token] + } + end + +end + +end +end +end diff --git a/lib/rex/proto/arachni/connection.rb b/lib/rex/proto/arachni/connection.rb new file mode 100644 index 000000000000..1fac0ed255c3 --- /dev/null +++ b/lib/rex/proto/arachni/connection.rb @@ -0,0 +1,128 @@ +=begin + + This file is part of the Arachni-RPC Pure project and may be subject to + redistribution and commercial restrictions. Please see the Arachni-RPC Pure + web site for more information on licensing and terms of use. + +=end + +require 'openssl' +require 'socket' +require 'zlib' +require 'msgpack' + +module Rex +module Proto +module Arachni + +# Represents an RPC connection, which is basically an OpenSSL socket with +# the ability to serialize/unserialize RPC messages. +# +# @author Tasos Laskos +class Connection + + # @param [Hash] options + # @option options [String] :host + # Hostname/IP address. + # @option options [Integer] :port + # Port number. + # @option options [String] :ssl_ca + # SSL CA certificate. + # @option options [String] :ssl_pkey + # SSL private key. + # @option options [String] :ssl_cert + # SSL certificate. + def initialize( options ) + context = OpenSSL::SSL::SSLContext.new + + if options[:ssl_cert] && options[:ssl_pkey] + context.cert = + OpenSSL::X509::Certificate.new( File.open( options[:ssl_cert] ) ) + + context.key = + OpenSSL::PKey::RSA.new( File.open( options[:ssl_pkey] ) ) + + context.ca_file = options[:ssl_ca] + context.verify_mode = + OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + end + + @socket = OpenSSL::SSL::SSLSocket.new( + TCPSocket.new( options[:host], options[:port] ), + context + ) + @socket.sync_close = true + @socket.connect + end + + # Closes the connection. + def close + @socket.close + end + + # @param [Hash] request + # RPC request message data. + def perform( request ) + send_rcv_object( request ) + end + + private + + def send_rcv_object( obj ) + send_object( obj ) + receive_object + end + + def send_object( obj ) + serialized = serialize( obj ) + @socket.puts( [ serialized.bytesize, serialized ].pack( 'Na*' ) ) + end + + def receive_object + while data = @socket.sysread( 99999 ) + (@buf ||= '') << data + while @buf.size >= 4 + if @buf.size >= 4 + ( size = @buf.unpack( 'N' ).first ) + @buf.slice!(0,4) + + complete = @buf.slice!( 0, size ) + @buf = '' + return unserialize( complete ) + else + break + end + end + end + end + + def serialize( object ) + MessagePack.dump object + end + + def unserialize( object ) + MessagePack.load try_decompress( object ) + end + + # @note Will return the `string` as is if it was not compressed. + # + # @param [String] string + # String to decompress. + # + # @return [String] + # Decompressed string. + def try_decompress( string ) + # Just an ID representing a serialized, empty data structure. + return string if string.size == 1 + + begin + Zlib::Inflate.inflate string + rescue Zlib::DataError + string + end + end + +end + +end +end +end diff --git a/plugins/arachni.rb b/plugins/arachni.rb new file mode 100644 index 000000000000..a23fe8a7633f --- /dev/null +++ b/plugins/arachni.rb @@ -0,0 +1,134 @@ +require 'digest' +require 'rex/proto/arachni' + +module Msf +class Plugin::Arachni < Msf::Plugin + class ArachniCommandDispatcher + include Msf::Ui::Console::CommandDispatcher + + def name + 'Arachni' + end + + def commands + { + 'arachni_connect' => 'Connect to an Arachni RPC instance', + 'arachni_scan' => 'Scan a URL', + 'arachni_scanlog' => 'Get the log for the scan', + 'arachni_savelog' => 'Save the results of the scan to the database' + } + end + + def cmd_arachni_connect(*args) + @dispatcher = Rex::Proto::Arachni::Client.new( + host: args[0] || '127.0.0.1', + port: args[1] ||7331 + ) + + instance_info = @dispatcher.call('dispatcher.dispatch', Rex::Text.rand_text_alpha(8)) + @instance = Rex::Proto::Arachni::Client.new( + host: args[0] || '127.0.0.1', + port: instance_info['port'], + token: instance_info['token'] + ) + end + + def cmd_arachni_scan(*args) + unless @instance + print_error("Please connect to your Arachni RPC instance with arachni_connect.") + return + end + + opts = {} + opts['url'] = args[0] + opts['checks'] = args[1] || '*' + opts['audit'] = {} + opts['audit']['elements'] = ['links', 'forms'] + + @instance.call('service.scan', opts) + + @url = args[0] + end + + def cmd_arachni_scanlog(*args) + unless @instance + print_error("Please connect to your Arachni RPC instance with arachni_connect.") + return + end + + log = @instance.call('service.progress', {"with": "issues"}) + status = @instance.call('service.busy?') + + i = 1 + log["issues"].each do |issue| + print_good(i.to_s + ". " + issue["name"]) + i = i + 1 + end + + print_good("Scan running: " + status.to_s) + end + + def cmd_arachni_savelog(*args) + + unless @instance + print_error("Please connect to your Arachni RPC instance with arachni_connect.") + return + end + + unless @url + print_error("Please start a scan against a web server before trying to save the results.") + return + end + + busy = @instance.call('service.busy?') + + unless !busy + print_error("Please save the scan after it's finished running. Check the status with arachni_scanlog.") + return + end + + log = @instance.call('service.progress', {"with": "issues"}) + + log["issues"].each do |issue| + port = issue["vector"]["action"].split(':')[2] + port = ((issue["vector"]["action"].split(':') == 'http') ? 80 : 443) unless port + p port + vuln_info = {} + vuln_info[:web_site] = issue["vector"]["action"] + vuln_info[:pname] = issue['vector']['affected_input_name'] + vuln_info[:method] = issue['vector']['method'].upcase + vuln_info[:name] = issue['name'] + vuln_info[:category] = 'Arachni' + vuln_info[:host] = issue["vector"]["action"].split('/')[2] + vuln_info[:port] = port + vuln_info[:ssl] = issue["vector"]["action"].split(':') == 'http' ? false : true + vuln_info[:risk] = 'Unknown' + vuln_info[:path] = issue["vector"]["action"] + vuln_info[:params] = issue['request']['parameters'].map{|k,v| [k,v]} + vuln_info[:description] = issue['description'] + vuln_info[:proof] = issue['proof'] + p vuln_info + framework.db.report_web_vuln(vuln_info) + end + end + end + + def initialize(framework, opts) + super + print_status("Arachni plugin loaded.") + add_console_dispatcher(ArachniCommandDispatcher) + end + + def cleanup + remove_console_dispatcher('Arachni') + end + + def name + 'Arachni' + end + + def desc + 'Integrate Arachni with Metasploit' + end +end +end From 4b6997fb359312bdceff190d0cc7ec6ba4ff0c07 Mon Sep 17 00:00:00 2001 From: Brandon Perry Date: Mon, 26 Jun 2017 18:51:58 -0500 Subject: [PATCH 2/2] remove debugging puts --- plugins/arachni.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/arachni.rb b/plugins/arachni.rb index a23fe8a7633f..2be460ab3458 100644 --- a/plugins/arachni.rb +++ b/plugins/arachni.rb @@ -92,7 +92,6 @@ def cmd_arachni_savelog(*args) log["issues"].each do |issue| port = issue["vector"]["action"].split(':')[2] port = ((issue["vector"]["action"].split(':') == 'http') ? 80 : 443) unless port - p port vuln_info = {} vuln_info[:web_site] = issue["vector"]["action"] vuln_info[:pname] = issue['vector']['affected_input_name'] @@ -107,7 +106,6 @@ def cmd_arachni_savelog(*args) vuln_info[:params] = issue['request']['parameters'].map{|k,v| [k,v]} vuln_info[:description] = issue['description'] vuln_info[:proof] = issue['proof'] - p vuln_info framework.db.report_web_vuln(vuln_info) end end