diff --git a/CHANGELOG b/CHANGELOG index c3aebe220..134ece2ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script http-waf-fingerprint which tries to detect the presence of + a web application firewall and its type and version. [Hani Benhabiles] + o [NSE] More Windows 7 and Windows 2008 fixes for the smb library and smb-ls scripts. [Patrik Karlsson] diff --git a/scripts/http-waf-fingerprint.nse b/scripts/http-waf-fingerprint.nse new file mode 100644 index 000000000..02bc24395 --- /dev/null +++ b/scripts/http-waf-fingerprint.nse @@ -0,0 +1,605 @@ +local http = require "http" +local stdnse = require "stdnse" +local shortport = require "shortport" +local string = require "string" +local url = require "url" + +description = [[ +Tries to detect the presence of web application firewall and its type and version. + +This works by sending a number of requests and looking in the responses for known behavior and fingerprints +such as Server header, cookies and headers values. +]] + +--- +-- @args http-waf-fingerprint.root The base path. Defaults to /. +-- +-- @usage +-- nmap --script=http-waf-fingerprint +-- +--@output +--PORT STATE SERVICE REASON +--80/tcp open http syn-ack +--| http-waf-fingerprint: +--| Detected firewalls +--|_ BinarySec version 3.2.2 + +author = "Hani Benhabiles" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "intrusive"} + +-- +-- Version 0.1: +-- - Initial version based on work done with wafw00f. +-- - Removed many false positives. +-- - Added fingeprints for WAFs such as Incapsula WAF, Cloudflare, USP-SES ,Cisco ACE XML Gateway and ModSecurity. +-- - Added fingerprints and version detection for Webknight and BinarySec, Citrix Netscaler and ModSecurity +-- +-- +-- TODO: Fingerprints for other WAFs +-- Add intensive mode (WAF specific requests) +-- + +portrule = shortport.service("http") + +-- Each WAF has a table with name, version and detected keys +-- as well as a match function. +-- HTTP Responses are passed to match function which will alter detected +-- and version values after analyzing responses if adequate fingerprints +-- are found. + +local bigip +bigip = { + name = "F5 BigIP", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + + if response.header['x-cnection'] then + stdnse.print_debug("%s BigIP detected through X-Cnection header.", SCRIPT_NAME) + bigip.detected = true + return + end + + if response.header.server == 'BigIP' then -- + stdnse.print_debug("%s BigIP detected through Server header.", SCRIPT_NAME) + bigip.detected = true + return + end + + for _, cookie in pairs(response.cookies) do -- + if string.find(cookie.name, "BIGipServer") then + stdnse.print_debug("%s BigIP detected through cookies.", SCRIPT_NAME) + bigip.detected = true + return + end + -- Application Security Manager module + if string.match(cookie.name, 'TS%w+') and string.len(cookie.name) <= 8 then + stdnse.print_debug("%s F5 ASM detected through cookies.", SCRIPT_NAME) + bigip.detected = true + return + end + end + end + end, +} + +local webknight +webknight = { + name = "Webknight", + detected = false, + version = nil, + + match = function(responses) + for name, response in pairs(responses) do + if response.header.server and string.find(response.header.server, 'WebKnight/') then -- + stdnse.print_debug("%s WebKnight detected through Server Header.", SCRIPT_NAME) + webknight.version = string.sub(response.header.server, 11) + webknight.detected = true + return + end + if response.status == 999 then + if not webknight.detected then stdnse.print_debug("%s WebKnight detected through 999 response status code.", SCRIPT_NAME) end + webknight.detected = true + end + end + end, +} + +local isaserver +isaserver = { + name = "ISA Server", + detected = false, + version = nil, + -- TODO Check if version detection is possible + -- based on the response reason + reason = {"Forbidden %( The server denied the specified Uniform Resource Locator %(URL%). Contact the server administrator. %)", + "Forbidden %( The ISA Server denied the specified Uniform Resource Locator %(URL%)" + }, + + match = function(responses) + for _, response in pairs(responses) do + for _, reason in pairs(isaserver.reason) do -- + if http.response_contains(response, reason, true) then -- TODO Replace with something more performant + stdnse.print_debug("%s ISA Server detected through response reason.", SCRIPT_NAME) + isaserver.detected = true + return + end + end + end + end, +} + +local airlock +airlock = { + name = "Airlock", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + for _, cookie in pairs(response.cookies) do -- + -- TODO Check if version detection is possible + -- based on the difference in cookies name + if cookie.name == "AL_LB" and string.sub(cookie.value, 1, 4) == '$xc/' then + stdnse.print_debug("%s Airlock detected through AL_LB cookies.", SCRIPT_NAME) + airlock.detected = true + return + end + if cookie.name == "AL_SESS" and (string.sub(cookie.value, 1, 5) == 'AAABL' + or string.sub(cookie.value, 1, 5) == 'LgEAA' )then + stdnse.print_debug("%s Airlock detected through AL_SESS cookies.", SCRIPT_NAME) + airlock.detected = true + return + end + end + end + end, +} + +local barracuda +barracuda = { + name = "Barracuda", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + for _, cookie in pairs(response.cookies) do + if cookie.name == "barra_counter_session" then + stdnse.print_debug("%s Barracuda detected through cookies.", SCRIPT_NAME) + barracuda.detected = true + return + end + end + end + end, +} + +local denyall +denyall = { + name = "Denyall", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + for _, cookie in pairs(response.cookies) do + -- TODO Check accuracy + if cookie.name == "sessioncookie" then + stdnse.print_debug("%s Denyall detected through cookies.", SCRIPT_NAME) + denyall.detected = true + return + end + end + end + end, +} + +local f5trafficshield +f5trafficshield = { + name = "F5 Traffic Shield", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + -- TODO Check for version detection possibility + -- based on the cookie name / server header presence + if response.header.server == "F5-TrafficShield" then + stdnse.print_debug("%s F5 Traffic Shield detected through Server header.", SCRIPT_NAME) + f5trafficshield.detected = true + return + end + + for _, cookie in pairs(response.cookies) do + if cookie.name == "ASINFO" then + stdnse.print_debug("%s F5 Traffic Shield detected through cookies.", SCRIPT_NAME) + f5trafficshield.detected = true + return + end + end + end + end, +} + +local teros +teros = { + name = "Teros / Citrix Application Firewall Enterprise", -- CAF EX, according to citrix documentation + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + for _, cookie in pairs(response.cookies) do + if cookie.name == "st8id" or cookie.name == "st8_wat" or cookie.name == "st8_wlf" then + stdnse.print_debug("%s Teros / CAF detected through cookies.", SCRIPT_NAME) + teros.detected = true + return + end + end + end + end, +} + +local binarysec +binarysec = { + name = "BinarySec", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header.server and string.find(response.header.server, 'BinarySEC/') then -- + stdnse.print_debug("%s BinarySec detected through Server Header.", SCRIPT_NAME) + binarysec.version = string.sub(response.header.server, 11) + binarysec.detected = true + return + end + if response.header['x-binarysec-via'] or response.header['x-binarysec-nocache']then + if not binarysec.detected then stdnse.print_debug("%s BinarySec detected through header.", SCRIPT_NAME) end + binarysec.detected = true + end + end + end, +} + +local profense +profense = { + name = "Profense", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header.server == 'Profense' then + stdnse.print_debug("%s Profense detected through Server header.", SCRIPT_NAME) + profense.detected = true + return + end + for _, cookie in pairs(response.cookies) do + if cookie.name == "PLBSID" then + stdnse.print_debug("%s Profense detected through cookies.", SCRIPT_NAME) + profense.detected = true + return + end + end + end + end, +} + +local netscaler +netscaler = { + name = "Citrix Netscaler", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + + -- TODO Check for other version detection possibilities + -- based on fingerprint difference + if response.header.via and string.find(response.header.via, 'NS-CACHE') then -- + stdnse.print_debug("%s Citrix Netscaler detected through Via Header.", SCRIPT_NAME) + netscaler.version = string.sub(response.header.via, 10, 12) + netscaler.detected = true + return + end + + if response.header.cneonction == "close" or response.header.nncoection == "close" then + if not netscaler.detected then stdnse.print_debug("%s Netscaler detected through Cneoction/nnCoection header.", SCRIPT_NAME) end + netscaler.detected = true + end + + -- TODO Does X-CLIENT-IP apply to Citrix Application Firewall too ? + if response.header['x-client-ip'] then + if not netscaler.detected then stdnse.print_debug("%s Netscaler detected through X-CLIENT-IP header.", SCRIPT_NAME) end + netscaler.detected = true + end + + for _, cookie in pairs(response.cookies) do + if cookie.name == "ns_af" or cookie.name == "citrix_ns_id" or + string.find(cookie.name, "NSC_") then + if not netscaler.detected then stdnse.print_debug("%s Netscaler detected through cookies.", SCRIPT_NAME) end + netscaler.detected = true + end + end + end + end, +} + +local dotdefender +dotdefender = { + name = "dotDefender", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header['X-dotdefender-denied'] == "1" then + stdnse.print_debug("%s dotDefender detected through X-dotDefender-denied header.", SCRIPT_NAME) + dotdefender.detected = true + return + end + end + end, +} + +local ibmdatapower +ibmdatapower = { + name = "IBM DataPower", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header['x-backside-transport'] then + stdnse.print_debug("%s IBM DataPower detected through X-Backside-Transport header.", SCRIPT_NAME) + ibmdatapower.detected = true + return + end + end + end, +} + +local cloudflare +cloudflare = { + name = "Cloudflare", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header.server == 'cloudflare-nginx' then + stdnse.print_debug("%s Cloudflare detected through Server header.", SCRIPT_NAME) + cloudflare.detected = true + return + end + for _, cookie in pairs(response.cookies) do + if cookie.name == "__cfduid" then + stdnse.print_debug("%s Cloudflare detected through cookies.", SCRIPT_NAME) + cloudflare.detected = true + return + end + end + end + end, +} + +local incapsula +incapsula = { + name = "Incapsula WAF", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + for _, cookie in pairs(response.cookies) do + if string.find(cookie.name, 'incap_ses') or string.find(cookie.name, 'visid_incap') then + stdnse.print_debug("%s Incapsula WAF detected through cookies.", SCRIPT_NAME) + incapsula.detected = true + return + end + end + end + end, +} + +local uspses +uspses = { + name = "USP Secure Entry Server", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header.server == 'Secure Entry Server' then + stdnse.print_debug("%s USP-SES detected through Server header.", SCRIPT_NAME) + uspses.detected = true + return + end + end + end, +} + +local ciscoacexml +ciscoacexml = { + name = "Cisco ACE XML Gateway", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header.server == 'ACE XML Gateway' then + stdnse.print_debug("%s Cisco ACE XML Gateway detected through Server header.", SCRIPT_NAME) + ciscoacexml.detected = true + return + end + end + end, +} + + +local modsecurity +modsecurity = { + -- Credit to Brendan Coles + name = "ModSecurity", + detected = false, + version = nil, + + match = function(responses) + for _, response in pairs(responses) do + if response.header.server and string.find(response.header.server, 'mod_security/') then + stdnse.print_debug("%s Modsecurity detected through Server Header.", SCRIPT_NAME) + local pos = string.find(response.header.server, 'mod_security/') + modsecurity.version = string.sub(response.header.server, pos + 13, pos + 18) + modsecurity.detected = true + return + end + + if response.header.server and string.find(response.header.server, 'Mod_Security') then + stdnse.print_debug("%s Modsecurity detected through Server Header.", SCRIPT_NAME) + modsecurity.version = string.sub(response.header.server, 13, -9) + modsecurity.detected = true + return + end + + -- The default SecServerSignature value is "NOYB" <= TODO For older versions, so we could + -- probably do some version detection out of it. + if response.header.server == 'NOYB' then + stdnse.print_debug("%s modsecurity detected through Server header.", SCRIPT_NAME) + modsecurity.detected = true + end + end + end, +} + + +local wafs = { + -- WAFs that are commented out don't have reliable fingerprints + -- with no false positives yet. + + bigip = bigip, + webknight = webknight, + isaserver = isaserver, + airlock = airlock, + barracuda = barracuda, + denyall = denyall, + f5trafficshield = f5trafficshield, + teros = teros, + binarysec = binarysec, + profense = profense, + netscaler = netscaler, + dotdefender = dotdefender, + ibmdatapower = ibmdatapower, + cloudflare = cloudflare, + incapsula = incapsula, + uspses = uspses, + ciscoacexml = ciscoacexml, + modsecurity = modsecurity, +-- netcontinuum = netcontinuum, +-- secureiis = secureiis, +-- urlscan = urlscan, +-- beeware = beeware, +-- hyperguard = hyperguard, +-- websecurity = websecurity, +-- imperva = imperva, +-- ibmwas = ibmwas, +-- naxsi = naxsi, +-- nevisProxy = nevisProxy, +-- genericwaf = genericwaf, +} + + +local send_requests = function(host, port, root) + local requests, all, responses = {}, {}, {} + + local dirtraversal = "../../../etc/passwd" + local cleanhtml = "hello" + local xssstring = "" + local cmdexe = "cmd.exe" + + -- Normal index + all = http.pipeline_add(root, nil, all, "GET") + table.insert(requests,"normal") + + -- Normal inexisting + all = http.pipeline_add(root .. "asofKlj", nil, all, "GET") + table.insert(requests,"inexisting") + + -- Invalid Method + all = http.pipeline_add(root, nil, all, "ASDE") + table.insert(requests,"invalidmethod") + + -- Directory traversal + all = http.pipeline_add(root .. "?parameter=" .. dirtraversal, nil, all, "GET") + table.insert(requests,"invalidmethod") + + -- Invalid Host + all = http.pipeline_add(root , {header= {Host = "somerandomsite.com"}}, all, "GET") + table.insert(requests,"invalidhost") + + --Clean HTML encoded + all = http.pipeline_add(root .. "?parameter=" .. cleanhtml , nil, all, "GET") + table.insert(requests,"cleanhtml") + + --Clean HTML + all = http.pipeline_add(root .. "?parameter=" .. url.escape(cleanhtml), nil, all, "GET") + table.insert(requests,"cleanhtmlencoded") + + -- XSS + all = http.pipeline_add(root .. "?parameter=" .. xssstring, nil, all, "GET") + table.insert(requests,"xss") + + -- XSS encoded + all = http.pipeline_add(root .. "?parameter=" .. url.escape(xssstring), nil, all, "GET") + table.insert(requests,"xssencoded") + + -- cmdexe + all = http.pipeline_add(root .. "?parameter=" .. cmdexe, nil, all, "GET") + table.insert(requests,"cmdexe") + + + -- send all requests + local pipeline_responses = http.pipeline_go(host, port, all) + if not pipeline_responses then + stdnse.print_debug("%s No response from pipelined requests", SCRIPT_NAME) + return nil + end + + -- Associate responses with requests names + for i, response in pairs(pipeline_responses) do + responses[requests[i]] = response + end + + return responses +end + +action = function(host, port) + local root = stdnse.get_script_args(SCRIPT_NAME .. '.root') or "/" + local result = {"Detected firewalls", {}} + + -- We send requests + local responses = send_requests(host, port, root) + if not responses then + return nil + end + + -- We iterate over wafs table passing the responses list to each function to analyze + -- the presence of any fingerprints. + for _, waf in pairs(wafs) do + waf.match(responses) + if waf.detected then + if waf.version then + table.insert(result[2], waf.name .. " version " .. waf.version) + else + table.insert(result[2], waf.name) + end + end + end + if #result[2] > 0 then + return stdnse.format_output(true, result) + end +end diff --git a/scripts/script.db b/scripts/script.db index 2ea7d10c5..9d50e9159 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -183,6 +183,7 @@ Entry { filename = "http-vuln-cve2011-3192.nse", categories = { "safe", "vuln", Entry { filename = "http-vuln-cve2011-3368.nse", categories = { "intrusive", "vuln", } } Entry { filename = "http-vuln-cve2012-1823.nse", categories = { "exploit", "intrusive", "vuln", } } Entry { filename = "http-waf-detect.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "http-waf-fingerprint.nse", categories = { "discovery", "intrusive", } } Entry { filename = "http-wordpress-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "http-wordpress-enum.nse", categories = { "auth", "intrusive", "vuln", } } Entry { filename = "http-wordpress-plugins.nse", categories = { "discovery", "intrusive", } }