-
Notifications
You must be signed in to change notification settings - Fork 14.1k
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
Arachni plugin #8618
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
require 'rex/proto/arachni/client' | ||
require 'rex/proto/arachni/connection' |
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 |
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( | ||
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 ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any danger in potentially returning incompletely received binary data here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the localhost isnt the Arachni master, this approach blows up:
|
||
@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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.