This patch introduces a really basic RPC service. It is still a long way from its final version

git-svn-id: file:///home/svn/framework3/trunk@5991 4d416f70-5f16-0410-b530-b9f4589650da
This commit is contained in:
HD Moore 2008-12-02 22:09:34 +00:00
parent 8eda1cccbf
commit 71c5175a85
14 changed files with 976 additions and 1 deletions

166
documentation/msfrpc.txt Normal file
View File

@ -0,0 +1,166 @@
[ INTRODUCTION ]
The msfrpcd daemon uses the xmlrpc plugin to provide a remote
interface to the Metasploit Framework. By default, This service
listens on port 55553, uses SSL, and is password protected.
The RPC interface allows access to a minimal set of framework
APIs, covering the core framework, the module set, the job list,
and the session table. These APIs can be used to enumerate
modules, execute them, and interact with the resulting sessions
and jobs.
[ USAGE ]
To activate the RPC interface, launch msfrpcd, or load msfconsole
and load the xmlrpc plugin.
$ ./msfrpcd -P s3cr3tp4ss
- or -
msf> load xmlrpc Pass=s3cr3tp4ss
Once the interface is started, any compatible RPC interface be used
to interact with the service. The 'msfrpc' client provides a Ruby
shell that can be used to talk to the service.
$ ./msfrpc -h server_name -P s3cr3tp4ss
[*] The 'rpc' object holds the RPC client interface
>> rpc.call("core.version")
=> {"version"=>"3.3-dev"}
[ API - AUTH ]
Method: auth.login
Expects: username, password
Returns: { "result" => "success", "token" => "<token>" }
Summary: This method is used by rpc.login() to obtain the session key
(token) which is sent in subsequent requests. This token uniquely
identifies a particular client and can be used by multiple clients,
even after the originating TCP session is closed. The RPC client
object automatically sends this token with all other method calls.
Inactive tokens are destroyed after five minutes of non-use.
[ API - CORE ]
Method: core.version
Expects: none
Returns: { "version" => "<framework-version>" }
[ API - MODULE ]
Method: module.exploits
Method: module.auxiliary
Method: module.payloads
Method. module.encoders
Method: module.nops
Expects: none
Returns: { "modules" => ["module1", "module2", ...] }
Summary: This method is used to obtain a list of available modules
of the specified type. The resulting module namees can be used in
other calls within the module service.
Method: module.info
Expects: module_type, module_name
Returns: { "name" => "<name>", ... }
Summary: This method returns all shared module fields (name, authors,
version, description, etc), but also the list of targets and actions
when appropriate.
Method: module.options
Expects: module_type, module_name
Returns: { "<option_name>" => { "type" => "integer", ... } }
Summary: This method returns a list of all options for a given module,
including advanced and evasion options. The returned hash contains
detailed information about each option, including its type, its
default value, whether it is required, and so on.
Method: module.compatible_payloads
Expects: module_name
Returns: { "payloads" => [ "payload1", "payload2", ... ] }
Summary: This method only works for exploit modules and returns a
list of payloads that are compatible with the specified exploit.
Method: module.execute
Expects: module_type, module_name, options_hash
Returns: { "result" => "success" }
Summary: This method only works for exploit and auxiliary modules
and uses the simplified framework API to launch these modules
with the specified options. Option values should be placed into
the options_hash argument, including items such as PAYLOAD,
TARGET, ACTION, and all required options.
[ API - JOB ]
Method: job.list
Expects: none
Returns: { "<job_id>" => "<job_name>" }
Summary: This method returns a list of running jobs, along with
the name of the job.
Method: job.stop
Expects: job_id
Returns: { "result" => "success" }
Summary: This method kills a specific job by ID
[ API - SESSION ]
Method: session.list
Expects: none
Returns: { "<session_id>" => { "type" => "shell", ... }}
Summary: This method returns a list of active sessions, including
the fields type, tunnel_local, tunnel_peer, via_exploit,
via_payload, and desc.
Method: session.stop
Expects: session_id
Returns: { "result" => "success" }
Summary: This method kills a specific session by ID
Method: session.shell_read
Expects: session_id
Returns: { "data" => "<shell_data>" }
Summary: This method reads any pending output from a session. This
method only works for sessions of type "shell" and does not block.
Method: session.shell_write
Expects: session_id, shell_data
Returns: { "write_count" => "<number_of_bytes_written>" }
Summary: This method writes the specified input into the session.
This method only works for sessions of type "shell" and does not
block.
[ EXCEPTIONS ]
When an error occurs, an exception is thrown on the client side. This
exception will be of class XMLRPC::FaultException and the faultCode
and faultString methods of this exception will contain defailed
information about the problem. Many API calls will raise faultCode
of 404 when the specified item is not found. An unhandled, server
exception will result in a faultCode of 500 on the client side.
[ SECURITY CONSIDERATIONS ]
At this time, the SSL certificate used by the service is
dynamically allocated, making it vulnerable to a man-in-the-middle
attack. Future versions will address this by allowing a certificate
to be generated and verified.
The current implementation passes the username and password for the
RPC service as parameters on the command line. This can lead to
disclosure of the password to other local users on some Unix systems.
The msfrpc and msfrpcd applications change the displayed arguments
as soon as they are launched, but there is still a brief window of
time where another local user may snoop the msfrpcd password. In the
future, the password will be specified via TTY or file.

