284 lines
8.3 KiB
Plaintext
284 lines
8.3 KiB
Plaintext
|
description = [[
|
||
|
Posts specially crafted strings to every form it
|
||
|
encounters and then searches through the website for those
|
||
|
strings to determine whether the payloads were successful.
|
||
|
]]
|
||
|
|
||
|
---
|
||
|
-- @usage nmap -p80 --script http-stored-xss.nse <target>
|
||
|
--
|
||
|
-- This script works in two phases.
|
||
|
-- 1) Posts specially crafted strings to every form it encounters.
|
||
|
-- 2) Crawls through the page searching for these strings.
|
||
|
--
|
||
|
-- If any string is reflected on some page without any proper
|
||
|
-- HTML escaping, it's a sign for potential XSS vulnerability.
|
||
|
--
|
||
|
-- @args http-stored-xss.formpaths The pages that contain
|
||
|
-- the forms to exploit. For example, {/upload.php, /login.php}.
|
||
|
-- Default: nil (crawler mode on)
|
||
|
-- @args http-stored-xss.uploadspaths The pages that reflect
|
||
|
-- back POSTed data. For example, {/comments.php, /guestbook.php}.
|
||
|
-- Default: nil (Crawler mode on)
|
||
|
-- @args http-stored-xss.fieldvalues The script will try to
|
||
|
-- fill every field found in the form but that may fail due to
|
||
|
-- fields' restrictions. You can manually fill those fields using
|
||
|
-- this table. For example, {gender = "male", email = "foo@bar.com"}.
|
||
|
-- Default: {}
|
||
|
-- @args http-stored-xss.dbfile The path of a plain text file
|
||
|
-- that contains one XSS vector per line. Default: nil
|
||
|
--
|
||
|
-- @output
|
||
|
-- PORT STATE SERVICE REASON
|
||
|
-- 80/tcp open http syn-ack
|
||
|
-- | http-stored-xss:
|
||
|
-- | Found the following stored XSS vulnerabilities:
|
||
|
-- |
|
||
|
-- | Payload: ghz>hzx
|
||
|
-- | Uploaded on: /guestbook.php
|
||
|
-- | Description: Unfiltered '>' (greater than sign). An indication of potential XSS vulnerability.
|
||
|
-- | Payload: zxc'xcv
|
||
|
-- | Uploaded on: /guestbook.php
|
||
|
-- | Description: Unfiltered ' (apostrophe). An indication of potential XSS vulnerability.
|
||
|
-- |
|
||
|
-- | Payload: ghz>hzx
|
||
|
-- | Uploaded on: /posts.php
|
||
|
-- | Description: Unfiltered '>' (greater than sign). An indication of potential XSS vulnerability.
|
||
|
-- | Payload: hzx"zxc
|
||
|
-- | Uploaded on: /posts.php
|
||
|
-- |_ Description: Unfiltered " (double quotation mark). An indication of potential XSS vulnerability.
|
||
|
--
|
||
|
-- @see http-dombased-xss.nse
|
||
|
-- @see http-phpself-xss.nse
|
||
|
-- @see http-xssed.nse
|
||
|
-- @see http-unsafe-output-escaping.nse
|
||
|
|
||
|
categories = {"intrusive", "exploit", "vuln"}
|
||
|
author = "George Chatzisofroniou"
|
||
|
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||
|
|
||
|
local http = require "http"
|
||
|
local io = require "io"
|
||
|
local string = require "string"
|
||
|
local httpspider = require "httpspider"
|
||
|
local shortport = require "shortport"
|
||
|
local stdnse = require "stdnse"
|
||
|
local table = require "table"
|
||
|
|
||
|
portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
|
||
|
|
||
|
|
||
|
-- A list of payloads.
|
||
|
--
|
||
|
-- You can manually add / remove your own payloads but make sure you
|
||
|
-- don't mess up, otherwise the script may succeed when it actually
|
||
|
-- hasn't.
|
||
|
--
|
||
|
-- Note, that more payloads will slow down your scan.
|
||
|
payloads = {
|
||
|
|
||
|
-- Basic vectors. Each one is an indication of potential XSS vulnerability.
|
||
|
{ vector = 'ghz>hzx', description = "Unfiltered '>' (greater than sign). An indication of potential XSS vulnerability." },
|
||
|
{ vector = 'hzx"zxc', description = "Unfiltered \" (double quotation mark). An indication of potential XSS vulnerability." },
|
||
|
{ vector = 'zxc\'xcv', description = "Unfiltered ' (apostrophe). An indication of potential XSS vulnerability." },
|
||
|
}
|
||
|
|
||
|
|
||
|
-- Create customized requests for all of our payloads.
|
||
|
local makeRequests = function(host, port, submission, fields, fieldvalues)
|
||
|
|
||
|
local postdata = {}
|
||
|
for _, p in ipairs(payloads) do
|
||
|
for __, field in ipairs(fields) do
|
||
|
if field["type"] == "text" or field["type"] == "textarea" or field["type"] == "radio" or field["type"] == "checkbox" then
|
||
|
|
||
|
local value = fieldvalues[field["name"]]
|
||
|
if value == nil then
|
||
|
value = p.vector
|
||
|
end
|
||
|
|
||
|
postdata[field["name"]] = value
|
||
|
|
||
|
end
|
||
|
end
|
||
|
|
||
|
stdnse.debug2("Making a POST request to " .. submission .. ": ")
|
||
|
for i, content in pairs(postdata) do
|
||
|
stdnse.debug2(i .. ": " .. content)
|
||
|
end
|
||
|
local response = http.post(host, port, submission, { no_cache = true }, nil, postdata)
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
local checkPayload = function(body, p)
|
||
|
|
||
|
if (body:match(p)) then
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
-- Check if the payloads were successful by checking the content of pages in the uploadspaths array.
|
||
|
local checkRequests = function(body, target)
|
||
|
|
||
|
local output = {}
|
||
|
for _, p in ipairs(payloads) do
|
||
|
if checkPayload(body, p.vector) then
|
||
|
local report = " Payload: " .. p.vector .. "\n\t Uploaded on: " .. target
|
||
|
if p.description then
|
||
|
report = report .. "\n\t Description: " .. p.description
|
||
|
end
|
||
|
table.insert(output, report)
|
||
|
end
|
||
|
end
|
||
|
return output
|
||
|
end
|
||
|
|
||
|
local readFromFile = function(filename)
|
||
|
local database = { }
|
||
|
for l in io.lines(filename) do
|
||
|
table.insert(payloads, { vector = l })
|
||
|
end
|
||
|
end
|
||
|
|
||
|
action = function(host, port)
|
||
|
|
||
|
local formpaths = stdnse.get_script_args("http-stored-xss.formpaths")
|
||
|
local uploadspaths = stdnse.get_script_args("http-stored-xss.uploadspaths")
|
||
|
local fieldvalues = stdnse.get_script_args("http-stored-xss.fieldvalues") or {}
|
||
|
local dbfile = stdnse.get_script_args("http-stored-xss.dbfile")
|
||
|
|
||
|
if dbfile then
|
||
|
readFromFile(dbfile)
|
||
|
end
|
||
|
|
||
|
local returntable = {}
|
||
|
local result
|
||
|
|
||
|
local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME, no_cache = true } )
|
||
|
|
||
|
if (not(crawler)) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
crawler:set_timeout(10000)
|
||
|
|
||
|
local index, k, target, response
|
||
|
|
||
|
-- Phase 1. Crawls through the website and POSTs malicious payloads.
|
||
|
while (true) do
|
||
|
|
||
|
if formpaths then
|
||
|
|
||
|
k, target = next(formpaths, index)
|
||
|
if (k == nil) then
|
||
|
break
|
||
|
end
|
||
|
response = http.get(host, port, target, { no_cache = true })
|
||
|
target = host.name .. target
|
||
|
else
|
||
|
|
||
|
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
|
||
|
|
||
|
target = tostring(r.url)
|
||
|
response = r.response
|
||
|
|
||
|
end
|
||
|
|
||
|
if response.body then
|
||
|
|
||
|
local forms = http.grab_forms(response.body)
|
||
|
|
||
|
for i, form in ipairs(forms) do
|
||
|
|
||
|
form = http.parse_form(form)
|
||
|
|
||
|
if form and form.action then
|
||
|
|
||
|
local action_absolute = string.find(form["action"], "https*://")
|
||
|
|
||
|
-- Determine the path where the form needs to be submitted.
|
||
|
local submission
|
||
|
if action_absolute then
|
||
|
submission = form["action"]
|
||
|
else
|
||
|
local path_cropped = string.match(target, "(.*/).*")
|
||
|
path_cropped = path_cropped and path_cropped or ""
|
||
|
submission = path_cropped..form["action"]
|
||
|
end
|
||
|
|
||
|
makeRequests(host, port, submission, form["fields"], fieldvalues)
|
||
|
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if (index) then
|
||
|
index = index + 1
|
||
|
else
|
||
|
index = 1
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME } )
|
||
|
local index
|
||
|
|
||
|
-- Phase 2. Crawls through the website and searches for the special crafted strings that were POSTed before.
|
||
|
while true do
|
||
|
if uploadspaths then
|
||
|
k, target = next(uploadspaths, index)
|
||
|
if (k == nil) then
|
||
|
break
|
||
|
end
|
||
|
response = http.get(host, port, target)
|
||
|
else
|
||
|
|
||
|
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
|
||
|
|
||
|
target = tostring(r.url)
|
||
|
response = r.response
|
||
|
|
||
|
end
|
||
|
|
||
|
if response.body then
|
||
|
|
||
|
result = checkRequests(response.body, target)
|
||
|
|
||
|
if next(result) then
|
||
|
table.insert(returntable, result)
|
||
|
end
|
||
|
end
|
||
|
if (index) then
|
||
|
index = index + 1
|
||
|
else
|
||
|
index = 1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if next(returntable) then
|
||
|
table.insert(returntable, 1, "Found the following stored XSS vulnerabilities: ")
|
||
|
return returntable
|
||
|
else
|
||
|
return "Couldn't find any stored XSS vulnerabilities."
|
||
|
end
|
||
|
end
|