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", } }