Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arachni plugin #8618

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/rex/proto/arachni.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'rex/proto/arachni/client'
require 'rex/proto/arachni/connection'
92 changes: 92 additions & 0 deletions lib/rex/proto/arachni/client.rb
Original file line number Diff line number Diff line change
@@ -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 <tasos.laskos@arachni-scanner.com>
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
128 changes: 128 additions & 0 deletions lib/rex/proto/arachni/connection.rb
Original file line number Diff line number Diff line change
@@ -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 <tasos.laskos@arachni-scanner.com>
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably make sense to use Rex::Socket::TcpSsl for this to allow us to access Arachni installs on the other side of a compromised host (for instance if deploying the scanner as a form of payload for rapid internal web scans of the environment).
Forcing TLS validation may also be a problem in some cases, though optional validation is definitely a good thing (even a good default, just suggesting the option of NO_VERIFY).
Rex Socket SSL client certificate support may help a bit for this.

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 )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to comment this process for intent and functionality - what's being recv'd, unpacked, and unserialized.

(@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any danger in potentially returning incompletely received binary data here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially, I suppose, but I'm not sure of a better way to handle the situation. This is functionally equivalent of checking the first few header bytes to determine if the string is actually zlib-compressed, and, if not, just return the data as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't know if the data is zlib compressed without testing it.

end
end

end

end
end
end
132 changes: 132 additions & 0 deletions plugins/arachni.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the localhost isnt the Arachni master, this approach blows up:

[-] Error while running command arachni_connect: Connection refused - connect(2) for "127.0.0.1" port 7331

Call stack:
/opt/metasploit4/msf4/lib/rex/proto/arachni/connection.rb:51:in `initialize'
/opt/metasploit4/msf4/lib/rex/proto/arachni/connection.rb:51:in `new'
/opt/metasploit4/msf4/lib/rex/proto/arachni/connection.rb:51:in `initialize'
/opt/metasploit4/msf4/lib/rex/proto/arachni/client.rb:62:in `new'
/opt/metasploit4/msf4/lib/rex/proto/arachni/client.rb:62:in `with_connection'
/opt/metasploit4/msf4/lib/rex/proto/arachni/client.rb:53:in `call'
/opt/metasploit4/msf4/plugins/arachni.rb:28:in `cmd_arachni_connect'

@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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above regarding remote masters

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
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']
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