9
lib/msf/core/rpc.rb Normal file
View File

@ -0,0 +1,9 @@
require "msf/core/rpc/service"
require "msf/core/rpc/client"
require "msf/core/rpc/base"
require "msf/core/rpc/auth"
require "msf/core/rpc/core"
require "msf/core/rpc/session"
require "msf/core/rpc/module"
require "msf/core/rpc/job"

32
lib/msf/core/rpc/auth.rb Normal file
View File

@ -0,0 +1,32 @@
module Msf
module RPC
class Auth < Base
def login(user,pass)
# handle authentication here
fail = true
@users.each do |u|
if(u[0] == user and u[1] == pass)
fail = false
break
end
end
if(fail)
raise ::XMLRPC::FaultException.new(401, "authentication error")
end
token = Rex::Text.rand_text_alphanumeric(32)
@tokens[token] = [user, Time.now.to_i, Time.now.to_i]
{ "result" => "success", "token" => token }
end
def logout(token)
@tokens.delete(token)
{ "result" => "success" }
end
end
end
end

33
lib/msf/core/rpc/base.rb Normal file
View File

@ -0,0 +1,33 @@
module Msf
module RPC
class Base
def initialize(framework,tokens,users)
@framework = framework
@tokens = tokens
@users = users
end
def authenticate(token)
stale = []
@tokens.each_key do |t|
user,ctime,mtime = @tokens[t]
if(mtime + 300 < Time.now.to_i)
stale << t
end
end
stale.each { |t| @tokens.delete(t) }
if(not @tokens[token])
raise ::XMLRPC::FaultException.new(401, "authentication error")
end
@tokens[token][2] = Time.now.to_i
end
end
end
end

View File

@ -0,0 +1,66 @@
require "xmlrpc/client"
require "rex"
module Msf
module RPC
# Loosely based on the XMLRPC::ClientS class
# Reimplemented for Metasploit
class Client < ::XMLRPC::Client
attr_accessor :sock, :token
# Use a TCP socket to do RPC
def initialize(info={})
@buff = ""
self.sock = Rex::Socket::Tcp.create(
'PeerHost' => info[:host],
'PeerPort' => info[:port],
'SSL' => info[:ssl]
)
end
# This override hooks into the RPCXML library
def do_rpc(request,async)
self.sock.put(request + "\x00")
while(not @buff.index("\x00"))
resp = self.sock.get_once
if (not resp and @buff.index("\x00").nil?)
raise RuntimeError, "XMLRPC connection closed"
end
@buff << resp if resp
end
mesg,left = @buff.split("\x00", 2)
@buff = left.to_s
mesg
end
def login(user,pass)
res = self.call("auth.login", user, pass)
if(not (res and res['result'] == "success"))
raise RuntimeError, "authentication failed"
end
self.token = res['token']
true
end
# Prepend the authentication token as the first parameter
# of every call except auth.login. Requires the
def call(meth, *args)
if(meth != "auth.login")
if(not self.token)
raise RuntimeError, "client not authenticated"
end
args.unshift(self.token)
end
super(meth, *args)
end
end
end
end

