318 lines
10 KiB
Ruby
318 lines
10 KiB
Ruby
##
|
|
#
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
#
|
|
# TODO: SSL Support, Authentication, Listen to localhost only by default
|
|
#
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Exploit::Remote::HttpServer::HTML
|
|
include Msf::Auxiliary::Report
|
|
|
|
HWBRIDGE_API_VERSION = "0.0.4"
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Hardware Bridge Server',
|
|
'Description' => %q{
|
|
This module sets up a web server to bridge communications between
|
|
Metasploit and physically attached hardware.
|
|
Currently this module supports: automotive
|
|
},
|
|
'Author' => [ 'Craig Smith' ],
|
|
'License' => MSF_LICENSE,
|
|
'Actions' =>
|
|
[
|
|
[ 'WebServer' ]
|
|
],
|
|
'PassiveActions' =>
|
|
[
|
|
'WebServer'
|
|
],
|
|
'DefaultAction' => 'WebServer'))
|
|
|
|
@operational_status = 0 # 0=unk, 1=connected, 2=not connected
|
|
@last_errors = {}
|
|
@server_started = Time.new
|
|
@can_interfaces = []
|
|
@pkt_response = {} # Candump returned packets
|
|
@packets_sent = 0
|
|
@last_sent = nil
|
|
end
|
|
|
|
def detect_can
|
|
@can_interfaces = []
|
|
Socket.getifaddrs.each do |i|
|
|
if i.name =~ /^can\d+$/ || i.name =~ /^vcan\d+$/ || i.name =~ /^slcan\d+$/
|
|
@can_interfaces << i.name
|
|
end
|
|
end
|
|
end
|
|
|
|
def get_status
|
|
status = {}
|
|
status["operational"] = @operational_status
|
|
status["hw_specialty"] = {}
|
|
status["hw_capabilities"] = {}
|
|
status["last_10_errors"] = @last_errors # NOTE: no support for this yet
|
|
status["api_version"] = HWBRIDGE_API_VERSION
|
|
status["fw_version"] = "not supported"
|
|
status["hw_version"] = "not supported"
|
|
unless @can_interfaces.empty?
|
|
status["hw_specialty"]["automotive"] = true
|
|
status["hw_capabilities"]["can"] = true
|
|
end
|
|
status["hw_capabilities"]["custom_methods"] = true # To test custom methods
|
|
status
|
|
end
|
|
|
|
def get_statistics
|
|
stats = {}
|
|
stats["uptime"] = Time.now - @server_started
|
|
stats["packet_stats"] = @packets_sent
|
|
stats["last_request"] = @last_sent if @last_sent
|
|
stats["voltage"] = "not supported"
|
|
stats
|
|
end
|
|
|
|
def get_datetime
|
|
{ "system_datetime" => Time.now }
|
|
end
|
|
|
|
def get_timezone
|
|
{ "system_timezone" => Time.now.getlocal.zone }
|
|
end
|
|
|
|
def get_ip_config
|
|
end
|
|
|
|
#
|
|
# Stub fucntion to test custom methods
|
|
# Defines a method "sample_cmd" with one argument "data" which is required
|
|
#
|
|
def get_custom_methods
|
|
m = {}
|
|
m["Methods"] = []
|
|
meth = { "method_name" => "custom/sample_cmd", "method_desc" => "Sample HW test command", "args" => [] }
|
|
arg = { "arg_name" => "data", "arg_type" => "string", "required" => true }
|
|
meth["args"] << arg
|
|
meth["return"] = "string"
|
|
m["Methods"] << meth
|
|
m
|
|
end
|
|
|
|
def get_auto_supported_buses
|
|
detect_can()
|
|
buses = []
|
|
@can_interfaces.each do |can|
|
|
buses << { "bus_name" => can }
|
|
end
|
|
buses
|
|
end
|
|
|
|
# Sends a raw CAN packet
|
|
# bus = string
|
|
# id = hex ID
|
|
# data = string of up to 8 hex bytes
|
|
def cansend(bus, id, data)
|
|
result = {}
|
|
result["Success"] = false
|
|
id = id.to_i(16).to_s(16) # Clean up the HEX
|
|
bytes = data.scan(/../) # Break up data string into 2 char (byte) chunks
|
|
if bytes.size > 8
|
|
print_error("Data section can only contain a max of 8 bytes")
|
|
return result
|
|
end
|
|
`which cansend`
|
|
unless $?.success?
|
|
print_error("cansend from can-utils not found in path")
|
|
return result
|
|
end
|
|
@can_interfaces.each do |can|
|
|
if can == bus
|
|
system("cansend #{bus} #{id}##{bytes.join}")
|
|
@packets_sent += 1
|
|
@last_sent = Time.now.to_i
|
|
result["Success"] = true if $?.success?
|
|
end
|
|
end
|
|
result
|
|
end
|
|
|
|
# Converts candump output to {Packets => [{ ID=> id DATA => [] }]}
|
|
def candump2hash(str_packets)
|
|
hash = {}
|
|
hash["Packets"] = []
|
|
lines = str_packets.split(/\n/)
|
|
lines.each do |line|
|
|
if line =~ /\w+\s+(\w+) \[\d\] (.+)$/
|
|
id = $1
|
|
str_data = $2
|
|
data = str_data.split
|
|
hash["Packets"] << { "ID" => id, "DATA" => data }
|
|
end
|
|
end
|
|
hash
|
|
end
|
|
|
|
def candump(bus, id, timeout, maxpkts)
|
|
$candump_sniffer = Thread.new do
|
|
output = `candump #{bus},#{id}:FFFFFF -T #{timeout} -n #{maxpkts}`
|
|
@pkt_response = candump2hash(output)
|
|
Thread::exit
|
|
end
|
|
end
|
|
|
|
# Sends an ISO-TP style CAN packet and waites for a response or a timeout
|
|
# bus = string
|
|
# srcid = hex id of the sent packet
|
|
# dstid = hex id of the return packets
|
|
# data = string of hex bytes to send
|
|
# OPT = Options
|
|
# timeout = optional int to timeout on lack of response
|
|
# maxpkts = max number of packets to recieve
|
|
# padding = append bytes to end of packet (Doesn't increase reported ISO-TP size)
|
|
# fc = flow control, if true forces flow control packets
|
|
def isotp_send_and_wait(bus, srcid, dstid, data, opt = {})
|
|
result = {}
|
|
result["Success"] = false
|
|
srcid = srcid.to_i(16).to_s(16)
|
|
dstid = dstid.to_i(16).to_s(16)
|
|
timeout = 2000
|
|
maxpkts = 3
|
|
flowcontrol = nil
|
|
padding = nil
|
|
timeout = opt['TIMEOUT'] if opt.key? 'TIMEOUT'
|
|
maxpkts = opt['MAXPKTS'] if opt.key? 'MAXPKTS'
|
|
padding = opt['PADDING'] if opt.key? 'PADDING'
|
|
flowcontrol = opt['FC'] if opt.key? 'FC'
|
|
bytes = data.scan(/../)
|
|
if bytes.size > 8
|
|
print_error("Data section currently has to be less than 8 bytes")
|
|
return result
|
|
else
|
|
sz = "%02x" % bytes.size
|
|
bytes = sz + bytes.join
|
|
end
|
|
if padding && bytes.size < 16 # 16 == 8 bytes because of ascii size
|
|
padding = "%02x" % padding.to_i
|
|
bytes += ([ padding ] * (16 - bytes.size)).join
|
|
end
|
|
# Should we ever require isotpsend for this?
|
|
`which cansend`
|
|
unless $?.success?
|
|
print_error("cansend from can-utils not found in path")
|
|
return result
|
|
end
|
|
@can_interfaces.each do |can|
|
|
if can == bus
|
|
if flowcontrol
|
|
candump(bus, dstid, timeout, 1)
|
|
system("cansend #{bus} #{srcid}##{bytes}")
|
|
@packets_sent += 1
|
|
@last_sent = Time.now.to_i
|
|
result["Success"] = true if $?.success?
|
|
result["Packets"] = []
|
|
$candump_sniffer.join
|
|
unless @pkt_response.empty?
|
|
result = @pkt_response
|
|
if result.key?("Packets") && result["Packets"].size > 0 && result["Packets"][0].key?("DATA")
|
|
if result["Packets"][0]["DATA"][0] == "10"
|
|
system("cansend #{bus} #{srcid}#3000000000000000")
|
|
candump(bus, dstid, timeout, maxpkts)
|
|
@packets_sent += 1
|
|
@last_sent = Time.now.to_i
|
|
$candump_sniffer.join
|
|
unless @pkt_response.empty?
|
|
if @pkt_response.key?("Packets") && @pkt_response["Packets"].size > 0
|
|
result["Packets"] += @pkt_response["Packets"]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
else
|
|
candump(bus, dstid, timeout, maxpkts)
|
|
system("cansend #{bus} #{srcid}##{bytes}")
|
|
@packets_sent += 1
|
|
@last_sent = Time.now.to_i
|
|
result["Success"] = true if $?.success?
|
|
result["Packets"] = []
|
|
$candump_sniffer.join
|
|
unless @pkt_response.empty?
|
|
result = @pkt_response
|
|
end
|
|
end
|
|
end
|
|
end
|
|
result
|
|
|
|
end
|
|
|
|
#
|
|
# This is just a sample method that should show up
|
|
# as sample_cmd in the interface
|
|
#
|
|
def sample_custom_method(data)
|
|
res = {}
|
|
res["value"] = "Succesfully processed: #{data}"
|
|
res
|
|
end
|
|
|
|
def not_supported
|
|
{ "status" => "not supported" }
|
|
end
|
|
|
|
def on_request_uri(cli, request)
|
|
if request.uri =~ /status$/i
|
|
print_status("Sending status...") if datastore['VERBOSE']
|
|
send_response_html(cli, get_status().to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /statistics$/i
|
|
print_status("Sending statistics...") if datastore['VERBOSE']
|
|
send_response_html(cli, get_statistics().to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /settings\/datetime\/get$/i
|
|
print_status("Sending Datetime") if datastore['VERBOSE']
|
|
send_response_html(cli, get_datetime().to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /settings\/timezone\/get$/i
|
|
print_status("Sending Timezone") if datastore['VERBOSE']
|
|
send_response_html(cli, get_timezone().to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /custom_methods$/i
|
|
print_status("Sending custom methods") if datastore['VERBOSE']
|
|
send_response_html(cli, get_custom_methods().to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /custom\/sample_cmd\?data=(\S+)$/
|
|
print_status("Request for custom command with args #{$1}") if datastore['VERBOSE']
|
|
send_response_html(cli, sample_custom_method($1).to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /automotive/i
|
|
if request.uri =~ /automotive\/supported_buses/
|
|
print_status("Sending known buses...") if datastore['VERBOSE']
|
|
send_response_html(cli, get_auto_supported_buses().to_json, { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /automotive\/(\w+)\/cansend\?id=(\w+)&data=(\w+)/
|
|
print_status("Request to send CAN packets for #{$1} => #{$2}##{$3}") if datastore['VERBOSE']
|
|
send_response_html(cli, cansend($1, $2, $3).to_json(), { 'Content-Type' => 'application/json' })
|
|
elsif request.uri =~ /automotive\/(\w+)\/isotpsend_and_wait\?srcid=(\w+)&dstid=(\w+)&data=(\w+)/
|
|
bus = $1; srcid = $2; dstid = $3; data = $4
|
|
print_status("Request to send ISO-TP packet and wait for response #{srcid}##{data} => #{dstid}") if datastore['VERBOSE']
|
|
opt = {}
|
|
opt['TIMEOUT'] = $1 if request.uri =~ /&timeout=(\d+)/
|
|
opt['MAXPKTS'] = $1 if request.uri =~ /&maxpkts=(\d+)/
|
|
opt['PADDING'] = $1 if request.uri =~ /&padding=(\d+)/
|
|
opt['FC'] = true if request.uri =~ /&fc=true/i
|
|
send_response_html(cli, isotp_send_and_wait(bus, srcid, dstid, data, opt).to_json(), { 'Content-Type' => 'application/json' })
|
|
else
|
|
send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })
|
|
end
|
|
else
|
|
send_response_html(cli, not_supported().to_json(), { 'Content-Type' => 'application/json' })
|
|
end
|
|
end
|
|
|
|
def run
|
|
detect_can
|
|
@server_started = Time.now
|
|
exploit
|
|
end
|
|
end
|