diff options
Diffstat (limited to 'scripts/http-rfi-spider.nse')
-rw-r--r-- | scripts/http-rfi-spider.nse | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/scripts/http-rfi-spider.nse b/scripts/http-rfi-spider.nse new file mode 100644 index 0000000..1d05049 --- /dev/null +++ b/scripts/http-rfi-spider.nse @@ -0,0 +1,283 @@ +description = [[ +Crawls webservers in search of RFI (remote file inclusion) vulnerabilities. It +tests every form field it finds and every parameter of a URL containing a +query. +]] + +--- +-- @usage +-- nmap --script http-rfi-spider -p80 <host> +-- +-- +-- @output +-- PORT STATE SERVICE REASON +-- 80/tcp open http +-- | http-rfi-spider: +-- | Possible RFI in form fields +-- | Form "(form 1)" at /experiments/rfihome.html (action rfi.pl) with fields: +-- | inc +-- | Form "someform" at /experiments/rfihome.html (action rfi.pl) with fields: +-- | inc2 +-- | Possible RFI in query parameters +-- | Path /experiments/rfi.pl with queries: +-- |_ inc=http%3a%2f%2ftools%2eietf%2eorg%2fhtml%2frfc13%3f +-- +-- @xmloutput +-- <table key="Forms"> +-- <table key="/experiments/rfihome.html"> +-- <table key="(form 1)"> +-- <table key="Vulnerable fields"> +-- <elem>inc</elem> +-- </table> +-- <elem key="Action">rfi.pl</elem> +-- </table> +-- <table key="someform"> +-- <table key="Vulnerable fields"> +-- <elem>inc2</elem> +-- </table> +-- <elem key="Action">rfi.pl</elem> +-- </table> +-- </table> +-- </table> +-- <table key="Queries"> +-- <table key="/experiments/rfi.pl"> +-- <elem>inc=http%3a%2f%2ftools%2eietf%2eorg%2fhtml%2frfc13%3f</elem> +-- </table> +-- </table> +-- +-- @args http-rfi-spider.inclusionurl the url we will try to include, defaults +-- to <code>http://tools.ietf.org/html/rfc13?</code> +-- @args http-rfi-spider.pattern the pattern to search for in <code>response.body</code> +-- to determine if the inclusion was successful, defaults to +-- <code>'20 August 1969'</code> +-- @args http-rfi-spider.maxdepth the maximum amount of directories beneath +-- the initial url to spider. A negative value disables the limit. +-- (default: 3) +-- @args http-rfi-spider.maxpagecount the maximum amount of pages to visit. +-- A negative value disables the limit (default: 20) +-- @args http-rfi-spider.url the url to start spidering. This is a URL +-- relative to the scanned host eg. /default.html (default: /) +-- @args http-rfi-spider.withinhost only spider URLs within the same host. +-- (default: true) +-- @args http-rfi-spider.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) +-- + +author = "Piotr Olma" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"intrusive"} + +local shortport = require 'shortport' +local http = require 'http' +local stdnse = require 'stdnse' +local url = require 'url' +local httpspider = require 'httpspider' +local string = require 'string' +local table = require 'table' +local tableaux = require 'tableaux' + +-- this is a variable that will hold the function that checks if a pattern we are searching for is in +-- response's body +local check_response + +-- this variable will hold the injection url +local inclusion_url + +-- checks if a field is of type we want to check for rfi +local function rfi_field(field_type) + return field_type=="text" or field_type=="radio" or field_type=="checkbox" or field_type=="textarea" +end + +-- generates postdata with value of "sampleString" for every field (that satisfies rfi_field()) of a form +local function generate_safe_postdata(form) + local postdata = {} + for _,field in ipairs(form["fields"]) do + if rfi_field(field["type"]) then + postdata[field["name"]] = "sampleString" + end + end + return postdata +end + +-- checks each field of a form to see if it's vulnerable to rfi +local function check_form(form, host, port, path) + local vulnerable_fields = {} + local postdata = generate_safe_postdata(form) + local sending_function, response + + local form_submission_path = url.absolute(path, form.action) + if form["method"]=="post" then + sending_function = function(data) return http.post(host, port, form_submission_path, nil, nil, data) end + else + sending_function = function(data) return http.get(host, port, form_submission_path.."?"..url.build_query(data), nil) end + end + + for _,field in ipairs(form["fields"]) do + if rfi_field(field["type"]) then + stdnse.debug2("checking field %s", field["name"]) + postdata[field["name"]] = inclusion_url + response = sending_function(postdata) + if response and response.body and response.status==200 then + if check_response(response.body) then + vulnerable_fields[#vulnerable_fields+1] = field["name"] + end + end + postdata[field["name"]] = "sampleString" + end + end + return vulnerable_fields +end + +-- builds urls with a query that would let us decide if a parameter is rfi vulnerable +local function build_urls(injectable) + local new_urls = {} + for _,u in ipairs(injectable) do + if type(u) == "string" then + local parsed_url = url.parse(u) + local old_query = url.parse_query(parsed_url.query) + for f,v in pairs(old_query) do + old_query[f] = inclusion_url + parsed_url.query = url.build_query(old_query) + table.insert(new_urls, url.build(parsed_url)) + old_query[f] = v + end + end + end + return new_urls +end + +-- as in sql-injection.nse +local function inject(host, port, injectable) + local all = nil + for k, v in pairs(injectable) do + all = http.pipeline_add(v, nil, all, 'GET') + end + return http.pipeline_go(host, port, all) +end + +local function check_responses(urls, responses) + if responses == nil or #responses==0 then + return {} + end + local suspects = {} + for i,r in ipairs(responses) do + if r.body then + if check_response(r.body) then + local parsed = url.parse(urls[i]) + if suspects[parsed.path] then + table.insert(suspects[parsed.path], parsed.query) + else + suspects[parsed.path] = {} + table.insert(suspects[parsed.path], parsed.query) + end + end + end + end + return suspects +end + +portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open") + +function action(host, port) + inclusion_url = stdnse.get_script_args('http-rfi-spider.inclusionurl') or 'http://tools.ietf.org/html/rfc13?' + local pattern_to_search = stdnse.get_script_args('http-rfi-spider.pattern') or '20 August 1969' + + -- once we know the pattern we'll be searching for, we can set up the function + check_response = function(body) return string.find(body, pattern_to_search) end + + -- create a new crawler instance + local crawler = httpspider.Crawler:new( host, port, nil, { scriptname = SCRIPT_NAME} ) + + if ( not(crawler) ) then + return + end + + local output = stdnse.output_table() + output.Forms = stdnse.output_table() + output.Queries = stdnse.output_table() + + while(true) do + local status, r = crawler:crawl() + + if ( not(status) ) then + if ( r.err ) then + return stdnse.format_output(false, r.reason) + else + break + end + end + + -- first we try rfi on forms + if r.response and r.response.body and r.response.status==200 then + local path = r.url.path + local all_forms = http.grab_forms(r.response.body) + for seq, form_plain in ipairs(all_forms) do + local form = http.parse_form(form_plain) + if form and form.action then + local vulnerable_fields = check_form(form, host, port, path) + if #vulnerable_fields > 0 then + local out_form = stdnse.output_table() + out_form["Action"] = form.action + out_form["Vulnerable fields"] = vulnerable_fields + if not output.Forms[path] then output.Forms[path] = stdnse.output_table() end + output.Forms[path][form.id or string.format("(form %d)", seq)] = out_form + end + end + end --for + end --if + + -- now try inclusion by query parameters + local injectable = {} + -- search for injectable links (as in sql-injection.nse) + if r.response.status and r.response.body then + local links = httpspider.LinkExtractor:new(r.url, r.response.body, crawler.options):getLinks() + for _,u in ipairs(links) do + local url_parsed = url.parse(u) + if url_parsed.query then + table.insert(injectable, u) + end + end + end + if #injectable > 0 then + local new_urls = build_urls(injectable) + local responses = inject(host, port, new_urls) + local suspects = check_responses(new_urls, responses) + for p, q in pairs(suspects) do + local queries_out = output.Queries[p] or {} + for _, query in ipairs(q) do + queries_out[#queries_out+1] = query + end + output.Queries[p] = queries_out + end + end + end + + local text_output = {} + if #output.Forms > 0 then + local rfi = { name = "Possible RFI in form fields" } + for path, forms in pairs(output.Forms) do + for fid, fobj in pairs(forms) do + local out = tableaux.shallow_tcopy(fobj["Vulnerable fields"]) + out.name = string.format('Form "%s" at %s (action %s) with fields:', + fid, path, fobj["Action"]) + table.insert(rfi, out) + end + end + table.insert(text_output, rfi) + end + if #output.Queries > 0 then + local rfi = { name = "Possible RFI in query parameters" } + for path, queries in pairs(output.Queries) do + local out = tableaux.shallow_tcopy(queries) + out.name = string.format('Path %s with queries:', path) + table.insert(rfi, out) + end + table.insert(text_output, rfi) + end + + if #text_output > 0 then + return output, stdnse.format_output(true, text_output) + end +end + |