Spiders a website and attempts to identify output escaping problems where content is reflected back to the user. This script locates all parameters, ?x=foo&y=bar and checks if the values are reflected on the page. If they are indeed reflected, the script will try to insert ghz>hzx"zxc’xcv and check which (if any) characters were reflected back onto the page without proper html escaping. This is an indication of potential XSS vulnerability.
only spider URLs within the same host. (default: true)
the url to start spidering. This is a URL relative to the scanned host eg. /default.html (default: /)
the maximum amount of directories beneath the initial url to spider. A negative value disables the limit. (default: 3)
only spider URLs within the same domain. This widens the scope from withinhost
and can not be used in combination. (default: false)
the maximum amount of pages to visit. A negative value disables the limit (default: 20)
See the documentation for the slaxml library.
See the documentation for the httpspider library.
See the documentation for the smbauth library.
See the documentation for the http library.
nmap --script=http-unsafe-output-escaping <target>
PORT STATE SERVICE REASON
| http-unsafe-output-escaping:
| Characters [> " '] reflected in parameter kalle at http://foobar.gazonk.se/xss.php?foo=bar&kalle=john
|_ Characters [> " '] reflected in parameter foo at http://foobar.gazonk.se/xss.php?foo=bar&kalle=john
local http = require "http"
local httpspider = require "httpspider"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"
local url = require "url"
description = [[
Spiders a website and attempts to identify output escaping problems
where content is reflected back to the user. This script locates all
parameters, ?x=foo&y=bar and checks if the values are reflected on the
page. If they are indeed reflected, the script will try to insert
ghz>hzx"zxc'xcv and check which (if any) characters were reflected
back onto the page without proper html escaping. This is an
indication of potential XSS vulnerability.
]]
---
-- @usage
-- nmap --script=http-unsafe-output-escaping <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- | http-unsafe-output-escaping:
-- | Characters [> " '] reflected in parameter kalle at http://foobar.gazonk.se/xss.php?foo=bar&kalle=john
-- |_ Characters [> " '] reflected in parameter foo at http://foobar.gazonk.se/xss.php?foo=bar&kalle=john
--
-- @args http-unsafe-output-escaping.maxdepth the maximum amount of directories beneath
-- the initial url to spider. A negative value disables the limit.
-- (default: 3)
-- @args http-unsafe-output-escaping.maxpagecount the maximum amount of pages to visit.
-- A negative value disables the limit (default: 20)
-- @args http-unsafe-output-escaping.url the url to start spidering. This is a URL
-- relative to the scanned host eg. /default.html (default: /)
-- @args http-unsafe-output-escaping.withinhost only spider URLs within the same host.
-- (default: true)
-- @args http-unsafe-output-escaping.withindomain only spider URLs within the same
-- domain. This widens the scope from <code>withinhost</code> and can
-- not be used in combination. (default: false)
--
-- @see http-dombased-xss.nse
-- @see http-stored-xss.nse
-- @see http-phpself-xss.nse
-- @see http-xssed.nse
author = "Martin Holst Swende"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "intrusive"}
portrule = shortport.http
local dbg = stdnse.debug2
local function getHostPort(parsed)
return parsed.host, parsed.port or url.get_default_port(parsed.scheme)
end
local function getReflected(parsed, r)
local reflected_values,not_reflected_values = {},{}
local count = 0
-- Now, we need to check the parameters and keys
local q = url.parse_query(parsed.query)
-- Check the values (and keys) and see if they are reflected in the page
for k,v in pairs(q) do
if r.response.body and r.response.body:find(v, 1, true) then
dbg("Reflected content %s=%s", k,v)
reflected_values[k] = v
count = count +1
else
not_reflected_values[k] = v
end
end
if count > 0 then
return reflected_values,not_reflected_values,q
end
end
local function addPayload(v)
return v.."ghz>hzx\"zxc'xcv"
end
local function createMinedLinks(reflected_values, all_values)
local new_links = {}
for k,v in pairs(reflected_values) do
-- First of all, add the payload to the reflected param
local urlParams = { [k] = addPayload(v)}
for k2,v2 in pairs(all_values) do
if k2 ~= k then
urlParams[k2] = v2
end
end
new_links[k] = url.build_query(urlParams)
end
return new_links
end
local function locatePayloads(response)
local results = {}
if response.body:find("ghz>hzx") then table.insert(results,">") end
if response.body:find('hzx"zxc') then table.insert(results,'"') end
if response.body:find("zxc'xcv") then table.insert(results,"'") end
return #results > 0 and results
end
local function visitLinks(host, port,parsed,new_links, results,original_url)
for k,query in pairs(new_links) do
local ppath = url.parse_path(parsed.path or "")
local url = url.build_path(ppath)
if parsed.params then url = url .. ";" .. parsed.params end
url = url .. "?" .. query
dbg("Url to visit: %s", url)
local response = http.get(host, port, url)
local result = locatePayloads(response)
if result then
table.insert(results, ("Characters [%s] reflected in parameter %s at %s"):format(table.concat(result," "),k, original_url))
end
end
end
action = function(host, port)
local crawler = httpspider.Crawler:new(host, port, nil, { scriptname = SCRIPT_NAME } )
crawler:set_timeout(10000)
local results = {}
while(true) do
local status, r = crawler:crawl()
-- if the crawler fails it can be due to a number of different reasons
-- most of them are "legitimate" and should not be reason to abort
if ( not(status) ) then
if ( r.err ) then
return stdnse.format_output(false, r.reason)
else
break
end
end
-- parse the returned url
local parsed = url.parse(tostring(r.url))
-- We are only interested in links which have parameters
if parsed.query and #parsed.query > 0 then
local host, port = getHostPort(parsed)
local reflected_values,not_reflected_values,all_values = getReflected(parsed, r)
-- Now,were any reflected ?
if reflected_values then
-- Ok, create new links with payloads in the reflected slots
local new_links = createMinedLinks(reflected_values, all_values)
-- Now, if we had 2 reflected values, we should have 2 new links to fetch
visitLinks(host, port,parsed, new_links, results,tostring(r.url))
end
end
end
if ( #results> 0 ) then
return stdnse.format_output(true, results)
end
end