12
lib/msf/core/rpc/core.rb Normal file
View File

@ -0,0 +1,12 @@
module Msf
module RPC
class Core < Base
def version(token)
authenticate(token)
{ "version" => ::Msf::Framework::Version }
end
end
end
end

27
lib/msf/core/rpc/job.rb Normal file
View File

@ -0,0 +1,27 @@
module Msf
module RPC
class Job < Base
def list(token)
authenticate(token)
res = {}
res['jobs'] = {}
@framework.jobs.each do |j|
res['jobs'][j[0]] = j[1].name
end
res
end
def stop(token,jid)
authenticate(token)
obj = @framework.jobs[jid.to_s]
if(not obj)
raise ::XMLRPC::FaultException.new(404, "no such job")
else
obj.stop
{ "result" => "success" }
end
end
end
end
end

194
lib/msf/core/rpc/module.rb Normal file
View File

@ -0,0 +1,194 @@
module Msf
module RPC
class Module < Base
def exploits(token)
authenticate(token)
{ "modules" => @framework.exploits.keys }
end
def auxiliary(token)
authenticate(token)
{ "modules" => @framework.auxiliary.keys }
end
def payloads(token)
authenticate(token)
{ "modules" => @framework.payloads.keys }
end
def encoders(token)
authenticate(token)
{ "modules" => @framework.encoders.keys }
end
def nops(token)
authenticate(token)
{ "modules" => @framework.nops.keys }
end
def info(token, mtype, mname)
authenticate(token)
m = _find_module(mtype,mname)
res = {}
res['name'] = m.name
res['description'] = m.description
res['license'] = m.license
res['filepath'] = m.file_path
res['version'] = m.version
res['references'] = []
m.references.each do |r|
res['references'] << [r.ctx_id, r.ctx_val]
end
res['authors'] = []
m.each_author do |a|
res['authors'] << a.to_s
end
if(m.type == "exploit")
res['targets'] = {}
m.targets.each_index do |i|
res['targets'][i] = m.targets[i].name
end
if (m.default_target)
res['default_target'] = m.default_target
end
end
if(m.type == "auxiliary")
res['actions'] = {}
m.actions.each_index do |i|
res['actions'][i] = m.actions[i].name
end
if (m.default_action)
res['default_action'] = m.default_action
end
end
res
end
def compatible_payloads(token, mname)
authenticate(token)
m = @framework.exploits[mname]
if(not m)
raise ::XMLRPC::FaultException.new(404, "unknown module")
end
res = {}
res['payloads'] = []
m.compatible_payloads.each do |k|
res['payloads'] << k[0]
end
res
end
def options(token, mtype, mname)
authenticate(token)
m = _find_module(mtype,mname)
res = {}
m.options.each_key do |k|
o = m.options[k]
res[k] = {
'type' => o.type,
'required' => o.required,
'advanced' => o.advanced,
'evasion' => o.evasion,
'desc' => o.desc
}
if(not o.default.nil?)
res[k]['default'] = o.default
end
if(o.enums.length > 1)
res[k]['enums'] = o.enums
end
end
res
end
def execute(token, mtype, mname, opts)
authenticate(token)
begin
mod = _find_module(mtype,mname)
case mtype
when 'exploit'
_run_exploit(mod, opts)
when 'auxiliary'
_run_auxiliary(mod, opts)
when 'payload'
_run_payload(mod, opts)
end
rescue ::Exception => e
$stderr.puts "#{e.class} #{e} #{e.backtrace}"
end
end
protected
def _find_module(mtype,mname)
mod = @framework.modules.create(mname)
if(not mod)
raise ::XMLRPC::FaultException.new(404, "unknown module")
end
mod
end
def _run_exploit(mod, opts)
s = Msf::Simple::Exploit.exploit_simple(mod, {
'Payload' => opts['PAYLOAD'],
'Target' => opts['TARGET'],
'RunAsJob' => true,
'Options' => opts
})
{"result" => "success"}
end
def _run_auxiliary(mod, opts)
Msf::Simple::Auxiliary.run_simple(mod, {
'Action' => opts['ACTION'],
'RunAsJob' => true,
'Options' => opts
})
{"result" => "success"}
end
def _run_payload(mod, opts)
badchars = [opts['BadChars'] || ''].pack("H*")
begin
res = Msf::Simple::Payload.generate_simple(mod, {
'BadChars' => badchars,
'Encoder' => opts['Encoder'],
'NoComment' => true,
'Format' => 'raw',
'Options' => opts
})
{"result" => "success", "payload" => res.unpack("H*")[0]}
rescue ::Exception
raise ::XMLRPC::FaultException.new(500, "failed to generate")
end
end
end
end
end

