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