NSE LIB

Back to library
Unofficial informational Discovery

1539-cip-tags-enum

Collects all tag names and types for Allen-Bradley Logix 5000 PLCs via CIP Service Code 0x55 - Get_Instance_Attribute_List

Ports

Any

Protocols

n/a

Attribution

Luis Rosa (upstream: chinarulezzz/nmap-extra-nse)

Usage

Copy the command and adjust the target or script arguments as needed.

nmap --script cip-tags-enum.nse -p 44818 <host>
Script Source Toggle

The full script source is stored with this entry and is hidden by default to keep the page easier to scan.

description = [[
Collects all tag names and types for Allen-Bradley Logix 5000 PLCs via
CIP Service Code 0x55 - Get_Instance_Attribute_List

See Logix 5000 Controllers Data Access
https://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf

]]

author = "Luis Rosa"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery"}

---
-- @usage
-- nmap --script cip-tags-enum.nse -p 44818 <host>
--
--
-- @output
--PORT      STATE SERVICE
--44818/tcp open  EtherNetIP-2
--| cip-tags-enum:
--|   Controller Tags:
--|     (0x0002) VAR_1: REAL
--|     (0x000D) VAR_2: BOOL
--|     (0x000E) VAR_3: INT
--|   Program Tags:
--|     (0x0002) VAR_1: REAL
--|     (0x000D) VAR_2: BOOL
--|     (0x000E) VAR_3: INT

-- @xmloutput
--<table key="Controller Tags">
--    <elem key="(0x0002) VAR_1">REAL</elem>
--    <elem key="(0x000D) VAR_2">BOOL</elem>
--    <elem key="(0x000E) VAR_3">INT</elem>
--</table>
--<table key="Program Tags">
--    <elem key="(0x0002) VAR_1">REAL</elem>
--    <elem key="(0x000D) VAR_2">BOOL</elem>
--    <elem key="(0x000E) VAR_3">INT</elem>
--</table>

local math = require "math"
local comm = require "comm"
local shortport = require "shortport"
local stdnse = require "stdnse"
local nsedebug = require "nsedebug"

portrule = shortport.port_or_service(44818, "EtherNet-IP-2")

local tag_types = {
    ['c1'] = 'BOOL',
    ['c2'] = 'SINT',
    ['c3'] = 'INT',
    ['c4'] = 'DINT',
    ['ca'] = 'REAL',
    ['d3'] = 'DWORD',
    ['c5'] = 'LINT',
}

-- return a ENIP/CIP request with service code 0x55
get_instance_attribute_list_request = function(session, ot_connection_id, sequence, request_path)
    local command_specific_data = ""..
    "0200".. -- # attributes
    "0100".. -- symbol name
    "0200"   -- symbol type

    local sequence_s = string.gsub(string.format('%04X', sequence) , "(..)(..)", "%2%1") -- 2 bytes le sequence id
    local cip_payload = ""..
    "55"..   -- service code
    string.format('%02x', request_path:len()/4)..   -- path size
    request_path..
    command_specific_data

    local item_2_len = string.format("%04x", (sequence_s..cip_payload):len()/2):gsub("(..)(..)", "%2%1")

    local enip_payload = ""..
    "00000000"..          -- interface (cip)
    "0000"..              -- timeout
    "0200"..              -- item count (2)
    "a100"..              -- item 1 type (connected address item)
    "0400"..              -- item 1 length
    ot_connection_id..    -- item 1 connection id
    "b100"..              -- item 2 type (connected data item)
    item_2_len..          -- item 2 length
    sequence_s ..         -- sequence
    cip_payload

    local enip_len = string.format("%04x", (enip_payload):len()/2):gsub("(..)(..)", "%2%1")

    return ""..
    stdnse.fromhex(""..
    "7000"..              -- command (Send Unit Data)
    enip_len..            -- length
    session..             -- session handle
    "00000000"..          -- status
    "0000000000000000"..  -- sender context
    "00000000"..          -- options
    enip_payload)         -- req path + payload
end

-- return a ENIP/CIP Connection Manager Forward Open (0x54)
cm_forward_open = function(session)
    connection_serial_number = string.byte(math.random(0x00, 0xFF))..string.byte(math.random(0x00, 0xFF))
    return ""..
    stdnse.fromhex(""..
    "6f00"..                    -- command (Send RR Data)
    "4000"..                    -- length
    session..                   -- session handle
    "00000000"..                -- status
    "0000000000000000"..        -- sender context
    "00000000"..                -- options
    "00000000"..                -- interface (cip)
    "0000"..                    -- timeout
    "0200"..                    -- item count
    "0000"..                    -- item 1 type (Null Address Item)
    "0000"..                    -- item 1 length
    "b200"..                    -- item 2 type (Unconnected Data Item)
    "3000"..                    -- item 2 length
    "54"..                      -- service code
    "02"..                      -- request path length
    "20062401"..                -- request path
    "0af0"..                    -- timeout
    "00000000"..                -- ot_connection_id
    "00000000"..                -- to_connection_id
    connection_serial_number..  -- connection serial number
    "0000"..                    -- originator vendor id
    "00000000"..                -- originator serial number
    "07"..                      -- connection timeout
    "000000"..                  -- reserved
    "00400000" ..               -- ot rpi
    "1243" ..                   -- ot network connection params
    "00400000"..                -- to rpi
    "1243" ..                   -- to network connection parms
    "a3"..                      -- transport type
    "03"..                      -- connection path length
    "010020022401"              -- connection path
    )
end

-- return a ENIP/CIP Connection Manager Forward Close (0x4e)
cm_forward_close = function(session)
    connection_serial_number = string.byte(math.random(0x00, 0xFF))..string.byte(math.random(0x00, 0xFF))
    return ""..
    stdnse.fromhex(""..
    "6f00"..                    -- command (Send RR Data)
    "2800"..                    -- length
    session..                   -- session handle
    "00000000"..                -- status
    "0000000000000000"..        -- sender context
    "00000000"..                -- options
    "00000000"..                -- interface (cip)
    "0000"..                    -- timeout
    "0200"..                    -- item count
    "0000"..                    -- item 1 type (Null Address Item)
    "0000"..                    -- item 1 length
    "b200"..                    -- item 2 type (Unconnected Data Item)
    "1800"..                    -- item 2 length
    "4e"..                      -- service code
    "02"..                      -- request path length
    "20062401"..                -- request path
    "0af0"..                    -- timeout
    connection_serial_number..  -- connection serial number
    "0000"..                    -- originator vendor id
    "00000000"..                -- originator serial number
    "03"..                      -- connection path length
    "00"..                      -- reserved
    "010020022401"              -- connection path
    )
end

get_instances = function(session, ot_connection_id, sequence, base_request_path, output)

    local last_instance_id = "0000" -- starting instance id
    local instance_id
    repeat
        local request_path = base_request_path ..
        "206b2500".. -- class
        last_instance_id  -- instance id

        try(socket:send(get_instance_attribute_list_request(session, ot_connection_id, sequence, request_path))) -- AB CIP 0x55
        local response = try(socket:receive())

        local general_status = string.unpack("I1", response, 49)
        stdnse.debug("Received a general Status %d", general_status)

        offset = 51
        while(offset<response:len()) do
            instance_id, idx, pos = string.unpack("<I4", response, offset)
            offset = offset + 4

            local symbol_name_length = string.unpack("<I2", response, offset)
            offset = offset + 2

            local symbol_name = response:sub(offset,offset+symbol_name_length-1)
            offset = offset + symbol_name_length

            local symbol_type = string.unpack("<I2", response, offset)
            offset = offset + 2

            if not symbol_name:match("^__") then -- discard internal tags
                if tag_types[stdnse.tohex(symbol_type)] then
                    output["("..string.format('0x%04X', instance_id)..") "..symbol_name] = tag_types[stdnse.tohex(symbol_type)]
                else
                    output["("..string.format('0x%04X', instance_id)..") "..symbol_name] = "Unknown type (0x"..stdnse.tohex(symbol_type)..")"
                end
            end
        end
        last_instance_id = stdnse.tohex(string.pack("<I2",instance_id+1))
        sequence = sequence + 1
    until (general_status == 0)
end

action = function(host, port)

    local output = stdnse.output_table()

    local register_session_request = stdnse.fromhex(""..
    "6500"..             -- register session
    "0400"..             -- length
    "00000000"..         -- session
    "00000000"..         -- status
    "0000000000000000".. -- sender context
    "00000000"..         -- options
    "0100"..             -- protocol version
    "0000"               -- flags
    )

    socket = nmap.new_socket()
    local catch = function()
        socket:close()
    end

    try = nmap.new_try(catch)
    try(socket:connect(host, port))
    try(socket:send(register_session_request))
    local response = try(socket:receive())
    local session, status = stdnse.tohex(response):match("65000400(........)(........).*")
    if (status == '00000000') then -- success
        stdnse.debug("ENIP Register Session successful: 0x%s", session)
        try(socket:send(cm_forward_open(session))) -- CIP CM FO
        local response = try(socket:receive())

        local ot_connection_id = string.unpack(">I4", response, 45) -- CIP O->T Network Connection ID
        ot_connection_id = stdnse.tohex(ot_connection_id)

        -- controller tags
        local sequence = 1
        local last_instance_id = "0000" -- starting instance id

        -- get global tags
        local request_path = ""
        local controller_tags = stdnse.output_table()
        get_instances(session, ot_connection_id, sequence, request_path, controller_tags)

        -- get local tags
        request_path = ""..
        "91"..                                -- class
        "13"..                                -- tag name length
        stdnse.tohex("Program:MainProgram").. -- Default Main Program
        "00"                                  -- Padding

        local program_tags = stdnse.output_table()
        get_instances(session, ot_connection_id, sequence, request_path, program_tags)

        output["Controller Tags"] = controller_tags
        output["Program Tags"] = program_tags

        stdnse.debug(stdnse.tohex(cm_forward_close(session)))
        try(socket:send(cm_forward_close(session))) -- CIP CM FC
        response = try(socket:receive())
        session, status = stdnse.tohex(response):match("67001e00(........)(........).*")
        stdnse.debug("ENIP Forward Close %s", status)
        if (status == '00000000') then -- success
            stdnse.debug("ENIP Forward Close")
        end
    end

    stdnse.debug("ENIP Register Session failed")
    socket:close()

    return output
end