View File

@ -0,0 +1,75 @@
require "xmlrpc/server"
require "rex"
module Msf
module RPC
class Service < ::XMLRPC::BasicServer
attr_accessor :service, :state
def initialize(srvhost, srvport, ssl=false, cert=nil, ckey=nil)
self.service = Rex::Socket::TcpServer.create(
'LocalHost' => srvhost,
'LocalPort' => srvport,
'SSL' => ssl
)
self.service.on_client_connect_proc = Proc.new { |client|
on_client_connect(client)
}
self.service.on_client_data_proc = Proc.new { |client|
on_client_data(client)
}
self.service.on_client_close_proc = Proc.new { |client|
on_client_close(client)
}
self.state = {}
super()
end
def start
self.state = {}
self.service.start
end
def stop
self.state = {}
self.service.stop
end
def wait
self.service.wait
end
def on_client_close(c)
self.state.delete(c)
end
def on_client_connect(c)
self.state[c] = ""
end
def on_client_data(c)
data = c.get_once(-1)
self.state[c] << data if data
procxml(c)
end
def procxml(c)
while(self.state[c].index("\x00"))
mesg,left = self.state[c].split("\x00", 2)
self.state[c] = left
begin
res = process(mesg)
rescue ::Exception => e
$stderr.puts "ERROR: #{e.class} #{e}"
end
c.put(res+"\x00")
end
end
end
end
end

View File

@ -0,0 +1,65 @@
module Msf
module RPC
class Session < Base
def list(token)
authenticate(token)
res = {}
@framework.sessions.each do |sess|
i,s = sess
res[s.sid] = {
'type' => s.type,
'tunnel_local'=> s.tunnel_local,
'tunnel_peer' => s.tunnel_peer,
'via_exploit' => s.via_exploit,
'via_payload' => s.via_payload,
'desc' => s.desc
}
end
res
end
def stop(token, sid)
authenticate(token)
s = _find_session(sid)
s.kill
{ "result" => "success" }
end
def shell_read(token, sid)
authenticate(token)
s = _find_session(sid)
if(s.type != "shell")
raise ::XMLRPC::FaultException.new(403, "session is not a shell")
end
if(not s.rstream.has_read_data?(0))
{ "data" => "" }
else
{ "data" => s.read_shell }
end
end
def shell_write(token, sid, data)
authenticate(token)
s = _find_session(sid)
if(s.type != "shell")
raise ::XMLRPC::FaultException.new(403, "session is not a shell")
end
{ "write_count" => s.write_shell(data) }
end
protected
def _find_session(sid)
s = @framework.sessions[sid.to_i]
if(not s)
raise ::XMLRPC::FaultException.new(404, "unknown session")
end
s
end
end
end
end

