From 60cd04bc7b7f140c4b861709b8d165099b9a0d72 Mon Sep 17 00:00:00 2001 From: Craig Smith Date: Mon, 6 Mar 2017 16:10:14 -0800 Subject: [PATCH] Added module for zstumbler --- lib/msf/core/post/hardware.rb | 1 + lib/msf/core/post/hardware/zigbee/utils.rb | 259 ++++++++++++++++++ .../post/hwbridge/extensions/zigbee/zigbee.rb | 43 +++ .../ui/console/command_dispatcher/zigbee.rb | 48 +++- modules/auxiliary/client/hwbridge/connect.rb | 2 + modules/post/hardware/zigbee/zstumbler.rb | 102 +++++++ tools/hardware/killerbee_msfrelay | 60 +++- 7 files changed, 505 insertions(+), 10 deletions(-) create mode 100644 lib/msf/core/post/hardware/zigbee/utils.rb create mode 100644 modules/post/hardware/zigbee/zstumbler.rb diff --git a/lib/msf/core/post/hardware.rb b/lib/msf/core/post/hardware.rb index 9f6772ad85..c25549dcb4 100644 --- a/lib/msf/core/post/hardware.rb +++ b/lib/msf/core/post/hardware.rb @@ -1,4 +1,5 @@ # -*- coding: binary -*- module Msf::Post::Hardware require 'msf/core/post/hardware/automotive/uds' + require 'msf/core/post/hardware/zigbee/utils' end diff --git a/lib/msf/core/post/hardware/zigbee/utils.rb b/lib/msf/core/post/hardware/zigbee/utils.rb new file mode 100644 index 0000000000..149c992bbc --- /dev/null +++ b/lib/msf/core/post/hardware/zigbee/utils.rb @@ -0,0 +1,259 @@ +# -*- coding: binary -*- +module Msf +class Post +module Hardware +module Zigbee + +module Utils + + ## Constants for packet decoding fields + # Frame Control Field + DOT154_FCF_TYPE_MASK = 0x0007 #: Frame type mask + DOT154_FCF_SEC_EN = 0x0008 #: Set for encrypted payload + DOT154_FCF_FRAME_PND = 0x0010 #: Frame pending + DOT154_FCF_ACK_REQ = 0x0020 #: ACK request + DOT154_FCF_INTRA_PAN = 0x0040 #: Intra-PAN activity + DOT154_FCF_DADDR_MASK = 0x0C00 #: Destination addressing mode mask + DOT154_FCF_VERSION_MASK = 0x3000 #: Frame version + DOT154_FCF_SADDR_MASK = 0xC000 #: Source addressing mask mode + + # Frame Control Field Bit Shifts + DOT154_FCF_TYPE_MASK_SHIFT = 0 #: Frame type mask mode shift + DOT154_FCF_DADDR_MASK_SHIFT = 10 #: Destination addressing mode mask + DOT154_FCF_VERSION_MASK_SHIFT = 12 #: Frame versions mask mode shift + DOT154_FCF_SADDR_MASK_SHIFT = 14 #: Source addressing mask mode shift + + # Address Mode Definitions + DOT154_FCF_ADDR_NONE = 0x0000 #: Not sure when this is used + DOT154_FCF_ADDR_SHORT = 0x0002 #: 4-byte addressing + DOT154_FCF_ADDR_EXT = 0x0003 #: 8-byte addressing + + DOT154_FCF_TYPE_BEACON = 0 #: Beacon frame + DOT154_FCF_TYPE_DATA = 1 #: Data frame + DOT154_FCF_TYPE_ACK = 2 #: Acknowledgement frame + DOT154_FCF_TYPE_MACCMD = 3 #: MAC Command frame + + DOT154_CRYPT_NONE = 0x00 #: No encryption, no MIC + DOT154_CRYPT_MIC32 = 0x01 #: No encryption, 32-bit MIC + DOT154_CRYPT_MIC64 = 0x02 #: No encryption, 64-bit MIC + DOT154_CRYPT_MIC128 = 0x03 #: No encryption, 128-bit MIC + DOT154_CRYPT_ENC = 0x04 #: Encryption, no MIC + DOT154_CRYPT_ENC_MIC32 = 0x05 #: Encryption, 32-bit MIC + DOT154_CRYPT_ENC_MIC64 = 0x06 #: Encryption, 64-bit MIC + DOT154_CRYPT_ENC_MIC128 = 0x07 #: Encryption, 128-bit MIC + + # Retrieves the target Zigbee device. This is typically set by the user via the + # interactive HWBridge command line + # @return [String] Zigbee device ID + def get_target_device + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return + end + return client.zigbee.get_target_device + end + + # Sets the target default Zigbee Device. This command typically isn't called via a script + # Instead the user is expected to set this via the interactive HWBridge commandline + # @param device [String] Zigbee device ID + def set_target_device(device) + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return + end + client.zigbee.set_target_device device + end + + # Sets the Zigbee Channel + # @param device [String] Zigbee device ID + # @param channel [Integer] Channel number, typically 11-25 + def set_channel(device, channel) + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return {} + end + device = client.zigbee.target_device if not device + if not device + print_line("No target device set, use 'target' or specify bus via the options") + return {} + end + client.zigbee.set_channel(device, channel) + end + + # Inject raw packets. Need firmware on the zigbee device that supports transmission. + # @param device [String] Zigbee device ID + # @param data [String] Raw binary data sent as a string + def inject(device, data) + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return {} + end + device = client.zigbee.target_device if not device + if not device + print_line("No target device set, use 'target' or specify bus via the options") + return {} + end + client.zigbee.inject(device, data) + end + + # Recieves data from the Zigbee device + # @param device [String] Zigbee device ID + # @return [String] Binary blob of returned data + def recv(device) + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return {} + end + device = client.zigbee.target_device if not device + if not device + print_line("No target device set, use 'target' or specify bus via the options") + return {} + end + client.zigbee.recv(device) + end + + # Turn off Zigbee receiving + # @param device [String] Zigbee device ID + def sniffer_off(device) + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return {} + end + device = client.zigbee.target_device if not device + if not device + print_line("No target device set, use 'target' or specify bus via the options") + return {} + end + client.zigbee.sniffer_off(device) + end + + # Turn on Zigbee receiving + # @param device [String] Zigbee device ID + def sniffer_on(device) + if not client.zigbee + print_error("Not a Zigbee hwbridge session") + return {} + end + device = client.zigbee.target_device if not device + if not device + print_line("No target device set, use 'target' or specify bus via the options") + return {} + end + client.zigbee.sniffer_on(device) + end + + # Breaks up the packet into different sections. Also provides + # Some decoding information + # @param packet [String] Raw data from recv + # @return [Hash] { PktChop => [Array of data], .. + def dot154_packet_decode(packet) + result = {} + offset = 0 + pktchop = ['', '', '', '', '', '', [], ''] + pktchop[0] = packet[0,2] + # Sequence number + pktchop[1] = packet[2] + # Byte swap + fcf = pktchop[0].reverse.unpack("H*")[0].hex + result["FSF"] = fcf + result["SEQ"] = pktchop[1] + # Check if we are dealing with a beacon frame + if (fcf & DOT154_FCF_TYPE_MASK) == DOT154_FCF_TYPE_BEACON + beacondata = ["", "", "", "", "", "", "", "", "", ""] + # 802.15.4 fields, SPAN and SA + pktchop[4] = packet[3,2] + pktchop[5] = packet[5,2] + result["SPAN_ID"] = pktchop[4].reverse.unpack("H*")[0] + result["SOURCE"] = pktchop[5].reverse.unpack("H*")[0] + offset = 7 + + # Superframe specification + beacondata[0] = packet[offset,2] + result["SUPERFRAME"] = beacondata[0] + offset+=2 + + # GTS data + beacondata[1] = packet[offset] + result["GTS"] = beacondata[1] + offset+=1 + + # Pending address count + beacondata[2] = packet[offset] + result["PENDING_ADDRESS_COUNT"] = beacondata[2] + offset+=1 + + # Protocol ID + beacondata[3] = packet[offset] + result["PROTOCOL_ID"] = beacondata[3] + offset+=1 + + # Stack Profile version + beacondata[4] = packet[offset] + result["STACK_PROFILE"] = beacondata[4] + offset+=1 + + # Capability information + beacondata[5] = packet[offset] + result["CAPABILITY"] = beacondata[5] + offset+=1 + + # Extended PAN ID + beacondata[6] = packet[offset,8] + result["EXT_PAN_ID"] = beacondata[6].reverse.unpack("H*")[0] + offset+=8 + + # TX Offset + beacondata[7] = packet[offset,3] + result["TX_OFFSET"] = beacondata[7] + offset+=3 + + # Update ID + beacondata[8] = packet[offset] + result["UPDATE_ID"] = beacondata[8] + offset+=1 + pktchop[6] = beacondata + result["BEACONDATA"] = beacondata + else + # Not a beacon frame + + # DPAN + pktchop[2] = packet[3,2] + offset = 5 + + # Examine the destination addressing mode + daddr_mask = (fcf & DOT154_FCF_DADDR_MASK) >> 10 + if daddr_mask == DOT154_FCF_ADDR_EXT + pktchop[3] = packet[offset,8] + offset+=8 + elsif daddr_mask == DOT154_FCF_ADDR_SHORT + pktchop[3] = packet[offset,2] + offset+=2 + end + + # Examine the Intra-PAN flag + if (fcf & DOT154_FCF_INTRA_PAN) == 0 + pktchop[4] = packet[offset,2] + offset+=2 + end + + # Examine the source addressing mode + saddr_mask = (fcf & DOT154_FCF_SADDR_MASK) >> 14 + if daddr_mask == DOT154_FCF_ADDR_EXT + pktchop[5] = packet[offset,8] + offset+=8 + elsif daddr_mask == DOT154_FCF_ADDR_SHORT + pktchop[5] = packet[offset,2] + offset+=2 + end + end + # Append remaining payload + pktchop[7] = packet[offset,packet.size] if offset < packet.size + result["PktChop"] = pktchop + return result + end +end + +end +end +end +end diff --git a/lib/rex/post/hwbridge/extensions/zigbee/zigbee.rb b/lib/rex/post/hwbridge/extensions/zigbee/zigbee.rb index 835f9ed9fa..b456b33e86 100644 --- a/lib/rex/post/hwbridge/extensions/zigbee/zigbee.rb +++ b/lib/rex/post/hwbridge/extensions/zigbee/zigbee.rb @@ -28,11 +28,54 @@ class Zigbee < Extension ]) end + # Sets the default target device + # @param device [String] Target Zigbee device ID + def set_target_device(device) + self.target_device = device + end + + # Retrieves the default zigbee device ID + # @return [String] Zigbee device ID + def get_target_device + self.target_device + end + # Gets supported Zigbee Devices # @return [Array] Devices def supported_devices client.send_request("/zigbee/supported_devices") end + + # Sets the channel + # @param dev [String] Device to affect + # @param channel [Integer] Channel number + def set_channel(dev, channel) + client.send_request("/zigbee/#{dev}/set_channel?chan=#{channel}") + end + + def inject(dev, data) + data = Base64.urlsafe_encode64(data) + client.send_request("/zigbee/#{dev}/inject?data=#{data}") + end + + def recv(dev) + data = client.send_request("/zigbee/#{dev}/recv") + if data.size > 0 + data["data"] = Base64.urlsafe_decode64(data["data"]) if data.has_key? "data" + end + data + end + + def sniffer_off(dev) + client.send_request("/zigbee/#{dev}/sniffer_off") + end + + def sniffer_on(dev) + client.send_request("/zigbee/#{dev}/sniffer_on") + end + + attr_accessor :target_device + end end diff --git a/lib/rex/post/hwbridge/ui/console/command_dispatcher/zigbee.rb b/lib/rex/post/hwbridge/ui/console/command_dispatcher/zigbee.rb index d07e567a0e..c0ce03dd72 100644 --- a/lib/rex/post/hwbridge/ui/console/command_dispatcher/zigbee.rb +++ b/lib/rex/post/hwbridge/ui/console/command_dispatcher/zigbee.rb @@ -18,12 +18,21 @@ class Console::CommandDispatcher::Zigbee # def commands all = { - 'supported_devices' => 'Get supported zigbee devices' + 'supported_devices' => 'Get supported zigbee devices', + 'target' => 'Set the target device id', + 'channel' => 'Set the channel' } all end + # Sets the target device both in the UI class and in the base API + # @param device [String] Device ID + def set_target_device(device) + self.target_device = device + client.zigbee.set_target_device device + end + # # Lists all thesupported devices # @@ -38,7 +47,7 @@ class Console::CommandDispatcher::Zigbee print_line("none") return end - self.target_device = devices[0] if devices.size == 1 + set_target_device(devices[0]) if devices.size == 1 str = "Supported Devices: " str += devices.join(', ') str += "\nUse device name to set your desired device, default is: #{self.target_device}" @@ -61,12 +70,43 @@ class Console::CommandDispatcher::Zigbee print_line(device_opts.usage) return when '-d' - self.target_device = val + set_target_device val end end print_line("set target device to #{self.target_device}") end + # + # Sets the channel + # + def cmd_channel(*args) + chan = 11 + dev = self.target_device if self.target_device + xopts = Rex::Parser::Arguments.new( + '-h' => [ false, 'Help Banner' ], + '-d' => [ true, 'Zigbee Device' ], + '-c' => [ true, 'channel number' ] + ) + xopts.parse(args) do |opt, _idx, val| + case opt + when '-h' + print_line("Usage: channel -c \n") + print_line(xopts.usage) + return + when '-d' + dev = val + when '-c' + chan = val.to_i + end + end + if not dev + print_line("You must specify or set a target device") + return + end + client.zigbee.set_channel(dev, chan) + print_line("Device #{dev} channel set to #{chan}") + end + # # Name for this dispatcher # @@ -74,9 +114,7 @@ class Console::CommandDispatcher::Zigbee 'Zigbee' end -private attr_accessor :target_device - end end diff --git a/modules/auxiliary/client/hwbridge/connect.rb b/modules/auxiliary/client/hwbridge/connect.rb index 331af02385..1d2c82c39e 100644 --- a/modules/auxiliary/client/hwbridge/connect.rb +++ b/modules/auxiliary/client/hwbridge/connect.rb @@ -67,6 +67,8 @@ class MetasploitModule < Msf::Auxiliary if (res.code == 200) print_status res.body if datastore["DEBUGJSON"] == true return JSON.parse(res.body) + elsif res.code == 401 + print_error "Access Denied: #{res.body}" end return nil diff --git a/modules/post/hardware/zigbee/zstumbler.rb b/modules/post/hardware/zigbee/zstumbler.rb new file mode 100644 index 0000000000..f7482f21a5 --- /dev/null +++ b/modules/post/hardware/zigbee/zstumbler.rb @@ -0,0 +1,102 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'rex' +require 'msf/core/post/hardware/zigbee/utils' + +class MetasploitModule < Msf::Post + + include Msf::Post::Hardware::Zigbee::Utils + + def initialize(info={}) + super( update_info( info, + 'Name' => 'Sends Beacons to scan for active Zigbee networks', + 'Description' => %q{ Post Module to send beacon signals to the broadcast address while + channel hopping}, + 'License' => MSF_LICENSE, + 'Author' => ['Craig Smith'], + 'Platform' => ['hardware'], + 'SessionTypes' => ['hwbridge'] + )) + register_options([ + OptInt.new('CHANNEL', [false, "Disable channel hopping by forcing a channel", nil]), + OptInt.new('LOOP', [false, "How many times to loop over the channels. -1 is forever", 1]), + OptInt.new('DELAY', [false, "Delay in seconds to listen on each channel", 2]), + OptString.new('DEVICE', [false, "Zigbee device ID, defaults to target device", nil]) + ], self.class) + @seq = 0 + @channel = 11 + @stumbled = {} + @loop_count = 0 + end + + def display_details(routerdata) + stackprofile_map = {0 => "Network Specific", + 1 => "ZigBee Standard", + 2 => "ZigBee Enterprise"} + stackver_map = {0 => "ZigBee Prototype", + 1 => "ZigBee 2004", + 2 => "ZigBee 2006/2007"} + spanid, source, extpanid, stackprofilever, channel = routerdata + stackprofilever = stackprofilever.unpack("H*")[0].hex + stackprofile = stackprofilever & 0x0f + stackver = (stackprofilever & 0xf0) >> 4 + profile = "Unknown" + profile = stackprofile_map[stackprofile] if stackprofile_map.has_key? stackprofile + ver = "Unknown" + ver = stackver_map[stackver] if stackver_map.has_key? stackver + print_status("New Network: PANID: 0x#{spanid.upcase} SOURCE: 0x#{source.upcase}") + print_status(" Ext PANID: #{extpanid.upcase.scan(/../).join(':')} Stack Profile: #{profile}") + print_status(" Stack Version: #{ver}") + print_status(" Channel: #{@channel}") + end + + def scan + @loop_count += 1 if @channel > 26 or datastore["CHANNEL"] + @channel = 11 if @channel > 26 + @seq = 0 if @seq > 255 + print_status("Scanning Channel #{@channel}") + set_channel(datastore["DEVICE"], @channel) + beacon = "\x03\x08#{@seq.chr}\xff\xff\xff\xff\x07" + inject(datastore["DEVICE"], beacon) + delay = Time.now + datastore["DELAY"] + while delay > Time.now() + pkt = recv(datastore["DEVICE"]) + if pkt and pkt.size > 0 and pkt["valid_crc"] + pktdecode = dot154_packet_decode(pkt["data"]) + if (pktdecode["FSF"] & DOT154_FCF_TYPE_MASK) == DOT154_FCF_TYPE_BEACON + key = "#{pktdecode["SPAN_ID"]}#{pktdecode["SOURCE"]}" + value = [pktdecode["SPAN_ID"], pktdecode["SOURCE"], pktdecode["EXT_PAN_ID"], pktdecode["STACK_PROFILE"], @channel] + if not @stumbled.has_key? key + @stumbled[key] = value + display_details(value) + end + end + end + end + sniffer_off(datastore["DEVICE"]) # Needed to clear receive buffers + @seq += 1 + @channel += 1 if not datastore["CHANNEL"] + end + + def run + if not get_target_device and not datastore["DEVICE"] + print_error "No target device set. Either set one with the 'target' command or specify the DEVICE" + return + end + @channel = datastore["CHANNEL"] if datastore["CHANNEL"] + if datastore["LOOP"] == -1 + while(1) do + scan + end + else + while(@loop_count < datastore["LOOP"]) + scan + end + end + end + +end diff --git a/tools/hardware/killerbee_msfrelay b/tools/hardware/killerbee_msfrelay index c600232ddb..16e8195dda 100755 --- a/tools/hardware/killerbee_msfrelay +++ b/tools/hardware/killerbee_msfrelay @@ -22,6 +22,7 @@ packets_sent = 0 last_sent = 0 username = None password = None +kb = None class MSFHandler(BaseHTTPRequestHandler): def status(self): @@ -53,6 +54,7 @@ class MSFHandler(BaseHTTPRequestHandler): return status def statistics(self): + global packets_sent stats = {} stats["uptime"] = int(time.time()) - starttime stats["packet_stats"] = packets_sent @@ -66,6 +68,39 @@ class MSFHandler(BaseHTTPRequestHandler): def timezone(self): return { "system_timezone": time.strftime("%Z") } + def set_channel(self, args): + if not "chan" in args: + return self.not_supported() + chan = int(args["chan"][0]) + kb.set_channel(chan) + return { "success": True } + + def inject(self, args): + global packets_sent + if not "data" in args: + return self.not_supported() + try: + kb.inject(base64.urlsafe_b64decode(args["data"][0])) + packets_sent+=1 + except Exception, e: + print("ERROR: Unable to inject packet: {0}".format(e)) + return { "success": False } + return { "success": True } + + def recv(self): + pkt = kb.pnext() + if pkt != None and pkt[1]: + return {"data": base64.urlsafe_b64encode(pkt[0]), "valid_crc": pkt[1], "rssi": pkt[2] } + return {} + + def sniffer_off(self): + kb.sniffer_off() + return {"success": True } + + def sniffer_on(self): + kb.sniffer_on() + return {"success": True } + def supported_devices(self): devices = [] for dev in kbutils.devlist(): @@ -112,12 +147,20 @@ class MSFHandler(BaseHTTPRequestHandler): elif self.path=="/zigbee/supported_devices": self.send(self.supported_devices()) elif self.path.startswith("/zigbee/"): - re_idx = re.compile("/zigbee/(\w+)/") - m = re_idx.match(self.path) + re_dev = re.compile("/zigbee/([\d\w:]+)/") + m = re_dev.match(self.path) if m: - idx = m.group(1) - if self.path.find("/set_freq?") > -1: - self.send(self.set_freq(args)) + dev = m.group(1) + if self.path.find("/set_channel?") > -1: + self.send(self.set_channel(args)) + elif self.path.find("/inject?") > -1: + self.send(self.inject(args)) + elif self.path.find("/recv") > -1: + self.send(self.recv()) + elif self.path.find("/sniffer_off") > -1: + self.send(self.sniffer_off()) + elif self.path.find("/sniffer_on") > -1: + self.send(self.sniffer_on()) else: self.send(self.not_supported(), 404) else: @@ -160,6 +203,7 @@ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() + parser.add_argument('-i', '--iface', '--dev', action='store', dest='devstring') parser.add_argument('-u', '--user', default="msf_relay", help='HTTP Username', type=str) parser.add_argument('-p', '--password', default="rfcat_relaypass", help='HTTP Password', type=str) parser.add_argument('-P', '--Port', default=8080, type=int) @@ -168,6 +212,12 @@ if __name__ == "__main__": ifo = parser.parse_args() + try: + kb = KillerBee(device=ifo.devstring) + except KBInterfaceError as e: + print("Interface Error: {0}".format(e)) + sys.exit(-1) + username = ifo.user password = ifo.password ip = "0.0.0.0"