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:
- Another SCADA/ICS NMAP NSE script - Hirschmann/Innominate Security Technologies 'mGuard' firewall enumeration script Bob Radvanovsky (Dec 06)
- <Possible follow-ups>
- Re: Another SCADA/ICS NMAP NSE script - Hirschmann/Innominate Security Technologies 'mGuard' firewall enumeration script Bob Radvanovsky (Dec 06)