View File

@ -104,7 +104,7 @@ begin
length = 16384 unless length
begin
return sslsock.read(length)
return sslsock.sysread(length)
rescue EOFError, ::Errno::EPIPE
return nil
end

78
msfrpc Normal file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env ruby
#
# This user interface allows users to interact with a remote framework
# instance through a RPCXML socket.
#
msfbase = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
$:.unshift(File.join(File.dirname(msfbase), 'lib'))
$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']
require 'msf/core/rpc'
require 'rex/ui'
# Declare the argument parser for msfxmld
arguments = Rex::Parser::Arguments.new(
"-h" => [ true, "Connect to this IP address" ],
"-p" => [ true, "Connect to the specified port instead of 55553" ],
"-U" => [ true, "Specify the username to access msfrpcd" ],
"-P" => [ true, "Specify the password to access msfrpcd" ],
"-S" => [ false, "Disable SSL on the XMLRPC socket" ]
)
opts = {
'User' => 'msf',
'SSL' => true,
'ServerPort' => 55553
}
# Parse command line arguments.
arguments.parse(ARGV) { |opt, idx, val|
case opt
when "-h"
opts['ServerHost'] = val
when "-S"
opts['SSL'] = false
when "-p"
opts['ServerPort'] = val
when '-U'
opts['User'] = val
when '-P'
opts['Pass'] = val
when "-h"
print("\nUsage: #{File.basename(__FILE__)} <options>\n" + arguments.usage)
exit
end
}
if(not opts['ServerHost'])
$stderr.puts "[*] Error: a server IP must be specified (-h)"
$stderr.puts arguments.usage
exit(0)
end
if(not opts['Pass'])
$stderr.puts "[*] Error: a password must be specified (-P)"
$stderr.puts arguments.usage
exit(0)
end
$0 = "msfrpc"
rpc = Msf::RPC::Client.new(
:host => opts['ServerHost'],
:port => opts['ServerPort'],
:ssl => opts['SSL']
)
res = rpc.login(opts['User'], opts['Pass'])
puts "[*] The 'rpc' object holds the RPC client interface"
puts ""
while(ARGV.shift)
end
Rex::Ui::Text::IrbShell.new(binding).run

81
msfrpcd Normal file
View File

@ -0,0 +1,81 @@
#!/usr/bin/env ruby
#
# This user interface listens on a port and provides clients that connect to
# it with an XMLRPC interface to the Metasploit Framework.
#
msfbase = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
$:.unshift(File.join(File.dirname(msfbase), 'lib'))
$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB']
require 'msf/base'
require 'msf/ui'
# Declare the argument parser for msfrpcd
arguments = Rex::Parser::Arguments.new(
"-a" => [ true, "Bind to this IP address instead of loopback" ],
"-p" => [ true, "Bind to this port instead of 55553" ],
"-U" => [ true, "Specify the username to access msfrpcd" ],
"-P" => [ true, "Specify the password to access msfrpcd" ],
"-S" => [ false, "Disable SSL on the XMLRPC socket" ],
"-f" => [ false, "Run the daemon in the foreground" ],
"-h" => [ false, "Help banner" ])
opts = {
'RunInForeground' => true,
'SSL' => true,
'ServerHost' => '0.0.0.0',
'ServerPort' => 55553
}
foreground = false
# Parse command line arguments.
arguments.parse(ARGV) { |opt, idx, val|
case opt
when "-a"
opts['ServerHost'] = val
when "-S"
opts['SSL'] = false
when "-p"
opts['ServerPort'] = val
when '-U'
opts['User'] = val
when '-P'
opts['Pass'] = val
when "-f"
foreground = true
when "-h"
print("\nUsage: #{File.basename(__FILE__)} <options>\n" + arguments.usage)
exit
end
}
if(not opts['Pass'])
$stderr.puts "[*] Error: a password must be specified (-P)"
exit(0)
end
$0 = "msfrpcd"
$stderr.puts "[*] XMLRPC starting on #{opts['ServerHost']}:#{opts['ServerPort']} (#{opts['SSL'] ? "SSL" : "NO SSL"})..."
# Create an instance of the framework
$framework = Msf::Simple::Framework.create
$stderr.puts "[*] XMLRPC initializing..."
# Fork into the background if requested
begin
if (not foreground)
$stderr.puts "[*] XMLRPC backgrounding..."
exit(0) if Process.fork()
end
rescue ::NotImplementedError
$stderr.puts "[*] Background mode is not available on this platform"
end
# Run the plugin instance in the foreground.
$framework.plugins.load('xmlrpc', opts).run

