local string = require "string" local nmap = require "nmap" local shortport = require "shortport" local stdnse = require "stdnse" description = [[ This NSE script will query and parse pcworx protocol to a remote PLC. The script will send a initial request packets and once a response is received, it validates that it was a proper response to the command that was sent, and then will parse out the data. PCWorx is a protocol and Program by Phoenix Contact. http://digitalbond.com ]] --- -- @usage -- nmap --script pcworx-info -p 1962 -- -- -- @output --| pcworx-info: --| PLC Type: ILC 330 ETH --| Model Number: 2737193 --| Firmware Version: 3.95T --| Firmware Date: Mar 2 2012 --|_ Firmware Time: 09:39:02 -- -- -- @xmloutput --ILC 330 ETH --2737193 --3.95T --Mar 2 2012 --09:39:02 author = "Stephen Hilt (Digital Bond)" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery"} portrule = shortport.port_or_service(1962, "pcworx", "tcp") -- Safely extract a zero-terminated string if the blob is long enough -- Returns nil if it is not. local function get_string(blob, offset) if #blob >= offset then return string.unpack("z", blob, offset) end end --- -- Action Function that is used to run the NSE. This function will send the initial query to the -- host and port that were passed in via nmap. The initial response is parsed to determine if host -- is a pcworx Protocol device. If it is then more actions are taken to gather extra information. -- -- @param host Host that was scanned via nmap -- @param port port that was scanned via nmap action = function(host,port) local init_comms = "\x01\x01\0\x1a\0\0\0\0x\x80\0\x03\0\x0cIBETH01N0_M\0" -- create table for output local output = stdnse.output_table() -- create new socket local socket = nmap.new_socket() -- define the catch of the try statement local catch = function() socket:close() end local try = nmap.new_try(catch) try(socket:connect(host, port)) try(socket:send(init_comms)) local response = try(socket:receive()) if not response:match("^\x81") then stdnse.debug1("Unexpected or unknown PCWorx message.") return nil end -- pcworx has a session ID that is generated by the PLC -- This will pull the SID so we can communicate further to the PLC local sid = string.sub(response, 18, 18) local init_comms2 = "\x01\x05\0\x16\0\x01\0\0\x78\x80\0" .. sid .. "\0\0\0\x06\0\x04\x02\x95\0\0" try(socket:send(init_comms2)) -- receive response response = try(socket:receive()) -- TODO: verify this -- this is the request that will pull all the information from the PLC local req_info = "\x01\x06\0\x0e\0\x02\0\0\0\0\0" .. sid .. "\x04\0" try(socket:send(req_info)) -- receive response response = try(socket:receive()) -- if the response starts with 0x81 then we will continue if not response:match("^\x81") then stdnse.debug1("Unexpected or unknown PCWorx message.") socket:close() return nil end -- create output table with proper data output["PLC Type"] = get_string(response, 31) output["Model Number"] = get_string(response, 153) output["Firmware Version"] = get_string(response, 67) output["Firmware Date"] = get_string(response, 80) output["Firmware Time"] = get_string(response, 92) -- close socket and return output table socket:close() return output end