diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/http-sql-injection.nse | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/scripts/http-sql-injection.nse b/scripts/http-sql-injection.nse new file mode 100644 index 0000000..84f4460 --- /dev/null +++ b/scripts/http-sql-injection.nse @@ -0,0 +1,285 @@ +local http = require "http" +local httpspider = require "httpspider" +local io = require "io" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local url = require "url" + +description = [[ +Spiders an HTTP server looking for URLs containing queries vulnerable to an SQL +injection attack. It also extracts forms from found websites and tries to identify +fields that are vulnerable. + +The script spiders an HTTP server looking for URLs containing queries. It then +proceeds to combine crafted SQL commands with susceptible URLs in order to +obtain errors. The errors are analysed to see if the URL is vulnerable to +attack. This uses the most basic form of SQL injection but anything more +complicated is better suited to a standalone tool. + +We may not have access to the target web server's true hostname, which can prevent access to +virtually hosted sites. +]] + + +author = {"Eddie Bell", "Piotr Olma"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"intrusive", "vuln"} + +--- +-- @see http-vuln-cve2014-3704.nse +-- +-- @args http-sql-injection.maxpagecount the maximum amount of pages to visit. +-- A negative value disables the limit (default: 20) +-- @args http-sql-injection.url the url to start spidering. This is a URL +-- relative to the scanned host eg. /default.html (default: /) +-- @args http-sql-injection.withinhost only spider URLs within the same host. +-- (default: true) +-- @args http-sql-injection.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) +-- @args http-sql-injection.errorstrings a path to a file containing the error +-- strings to search for (one per line, lines started with # are treated as +-- comments). The default file is nselib/data/http-sql-errors.lst +-- which was taken from fuzzdb project, for more info, see http://code.google.com/p/fuzzdb/. +-- If someone detects some strings in that file causing a lot of false positives, +-- then please report them to dev@nmap.org. +-- +-- @output +-- PORT STATE SERVICE +-- 80/tcp open http syn-ack +-- | http-sql-injection: +-- | Possible sqli for queries: +-- | http://foo.pl/forms/page.php?param=13'%20OR%20sqlspider +-- | Possible sqli for forms: +-- | Form at path: /forms/f1.html, form's action: a1/check1.php. Fields that might be vulnerable: +-- | f1text +-- | Form at path: /forms/a1/../f2.html, form's action: a1/check2.php. Fields that might be vulnerable: +-- |_ f2text +-- + + +portrule = shortport.port_or_service({80, 443}, {"http","https"}) + +--[[ +Pattern match response from a submitted injection query to see +if it is vulnerable +--]] + +local errorstrings = {} +local function check_injection_response(response) + + local body = string.lower(response.body) + + if not (response.status == 200 or response.status ~= 500) then + return false + end + + if errorstrings then + for _,e in ipairs(errorstrings) do + if string.find(body, e) then + stdnse.debug2("error string matched: %s", e) + return true + end + end + end + return false +end + +--[[ +Replaces usual queries with malicious query and return a table with them. +]]-- + +local function build_injection_vector(urls) + local utab, k, v, urlstr, response + local qtab, old_qtab, results + local all = {} + + for _, injectable in ipairs(urls) do + if type(injectable) == "string" then + utab = url.parse(injectable) + qtab = url.parse_query(utab.query) + + for k, v in pairs(qtab) do + old_qtab = qtab[k]; + qtab[k] = qtab[k] .. "' OR sqlspider" + + utab.query = url.build_query(qtab) + urlstr = url.build(utab) + table.insert(all, urlstr) + + qtab[k] = old_qtab + utab.query = url.build_query(qtab) + end + end + end + return all +end + +--[[ +Creates a pipeline table and returns the result +]]-- +local function inject(host, port, injectable) + local all = {} + for k, v in pairs(injectable) do + all = http.pipeline_add(v, nil, all, 'GET') + end + return http.pipeline_go(host, port, all) +end + +--[[ +Checks if received responses matches with usual sql error messages, +what potentially means that the host is vulnerable to sql injection. +]]-- +local function check_responses(queries, responses) + local results = {} + for k, v in pairs(responses) do + if (check_injection_response(v)) then + table.insert(results, queries[k]) + end + end + return results +end + +-- checks if a field is of type we want to check for sqli +local function sqli_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 sqli_field()) of a form +local function generate_safe_postdata(form) + local postdata = {} + for _,field in ipairs(form["fields"]) do + if sqli_field(field["type"]) then + postdata[field["name"]] = "sampleString" + end + end + return postdata +end + +local function generate_get_string(data) + local get_str = {"?"} + for name,value in pairs(data) do + get_str[#get_str+1]=url.escape(name).."="..url.escape(value).."&" + end + return table.concat(get_str) +end + +-- checks each field of a form to see if it's vulnerable to sqli +local function check_form(form, host, port, path) + local vulnerable_fields = {} + local postdata = generate_safe_postdata(form) + local sending_function, response + + local action_absolute = string.find(form["action"], "^https?://") + -- determine the path where the form needs to be submitted + local form_submission_path + if action_absolute then + form_submission_path = form["action"] + else + local path_cropped = string.match(path, "(.*/).*") + path_cropped = path_cropped and path_cropped or "" + form_submission_path = path_cropped..form["action"] + end + + -- determine should the form be sent by post or get + local sending_function + 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..generate_get_string(data), nil) end + end + + for _,field in ipairs(form["fields"]) do + if sqli_field(field["type"]) then + stdnse.debug2("checking field %s", field["name"]) + postdata[field["name"]] = "' OR sqlspider" + response = sending_function(postdata) + if response and response.body and response.status==200 then + if check_injection_response(response) then + vulnerable_fields[#vulnerable_fields+1] = field["name"] + end + end + postdata[field["name"]] = "sampleString" + end + end + return vulnerable_fields +end + +-- load error strings to the errorstrings table +local function get_error_strings(path) + local f = nmap.fetchfile(path) or path + if f then + for e in io.lines(f) do + if not string.match(e, "^#") then + table.insert(errorstrings, e:lower()) + end + end + end + -- check if we loaded something + if #errorstrings == 0 then + -- if not, then load some default values + errorstrings = {"invalid query", "sql syntax", "odbc drivers error"} + end +end + +action = function(host, port) + local error_strings_path = stdnse.get_script_args('http-sql-injection.errorstrings') or 'nselib/data/http-sql-errors.lst' + get_error_strings(error_strings_path) + -- crawl to find injectable urls + local crawler = httpspider.Crawler:new(host, port, nil, {scriptname = SCRIPT_NAME}) + local injectable = {} + local results_forms = {name="Possible sqli for forms:"} + + 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 sqli on forms + if r.response and r.response.body and r.response.status==200 then + local all_forms = http.grab_forms(r.response.body) + for _,form_plain in ipairs(all_forms) do + local form = http.parse_form(form_plain) + local path = r.url.path + if form and form.action then + local vulnerable_fields = check_form(form, host, port, path) + if #vulnerable_fields > 0 then + vulnerable_fields["name"] = "Form at path: "..path..", form's action: "..form["action"]..". Fields that might be vulnerable:" + table.insert(results_forms, vulnerable_fields) + end + end + end --for + end --if + local links = {} + if r.response.status and r.response.body then + links = httpspider.LinkExtractor:new(r.url, r.response.body, crawler.options):getLinks() + end + for _,u in ipairs(links) do + if url.parse(u).query then + table.insert(injectable, u) + end + end + end + + -- try to inject + local results_queries = {} + if #injectable > 0 then + stdnse.debug1("Testing %d suspicious URLs", #injectable) + local injectableQs = build_injection_vector(injectable) + local responses = inject(host, port, injectableQs) + results_queries = check_responses(injectableQs, responses) + end + + results_queries["name"] = "Possible sqli for queries:" + local res = {results_queries, results_forms} + return stdnse.format_output(true, res) +end + |