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/client/iec104/iec104.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6#7# this module sends IEC104 commands8#910include Msf::Exploit::Remote::Tcp1112def initialize(info = {})13super(update_info(info,14'Name' => 'IEC104 Client Utility',15'Description' => %q(16This module allows sending 104 commands.17),18'Author' =>19[20'Michael John <mjohn.info[at]gmail.com>'21],22'License' => MSF_LICENSE,23'Actions' =>24[25['SEND_COMMAND', 'Description' => 'Send command to device']26],27'DefaultAction' => 'SEND_COMMAND'))2829register_options(30[31Opt::RPORT(2404),32OptInt.new('ORIGINATOR_ADDRESS', [true, "Originator Address", 0]),33OptInt.new('ASDU_ADDRESS', [true, "Common Address of ASDU", 1]),34OptInt.new('COMMAND_ADDRESS', [true, "Command Address / IOA Address", 0]),35OptInt.new('COMMAND_TYPE', [true, "Command Type", 100]),36OptInt.new('COMMAND_VALUE', [true, "Command Value", 20])37]38)39end4041# sends the frame data over tcp connection and returns received string42# using sock.get is causing quite some delay, but script needs to process responses from 104 server43def send_frame(data)44begin45sock.put(data)46sock.get(-1, sock.def_read_timeout)47rescue StandardError => e48print_error("Error:" + e.message)49end50end5152# ACPI formats:53# TESTFR_CON = '\x83\x00\x00\x00'54# TESTFR_ACT = '\x43\x00\x00\x00'55# STOPDT_CON = '\x23\x00\x00\x00'56# STOPDT_ACT = '\x13\x00\x00\x00'57# STARTDT_CON = '\x0b\x00\x00\x00'58# STARTDT_ACT = '\x07\x00\x00\x00'5960# creates and STARTDT Activation frame -> answer should be a STARTDT confirmation61def startcon62apci_data = "\x68"63apci_data << "\x04"64apci_data << "\x07"65apci_data << "\x00"66apci_data << "\x00"67apci_data << "\x00"68apci_data69end7071# creates and STOPDT Activation frame -> answer should be a STOPDT confirmation72def stopcon73apci_data = "\x68"74apci_data << "\x04"75apci_data << "\x13"76apci_data << "\x00"77apci_data << "\x00"78apci_data << "\x00"79apci_data80end8182# creates the acpi header of a 104 message83def make_apci(asdu_data)84apci_data = "\x68"85apci_data << [asdu_data.size + 4].pack("c") # size byte86apci_data << String([$tx].pack('v'))87apci_data << String([$rx].pack('v'))88$rx = $rx + 289$tx = $tx + 290apci_data << asdu_data91apci_data92end9394# parses the header of a 104 message95def parse_headers(response_data)96if !response_data[0].eql?("\x04") && !response_data[1].eql?("\x01")97$rx = + (response_data[2].unpack('H*').first + response_data[1].unpack('H*').first).to_i(16)98print_good(" TX: " + response_data[4].unpack('H*').first + response_data[3].unpack('H*').first + \99" RX: " + response_data[2].unpack('H*').first + response_data[1].unpack('H*').first)100end101if response_data[7].eql?("\x07")102print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activation Confirmation)")103elsif response_data[7].eql?("\x0a")104print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Termination Activation)")105elsif response_data[7].eql?("\x14")106print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Inrogen)")107elsif response_data[7].eql?("\x0b")108print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Feedback by distant command / Retrem)")109elsif response_data[7].eql?("\x03")110print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Spontaneous)")111elsif response_data[7].eql?("\x04")112print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Initialized)")113elsif response_data[7].eql?("\x05")114print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Interrogation)")115elsif response_data[7].eql?("\x06")116print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activiation)")117118# 104 error messages119elsif response_data[7].eql?("\x2c")120print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Type Identification Unknown)")121elsif response_data[7].eql?("\x2d")122print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Cause Unknown)")123elsif response_data[7].eql?("\x2e")124print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (ASDU Address Unknown)")125elsif response_data[7].eql?("\x2f")126print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (IOA Address Unknown)")127elsif response_data[7].eql?("\x6e")128print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Unknown Comm Address ASDU)")129end130end131132##############################################################################################################133# following functions parse different 104 ASDU messages and prints it content, not all messages of the standard are currently implemented134##############################################################################################################135def parse_m_sp_na_1(response_data)136sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000 # this bit determines the object addressing structure137response_data = response_data[11..-1] # cut out acpi data138if sq_bit.eql?(0b10000000)139ioa = response_data[0..3] # extract ioa value140response_data = response_data[3..-1] # cut ioa from message141i = 0142while response_data.length >= 1143print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \144" SIQ: 0x" + response_data[0].unpack('H*').first)145response_data = response_data[1..-1]146i += 1147end148else149while response_data.length >= 4150ioa = response_data[0..3] # extract ioa151print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \152" SIQ: 0x" + response_data[3].unpack('H*').first)153response_data = response_data[4..-1]154end155end156end157158def parse_m_me_nb_1(response_data)159sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000160response_data = response_data[11..-1] # cut out acpi data161if sq_bit.eql?(0b10000000)162ioa = response_data[0..3]163response_data = response_data[3..-1]164i = 0165while response_data.length >= 3166print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \167" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)168response_data = response_data[3..-1]169i += 1170end171else172while response_data.length >= 6173ioa = response_data[0..5]174print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \175" Value: 0x" + response_data[3..4].unpack('H*').first + " QDS: 0x" + + response_data[5].unpack('H*').first)176response_data = response_data[6..-1]177end178end179end180181def parse_c_sc_na_1(response_data)182sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000183response_data = response_data[11..-1] # cut out acpi data184if sq_bit.eql?(0b10000000)185ioa = response_data[0..3]186response_data = response_data[3..-1]187i = 0188while response_data.length >= 1189print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \190" DIQ: 0x" + response_data[0].unpack('H*').first)191response_data = response_data[1..-1]192i += 1193end194else195while response_data.length >= 4196ioa = response_data[0..3]197print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \198" DIQ: 0x" + response_data[3].unpack('H*').first)199response_data = response_data[4..-1]200end201end202end203204def parse_m_dp_na_1(response_data)205sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000206response_data = response_data[11..-1] # cut out acpi data207if sq_bit.eql?(0b10000000)208ioa = response_data[0..3]209response_data = response_data[3..-1]210i = 0211while response_data.length >= 1212print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \213" SIQ: 0x" + response_data[0].unpack('H*').first)214response_data = response_data[1..-1]215i += 1216end217else218while response_data.length >= 4219ioa = response_data[0..3]220print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \221" SIQ: 0x" + response_data[3].unpack('H*').first)222response_data = response_data[4..-1]223end224end225end226227def parse_m_st_na_1(response_data)228sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000229response_data = response_data[11..-1] # cut out acpi data230if sq_bit.eql?(0b10000000)231ioa = response_data[0..3]232response_data = response_data[3..-1]233i = 0234while response_data.length >= 2235print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \236" VTI: 0x" + response_data[0].unpack('H*').first + " QDS: 0x" + response_data[1].unpack('H*').first)237response_data = response_data[2..-1]238i += 1239end240else241while response_data.length >= 5242ioa = response_data[0..4]243print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \244" VTI: 0x" + response_data[3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)245response_data = response_data[5..-1]246end247end248end249250def parse_m_dp_tb_1(response_data)251sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000252response_data = response_data[11..-1] # cut out acpi data253if sq_bit.eql?(0b10000000)254ioa = response_data[0..3]255response_data = response_data[3..-1]256i = 0257while response_data.length >= 8258print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \259" DIQ: 0x" + response_data[0].unpack('H*').first)260print_cp56time2a(response_data[1..7])261response_data = response_data[8..-1]262i += 1263end264else265while response_data.length >= 11266ioa = response_data[0..10]267print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \268" DIQ: 0x" + response_data[3].unpack('H*').first)269print_cp56time2a(response_data[4..10])270response_data = response_data[11..-1]271end272end273end274275def parse_m_sp_tb_1(response_data)276sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000277response_data = response_data[11..-1] # cut out acpi data278if sq_bit.eql?(0b10000000)279ioa = response_data[0..3]280response_data = response_data[3..-1]281i = 0282while response_data.length >= 8283print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \284" SIQ: 0x" + response_data[0].unpack('H*').first)285print_cp56time2a(response_data[1..7])286response_data = response_data[8..-1]287i += 1288end289else290while response_data.length >= 11291ioa = response_data[0..10]292print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \293" SIQ: 0x" + response_data[3].unpack('H*').first)294print_cp56time2a(response_data[4..10])295response_data = response_data[11..-1]296end297end298end299300def parse_c_dc_na_1(response_data)301sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000302response_data = response_data[11..-1] # cut out acpi data303if sq_bit.eql?(0b10000000)304ioa = response_data[0..3]305response_data = response_data[3..-1]306i = 0307while response_data.length >= 1308print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \309" DCO: 0x" + response_data[0].unpack('H*').first)310response_data = response_data[1..-1]311i += 1312end313else314while response_data.length >= 4315ioa = response_data[0..3]316print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \317" DCO: 0x" + response_data[3].unpack('H*').first)318response_data = response_data[4..-1]319end320end321end322323def parse_m_me_na_1(response_data)324sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000325response_data = response_data[11..-1] # cut out acpi data326if sq_bit.eql?(0b10000000)327ioa = response_data[0..3]328response_data = response_data[3..-1]329i = 0330while response_data.length >= 3331print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \332" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)333response_data = response_data[3..-1]334i += 1335end336else337while response_data.length >= 6338ioa = response_data[0..3]339print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \340" Value: 0x" + ioa[3..4].unpack('H*').first + " QDS: 0x" + response_data[5].unpack('H*').first)341response_data = response_data[6..-1]342end343end344end345346def parse_m_me_nc_1(response_data)347sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000348response_data = response_data[11..-1] # cut out acpi data349if sq_bit.eql?(0b10000000)350ioa = response_data[0..3]351response_data = response_data[3..-1]352i = 0353while response_data.length >= 5354print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \355" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)356response_data = response_data[5..-1]357i += 1358end359else360while response_data.length >= 8361ioa = response_data[0..3]362print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \363" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)364response_data = response_data[8..-1]365end366end367end368369def parse_m_it_na_1(response_data)370sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000371response_data = response_data[11..-1] # cut out acpi data372if sq_bit.eql?(0b10000000)373response_data = response_data[11..-1]374ioa = response_data[0..3]375i = 0376while response_data.length >= 5377print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \378" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)379response_data = response_data[5..-1]380i += 1381end382else383while response_data.length >= 8384ioa = response_data[0..3]385print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \386" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)387response_data = response_data[8..-1]388end389end390end391392def parse_m_bo_na_1(response_data)393sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000394response_data = response_data[11..-1] # cut out acpi data395if sq_bit.eql?(0b10000000)396ioa = response_data[0..3]397response_data = response_data[3..-1]398i = 0399while response_data.length >= 5400print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \401" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)402response_data = response_data[5..-1]403i += 1404end405else406while response_data.length >= 8407ioa = response_data[0..3]408print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \409" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)410response_data = response_data[8..-1]411end412end413end414415# function to parses time format used in IEC 104416# function ported to ruby from: https://github.com/Ebolon/iec104417def print_cp56time2a(buf)418us = ((Integer(buf[1].unpack('c').first) & 0xFF) << 8) | (Integer(buf[0].unpack('c').first) & 0xFF)419second = Integer(us) / 1000420us = us % 1000421minute = Integer(buf[2].unpack('c').first) & 0x3F422hour = Integer(buf[3].unpack('c').first) & 0x1F423day = Integer(buf[4].unpack('c').first) & 0x1F424month = (Integer(buf[5].unpack('c').first) & 0x0F) - 1425year = (Integer(buf[6].unpack('c').first) & 0x7F) + 2000426print_good(" Timestamp: " + String(year) + "-" + String(format("%02d", month)) + "-" + String(format("%02d", day)) + " " + \427String(format("%02d", hour)) + ":" + String(format("%02d", minute)) + ":" + String(format("%02d", second)) + "." + String(us))428end429430##############################################################################################################431# END of individual parse functions section432##############################################################################################################433434# parses the 104 response string of a message435def parse_response(response)436response_elements = response.split("\x68")437response_elements.shift438response_elements.each do |response_element|439if response_element[5].eql?("\x64")440print_good(" Parsing response: Interrogation command (C_IC_NA_1)")441parse_headers(response_element)442elsif response_element[5].eql?("\x01")443print_good(" Parsing response: Single point information (M_SP_NA_1)")444parse_headers(response_element)445parse_m_sp_na_1(response_element)446elsif response_element[5].eql?("\x0b")447print_good(" Parsing response: Measured value, scaled value (M_ME_NB_1)")448parse_headers(response_element)449parse_m_me_nb_1(response_element)450elsif response_element[5].eql?("\x2d")451print_good(" Parsing response: Single command (C_SC_NA_1)")452parse_headers(response_element)453parse_c_sc_na_1(response_element)454elsif response_element[5].eql?("\x03")455print_good(" Parsing response: Double point information (M_DP_NA_1)")456parse_headers(response_element)457parse_m_dp_na_1(response_element)458elsif response_element[5].eql?("\x05")459print_good(" Parsing response: Step position information (M_ST_NA_1)")460parse_headers(response_element)461parse_m_st_na_1(response_element)462elsif response_element[5].eql?("\x1f")463print_good(" Parsing response: Double point information with time (M_DP_TB_1)")464parse_headers(response_element)465parse_m_dp_tb_1(response_element)466elsif response_element[5].eql?("\x2e")467print_good(" Parsing response: Double command (C_DC_NA_1)")468parse_headers(response_element)469parse_c_dc_na_1(response_element)470elsif response_element[5].eql?("\x1e")471print_good(" Parsing response: Single point information with time (M_SP_TB_1)")472parse_headers(response_element)473parse_m_sp_tb_1(response_element)474elsif response_element[5].eql?("\x09")475print_good(" Parsing response: Measured value, normalized value (M_ME_NA_1)")476parse_headers(response_element)477parse_m_me_na_1(response_element)478elsif response_element[5].eql?("\x0d")479print_good(" Parsing response: Measured value, short floating point value (M_ME_NC_1)")480parse_headers(response_element)481parse_m_me_nc_1(response_element)482elsif response_element[5].eql?("\x0f")483print_good(" Parsing response: Integrated total without time tag (M_IT_NA_1)")484parse_headers(response_element)485parse_m_it_na_1(response_element)486elsif response_element[5].eql?("\x07")487print_good(" Parsing response: Bitstring of 32 bits without time tag. (M_BO_NA_1)")488parse_headers(response_element)489parse_m_bo_na_1(response_element)490491elsif response_element[5].eql?("\x46")492print_good("Received end of initialisation confirmation")493parse_headers(response_element)494elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x01") && response_element[2].eql?("\x00")495print_good("Received S-Frame")496parse_headers(response_element)497elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x0b") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")498print_good("Received STARTDT_ACT")499elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x23") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")500print_good("Received STOPDT_ACT")501elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x43") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")502print_good("Received TESTFR_ACT")503else504print_status("Received unknown message")505parse_headers(response_element)506print_status(response_element.unpack('H*').first)507end508# Uncomment for print received data509# print_good("DEBUG: " + response_element.unpack('H*').first)510end511end512513# sends 104 command with configure datastore options514# default values are for a general interrogation command515# for example a switching command would be:516# COMMAND_TYPE => 46 // double command without time517# COMMAND_ADDRESS => 100 // any IOA address that should be switched518# COMMAND_VALUE => 6 // switching off with short pulse519# use value 5 to switch on with short pulse520#521# Structure of 104 message:522# 1byte command type523# 1byte num ix -> 1 (one item send)524# 1byte cause of transmission -> 6 (activation)525# 1byte originator address526# 2byte common adsu address527# 3byte command address528# 1byte command value529def func_send_command530print_status("Sending 104 command")531532asdu = [datastore['COMMAND_TYPE']].pack("c") # type of command533asdu << "\x01" # num ix -> only one item is send534asdu << "\x06" # cause of transmission = activation, 6535asdu << [datastore['ORIGINATOR_ADDRESS']].pack("c") # sets originator address of client536asdu << String([Integer(datastore['ASDU_ADDRESS'])].pack('v')) # sets the common address of ADSU537asdu << String([Integer(datastore['COMMAND_ADDRESS'])].pack('V'))[0..2] # sets the IOA address, todo: check command address fits in the 3byte address field538asdu << [datastore['COMMAND_VALUE']].pack("c") # sets the value of the command539540# Uncomment for debugging541# print_status("Sending: " + make_apci(asdu).unpack('H*').first)542response = send_frame(make_apci(asdu))543544if response.nil?545print_error("No answer")546else547parse_response(response)548end549print_status("operation ended")550end551552def run553$rx = 0554$tx = 0555begin556connect557rescue StandardError => e558print_error("Error:" + e.message)559return560end561562# send STARTDT_CON to activate connection563response = send_frame(startcon)564if response.nil?565print_error("Could not connect to 104 service")566return567else568parse_response(response)569end570571# send the 104 command572case action.name573when "SEND_COMMAND"574func_send_command575else576print_error("Invalid ACTION")577end578579# send STOPDT_CON to terminate connection580response = send_frame(stopcon)581if response.nil?582print_error("Terminating Connection")583return584else585print_status("Terminating Connection")586parse_response(response)587end588589begin590disconnect591rescue StandardError => e592print_error("Error:" + e.message)593end594end595end596597598