Nmap Development mailing list archives

Another SCADA/ICS NMAP NSE script - Hirschmann/Innominate Security Technologies 'mGuard' firewall enumeration script


From: Bob Radvanovsky <rsradvan () unixworks net>
Date: Mon, 06 Dec 2010 21:56:19 -0600

This is one of several enumeration scripts that I have written for the SCADA/industrial control systems community.  
This checks/validates the web-based traffic for the Hirschmann/Innominate Security Technologies/Phoenix Contacts 
'mGuard' firewall.  NOTE: This has ONLY tested with the Hirschmann OEM of the Innominate's software, as well as 
Innominate's software (direct), and has NOT been tested on the newer Phoenix Contacts 'mGuard' firewall (even though it 
continues to be an OEM'd version of Innominate's software).

As the firewall has been rendered inoperative through our various enumeration and validation tests, there is currently 
NO sample output from the NMAP NSE script...sorry

The same script is shown below; if you wish to download the script, the script may be accessed here:
http://www.infracritical.com/enum-scripts/mguard-10091201.nse

===============================================

description = "Confirms/verifies that target device is mGuard firewall."
author      = "Bob Radvanovsky <rsradvan at infracritical dot com>"
license     = "Refer to: http://nmap.org/book/man-legal.html for license."
categories  = {"safe", "discovery"}

--
--  Filename:      mguard.nse
--
--  Purpose:       Checks for the following elements confirming said device:
-- 
--  1.  PHASE I - TLS/SSL certification verification.
--      a.  STEP 1:  Performs verification of the self-signed
--                   TLS/SSL certificate against known string patterns
--                   initially discovered of said device.
--      b.  STEP 2:  Performs verification of the self-signed
--                   TLS/SSL certificate against known MD5 and SHA
--                   hashes initially discovered of said device.
--
--          NOTE: Returns "flash_id" string for device (in verbose).
--
--  2.  PHASE II - File existence/confirmation & verification.
--      a.  STEP 1:  Confirms file "/favicon.ico" exists..
--      b.  STEP 2:  Confirms file "/gai.js" exists.
--      c.  STEP 3:  Confirms file "/style.css" exists.
--      d.  STEP 4:  Confirms file "/error404.html" exists.
--                   (version 4.2.1 and higher)
--                
--  3.  PHASE III - HTML pattern matching & confirmation.
--      a.  STEP 1:  Confirms excerpted HTML code based on
--                   each version; each version has slightly variant
--                   differences for each HTML code.
--      b.  STEP 2:  Confirms web server type (from HEAD cmd).
--
--  4.  PHASE IV - Documentation.
--
--  ==========================================================================
--                      
--  Version(s):    3.1.1, 4.1.1, 4.2.1, 4.2.3
--
--  Usage:         nmap --script=./mguard.nse <IP> -PN
--
--  Author(s):     Bob Radvanovsky - Infracritical
--                 <rsradvan at infracritical dot com>
--
--  Initwritten:   August 2010
--
--  DATE----  INIT DESCRIPTION------------------------------------------------
--  10.28.08  rsr  Inital development - VERSION 001.
--  10.30.08  rsr  Added script argument feature; argument called "VERBOSE".
--  10.30.08  rsr  Issue with standard argument arrays; making singular arg.
--  10.02.09  rsr  Cleaned up some of the code for efficiency purposes.
--  10.02.09  rsr  Added feature for TERSE vs. VERBOSE modes; if TERSE mode,
--                 only the server name, version and flash ID is returned;
--                 if VERBOSE mode, everything is returned.
--  10.02.09  rsr  Fixed VERBOSE feature; added "if nmap.verbosity() > 0".
--  10.05.09  rsr  Fixed substring issue; found "stdnse.strsplit()" function
--                 for splitting based on any given delimiter.
--  10.12.09  rsr  Removed version detection from basic available ports;
--                 found open/filtered ports unreliable for version detection.
--
--  NOTE: Script is a derived work from David Fifield's "ssl-cert.nse"
--        NSE script, and has been modified to work specifically on/for the
--        Hirschmann/Innominate/Phoenix mGuard firewell devices.
--
--  NOTE: We try and verify, running tests on as many versions of this device
--        as possible.  If you encounter an "UNKNOWN", this may mean that you
--        have scanned a device version/variant that was not tested.
--
--        Since we are providing this script free-of-charge to everyone,
--        would help us - and the community - if you would report the variance
--        to us.  Submit your findings to "report () infracritical com".  Thanks!
--

