summaryrefslogtreecommitdiffstats
path: root/scripts/iec-identify.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/iec-identify.nse')
-rw-r--r--scripts/iec-identify.nse161
1 files changed, 161 insertions, 0 deletions
diff --git a/scripts/iec-identify.nse b/scripts/iec-identify.nse
new file mode 100644
index 0000000..81bcd61
--- /dev/null
+++ b/scripts/iec-identify.nse
@@ -0,0 +1,161 @@
+local shortport = require "shortport"
+local comm = require "comm"
+local stdnse = require "stdnse"
+local string = require "string"
+local match = require "match"
+
+description = [[
+Attempts to identify IEC 60870-5-104 ICS protocol.
+
+After probing with a TESTFR (test frame) message, a STARTDT (start data
+transfer) message is sent and general interrogation is used to gather the list
+of information object addresses stored.
+]]
+
+---
+-- @output
+-- | iec-identify:
+-- | ASDU address: 105
+-- |_ Information objects: 30
+--
+
+author = {"Aleksandr Timorin", "Daniel Miller"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+portrule = shortport.port_or_service(2404, "iec-104", "tcp")
+
+local function get_asdu(socket)
+ local status, data = socket:receive_buf(match.numbytes(2), true)
+ if not status then
+ return nil, data
+ end
+ if data:byte(1) ~= 0x68 then
+ return nil, "Not IEC-104"
+ end
+ local len = data:byte(2)
+ status, data = socket:receive_buf(match.numbytes(len), true)
+ if not status then
+ return nil, data
+ end
+ local apcitype = data:byte(1)
+ return apcitype, data
+end
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+ local socket, err = comm.opencon(host, port)
+ if not socket then
+ stdnse.debug1("Connect error: %s", err)
+ return nil
+ end
+
+ -- send TESTFR ACT command
+ -- Test frame, like "ping"
+ local TESTFR = "\x68\x04\x43\0\0\0"
+ local status, err = socket:send( TESTFR )
+ if not status then
+ stdnse.debug1("Failed to send: %s", err)
+ return nil
+ end
+
+ -- receive TESTFR answer
+ local apcitype, recv = get_asdu(socket)
+ if not apcitype then
+ stdnse.debug1("protocol error: %s", recv)
+ return nil
+ end
+ if apcitype ~= 0x83 then
+ stdnse.print_debug(1, "Not IEC-104. TESTFR response: %#x", apcitype)
+ return nil
+ end
+
+ -- send STARTDT ACT command
+ local STARTDT = "\x68\x04\x07\0\0\0"
+ status, err = socket:send( STARTDT )
+ if not status then
+ stdnse.debug1("Failed to send: %s", err)
+ return nil
+ end
+
+ -- receive STARTDT answer
+ apcitype, recv = get_asdu(socket)
+ if not apcitype then
+ stdnse.debug1("protocol error: %s", recv)
+ return nil
+ end
+ if apcitype ~= 0x0b then
+ stdnse.debug1("STARTDT ACT did not receive STARTDT CON: %#x", apcitype)
+ return nil
+ end
+
+ -- May also receive ME_EI_NA_1 (End of initialization), so check for that in the buffer after sending the next part
+
+ -- send C_IC_NA_1 command
+ -- type: 0x64, C_IC_NA_1,
+ -- numix: 1
+ -- TNCause: 6, Act
+ -- Originator address; 0
+ -- ASDU address: 0xffff
+ -- Information object address: 0
+ -- QOI: 0x14 (20), Station interrogation (global)
+ local C_IC_NA_1_broadcast = "\x68\x0e\0\0\0\0\x64\x01\x06\0\xff\xff\0\0\0\x14"
+ status, err = socket:send( C_IC_NA_1_broadcast )
+ if not status then
+ stdnse.debug1("Failed to send: %s", err)
+ return nil
+ end
+
+ local asdu_address
+ local ioas = 0
+ -- Have to draw the line somewhere.
+ local limit = 10
+ while limit > 0 do
+ limit = limit - 1
+ apcitype, recv = get_asdu(socket)
+ if not apcitype then
+ stdnse.debug1("Error in C_IC_NA_1: %s", recv)
+ break
+ end
+ if apcitype & 0x01 == 0 then -- Type I, numbered information transfer
+ -- skip 2 bytes Tx, 2 bytes Rx
+ local typeid = recv:byte(5)
+ if typeid == 70 then
+ -- ME_EI_NA_1, End of Initialization. Skip.
+ else
+ local numix = recv:byte(6) & 0x7f
+ local cause = recv:byte(7) & 0x3f
+ asdu_address = string.unpack("<I2", recv, 9)
+ stdnse.debug2("Got asdu=%d, type %d, cause %d, numix %d.", asdu_address, typeid, cause, numix)
+ if typeid == 100 then
+ -- C_IC_NA_1
+ if cause == 7 then
+ -- ActCon. Skip.
+ elseif cause == 10 then
+ -- ActTerm. The end!
+ break
+ else
+ -- TODO: do something!
+ end
+ else
+ if cause >= 20 and cause <= 36 then
+ -- Inrogen, response to general interrogation
+ ioas = ioas + numix
+ end
+ end
+ end
+ end
+ end
+
+ socket:close()
+
+ if asdu_address then
+ output["ASDU address"] = asdu_address
+ output["Information objects"] = ioas
+ else
+ output = "IEC-104 endpoint did not respond to C_IC_NA_1 request"
+ end
+
+ return output
+end