137
plugins/xmlrpc.rb Normal file
View File

@ -0,0 +1,137 @@
#!/usr/bin/env ruby
#
# This plugin provides an msf daemon interface that spawns a listener on a
# defined port (default 55554) and gives each connecting client its own
# console interface. These consoles all share the same framework instance.
# Be aware that the console instance that spawns on the port is entirely
# unauthenticated, so realize that you have been warned.
#
require "msf/core/rpc"
require "fileutils"
module Msf
###
#
# This class implements the msfd plugin interface.
#
###
class Plugin::XMLRPC < Msf::Plugin
#
# The default local hostname that the server listens on.
#
DefaultHost = "127.0.0.1"
#
# The default local port that the server listens on.
#
DefaultPort = 55553
#
# ServerPort
#
# The local port to listen on for connections. The default is 8888
#
def initialize(framework, opts)
super
host = opts['ServerHost'] || DefaultHost
port = opts['ServerPort'] || DefaultPort
ssl = (opts['SSL'] and opts['SSL'].to_s =~ /^[ty]/i) ? true : false
cert = opts['SSLCert']
ckey = opts['SSLKey']
user = opts['User'] || "msf"
pass = opts['Pass'] || ::Rex::Text.rand_text_alphanumeric(8)
print_status(" XMLRPC Service: #{host}:#{port} #{ssl ? " (SSL)" : ""}")
print_status("XMLRPC Username: #{user}")
print_status("XMLRPC Password: #{pass}")
@users = [ [user,pass] ]
self.server = ::Msf::RPC::Service.new(host,port,ssl,cert,ckey)
# If the run in foreground flag is not specified, then go ahead and fire
# it off in a worker thread.
if (opts['RunInForeground'] != true)
Thread.new {
run
}
end
end
#
# Returns 'xmlrpc'
#
def name
"xmlrpc"
end
#
# Returns the plugin description.
#
def desc
"Provides a XMLRPC interface over a listening TCP port."
end
#
# The meat of the plugin, sets up handlers for requests
#
def run
# Initialize the list of authenticated sessions
@tokens = {}
args = [framework,@tokens,@users]
# Add handlers for every class
self.server.add_handler(::XMLRPC::iPIMethods("auth"),
::Msf::RPC::Auth.new(*args)
)
self.server.add_handler(::XMLRPC::iPIMethods("core"),
::Msf::RPC::Core.new(*args)
)
self.server.add_handler(::XMLRPC::iPIMethods("session"),
::Msf::RPC::Session.new(*args)
)
self.server.add_handler(::XMLRPC::iPIMethods("job"),
::Msf::RPC::Job.new(*args)
)
self.server.add_handler(::XMLRPC::iPIMethods("module"),
::Msf::RPC::Module.new(*args)
)
# Set the default/catch-all handler
self.server.set_default_handler do |name, *args|
raise ::XMLRPC::FaultException.new(-99, "Method #{name} missing or wrong number of parameters!")
end
# Start the actual service
self.server.start
# Wait for the service to complete
self.server.wait
end
#
# Closes the listener service.
#
def cleanup
self.server.stop if self.server
self.server = nil
end
#
# The XMLRPC instance.
#
attr_accessor :server
end
end