require("nmap")
require("nsedebug")
require("datafiles")
require("stdnse")
require("shortport")
require("http")
require("url")
require("strbuf")

local md5sum, i, answer, s, date_to_string, status, err
local stringify_name, table_find, flashid, orgname
local port1 = ""; local port2 = ""; local port3 = ""; local port4 = ""
local server = ""; local result = ""; local version = ""; local servertype = ""
local HAVE_SSL = false

local STARTTLS_PORTS = { 25, 587 }

--
-- NOTE: Much of what is determined, may be found simply through interrogating
--       the onboard web server; if the web server is DISABLED, or if the
--       verification is performed through the EXTERNAL port, the results may
--       vary, depending on how the firewall is configured.
--
portrule = shortport.port_or_service({ 443 }, { "https" })

action = function(host, port)
--
-- NOTE: Default discovered information is shown in NON-VERBOSE MODE; to
--       perform a VERBOSE discovery, follow the usage statement above.
--
--
-- NOTE: Determine if Version 4.2.1 (or newer) or 4.1.1 (or older)
--  

-- local file = assert(io.open("/tmp/temp.out", "a"))
-- file:write("THIS IS A TEST")

--
-- Discovered MD5 hashes for various files
--
  local sslcerts = {
        {md5="089e6c5d1eb61af201b8cbb024d98d00", name="3.1.1"},
        {md5="c93063872150383b879a69f65ab6d7e5", name="4.2.1 or newer"}
        }

  local favicons = {
        {md5="d41d8cd98f00b204e9800998ecf8427e", name="Infracritical TEST"},
        {md5="64e511bd5e7cd3b027e5af2a761e952a", name="4.1.1 or older"},
        {md5="7449c1f67008cc3bfabbc8f885712207", name="4.2.1 or newer"}
        }

  local gaijs = {
        {md5="6c30d7d2034d9840e330110ae118c03e", name="4.1.1 or older"},
        {md5="e7696a86648dcdb6efb2e497e5a8616b", name="4.2.1"},
        {md5="910cf96059109cfb5c2a845ec8456053", name="4.2.3 or newer"}
        }

  local stylecss = {
        {md5="ffc439102a78ea79965c9d76646d7468", name="4.1.1 or older"},
        {md5="d71581409253d54902bea82107a1abb2", name="4.2.1"},
        {md5="f0f828948553d578861a66396c9306b2", name="4.2.3 or newer"}
        }

  if pcall(require,'openssl') then HAVE_SSL = true; end
  if HAVE_SSL == true then

    s = nmap.new_socket()

    if table_find(STARTTLS_PORTS, port.number) then
      local status = starttls_negotiate(host, port)
      if not status then return nil; end
    else
      local status, error = s:connect(host.ip, port.number, "ssl")
      if not status then
        if nmap.verbosity() > 0 then return error else return nil; end
      end
    end

    local checkme
    local cert = s:get_ssl_certificate()
    s:close()

    subject = "Subject: " .. stringify_name(cert.subject)
    issuer = "Issuer: " .. stringify_name(cert.issuer)
    md5cert = stdnse.tohex(cert:digest("md5"))
    result = "CONFIRM DEVICE AS HIRSCHMANN / INNOMINATE"

    if string.match(subject, "Hirschmann Automation and Control GmbH") or string.match(subject, "Innominate Security 
Technologies AG") then
      if nmap.verbosity() > 1 then
        result = result .. "\n** PHASE 1: TLS/SSL certificate verification"
        result = result .. "\n....Step 1: SSL certificate info   : CONFIRMED"
      else
        result = result .. "\n** IF YOU REQUIRE MORE INFO, USE THE \"-v\" OPTION"
      end
      if nmap.verbosity() > 1 then
        result = result .. "\n....Step 2: SSL certificate MD5 hash information"
      end
      local split = stdnse.strsplit("/",subject)
      local split2 = stdnse.strsplit(":", split[1])
      local split3 = stdnse.strsplit("=", split2[2])
      flashid = string.sub(split3[2],1,16)
      result = result .. "\n............Flash ID               : " .. flashid
      if nmap.verbosity() > 1 then
        local split = stdnse.strsplit("/",subject)
        local split2 = stdnse.strsplit("=", split[2])
        orgname = string.sub(split2[2],1,45)
        result = result .. "\n............Organization name      : " .. orgname
        checkme = "n"
        for i = 1, #sslcerts, 1 do
          if md5cert == sslcerts[i].md5 then
            checkme = "y"
            result = result .. "\n............SSL certificate MD5    : " .. md5cert
            result = result .. "\n............SSL certificate version: " .. sslcerts[i].name
          end
        end
        if checkme == "n" then
          result = result .. "\n............SSL certificate version: UNKNOWN"
          result = result .. "\n............SSL certificate MD5    : " .. md5cert
        end
      end
    else
      if nmap.verbosity() > 1 then result = "Fingerprint not found."; end
    end
    if nmap.verbosity() > 1 then result = result .. "\n"; end

    if nmap.verbosity() > 1 then
      result = result .. "\n** PHASE 2: File presence verification"
      result = result .. "\n....Step 1: Existence of \"/favicon.ico\""
      answer = http.get(host, port, "/favicon.ico")
      -- check for 200 response code
      if answer.status == 200 then
        md5sum=stdnse.tohex(openssl.md5(answer.body))
        checkme = "n"
        for i = 1, #favicons, 1 do
          if md5sum == favicons[i].md5 then
            checkme = "y"
            result = result .. "\n............File favicon.ico MD5   : " .. md5sum
            result = result .. "\n............Server type/version    : " .. favicons[i].name
          end
        end
        if checkme == "n" then
          result = result .. "\n............Unknown favicon.ico MD5: " .. md5sum
        end
      else
        result = "\n............File favicon.ico       : FAIL"
      end

      result = result .. "\n....Step 2: Existence of \"/gai.js\""
      answer = http.get(host, port, "/gai.js")
      -- check for 200 response code
      if answer.status == 200 then
        md5sum=stdnse.tohex(openssl.md5(answer.body))
        checkme = "n"
        for i = 1, #gaijs, 1 do
          if md5sum == gaijs[i].md5 then
            checkme = "y"
            if string.match(answer.body, "Innominate Security Technologies AG") then
              result = result .. "\n............File gai.js MD5        : " .. md5sum
              result = result .. "\n............Server type/version    : " .. gaijs[i].name
            end
          end
        end
        if checkme == "n" then
          result = result .. "\n............Unknown gai.js MD5     : " .. md5sum
        end
      else
        result = "\n............File gai.js            : FAIL"
      end
      
      result = result .. "\n....Step 3: Existence of \"/style.css\""
      answer = http.get(host, port, "/style.css")
      -- check for 200 response code
      if answer.status == 200 then
        md5sum=stdnse.tohex(openssl.md5(answer.body))
        checkme = "n"
        for i = 1, #stylecss, 1 do
          if md5sum == stylecss[i].md5 then
            checkme = "y"
            if string.match(answer.body, "Innominate Security Technologies AG") then
              result = result .. "\n............File style.css MD5     : " .. md5sum
              result = result .. "\n............Server type/version    : " .. stylecss[i].name
            end
          end
        end
        if checkme == "n" then
          result = result .. "\n............Unknown style.css MD5  : " .. md5sum
        end
      else
        result = "\n............File style.css         : FAIL"
      end

      result = result .. "\n** PHASE 3: HTML pattern matching verification"
      result = result .. "\n....Step 1: Confirmation of HTML code per version"
    end
    answer = http.get(host, port, "/")
    -- check for 200 response code
    if answer.status == 200 then
      if string.match(answer.body, "GAI.SESSIONID") and string.match(answer.body, "GAI.TAB.STATE") and 
string.match(answer.body, "Username:") and string.match(answer.body, "Password:") and string.match(answer.body, "Access 
Type:") and string.match(answer.body, "User Firewall") and string.match(answer.body, "GAI.SUBMIT.LOGIN") then
        if nmap.verbosity() > 1 then
          result = result .. "\n............HTML code verified     : CONFIRMED"
        end
        if string.match(answer.body, "EAGLE") then
          servertype = "hirschmann"
          if nmap.verbosity() > 1 then
            result = result .. "\n............HTML code variant      : Hirschmann"
          else
            result = result .. "\n............Manufacturer of device : Hirschmann"
          end
        end
        if string.match(answer.body, "mGuard") then
          servertype = "innominate"
          if nmap.verbosity() > 1 then
            result = result .. "\n............HTML code variant      : Innominate"
          else
            result = result .. "\n............Manufacturer of device : Innominate"
          end
        end
      else
        if nmap.verbosity() > 1 then
          result = result .. "\n............HTML code version      : UNKNOWN"
          result = result .. "\n"
        end
      end
    end

    if nmap.verbosity() > 1 then
      result = result .. "\n....Step 2: Confirmation web server verification"
      answer = http.get(host, port, "/")
      -- check for 200 response code
      if answer.status == 200 then
        server = string.match(stdnse.format_output(true, answer.rawheader), "Server: fnord/1.6")
        if server then
          version = string.sub(server,15)
          result = result .. "\n............Web server verified    : CONFIRMED"
          result = result .. "\n............Web server name/type   : fnord"
          result = result .. "\n............Web server version     : " .. version
        else
          result = result .. "\n............Web server name/type   : UNKNOWN"
        end
        result = result .. "\n"
      end
      result = result .. "\n** PHASE 4: Documentation"
      result = result .. "\n....Step 1: Documentation exist?   : YES"
      result = result .. "\n"
      if servertype == "hirschmann" then
        result = result .. "\n............ninja.infracritical.com/dox/hirschmann/UM_BAT54_SW_Rel754_en.pdf"
        result = result .. "\n............ninja.infracritical.com/dox/hirschmann/UM_EAGLE_401_EN.pdf"
      end
      if servertype == "innominate" then
        result = result .. "\n............ninja.infracritical.com/dox/innominate/handbuch_mguard_600_en.pdf"
      end
    end
    return result
  end -- HAVE_SSL
end

-- Find the index of a value in an array.
function table_find(t, value)
  local i, v
  for i, v in ipairs(t) do
    if v == value then return i; end
  end
  return nil
end

-- These are the subject/issuer name fields that will be shown, in this order,
-- without a high verbosity.
local NON_VERBOSE_FIELDS = { "commonName", "organizationName",
    "stateOrProvinceName", "countryName" }

function stringify_name(name)
  local fields = {}
  local _, k, v
  if not name then return nil; end
  for _, k in ipairs(NON_VERBOSE_FIELDS) do
    v = name[k]
    if v then
      fields[#fields + 1] = string.format("%s=%s", k, v)
    end
  end
  if nmap.verbosity() > 1 then
    for k, v in pairs(name) do
      -- Don't include a field twice.
      if not table_find(NON_VERBOSE_FIELDS, k) then
        if type(k) == "table" then
          k = stdnse.strjoin(".", k)
        end
        fields[#fields + 1] = string.format("%s=%s", k, v)
      end
    end
  end
  return stdnse.strjoin("/", fields)
end

function date_to_string(date)
  if not date then return "MISSING"; end
  if type(date) == "string" then
    return string.format("Can't parse; string is \"%s\"", date)
  else
    return os.date("%Y-%m-%d %H:%M:%S", os.time(date))
  end
end

function starttls_negotiate(host, port)
  -- Attempt to negotiate TLS over SMTP for services that support it
  -- Works for SMTP (25) and SMTP Submission (587)

  -- Open a standard TCP socket
  local status, error = s:connect(host, port, "tcp")  
  if not status then
    return nil
  else
    -- Loop until the service presents a banner to deal with server
    -- load and timing issues.  There may be a better way to handle this.
    local i = 0
    repeat
      status, resultEHLO = s:receive_lines(1)
      i = i + 1
    until string.match(resultEHLO, "^220") or i == 5

    -- Send EHLO because the the server expects it
    -- We are not going to check for STARTTLS in the capabilities
    -- list, sometimes it is not advertised.
    local query = "EHLO example.org\r\n"
    status = s:send(query)
    status, resultEHLO = s:receive_lines(1)

    if not (string.match(resultEHLO, "^250")) then
      stdnse.print_debug("1","%s",resultEHLO)
      stdnse.print_debug("1","EHLO with errors or timeout.  Enable --script-trace to see what is happening.")
      return nil
    end

    resultEHLO = ""
      
    -- Send STARTTLS command ask the service to start encryption    
    local query = "STARTTLS\r\n"
    status = s:send(query)
    status, resultEHLO = s:receive_lines(1)
        
    if not (string.match(resultEHLO, "^220")) then
      stdnse.print_debug("1","%s",resultEHLO)
      stdnse.print_debug("1","STARTTLS failed or unavailable.  Enable --script-trace to see what is happening.")
            
      -- Send QUIT to clean up server side connection
      local query = "QUIT\r\n"
      status = s:send(query)        
      resultEHLO = ""
      return nil
    end
        
    -- Service supports STARTTLS, tell NSE start SSL negotiation
    status, error = s:reconnect_ssl()
    if not status then
      stdnse.print_debug("1","Could not establish SSL session after STARTTLS command.")
      s:close()
      return nil
    end
  end    
  -- Should have a solid TLS over SMTP session now...
  return "Connected"
end 
_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://seclists.org/nmap-dev/


Current thread: