diff --git a/scripts/sql-injection.nse b/scripts/sql-injection.nse index bd4e19a8a..5bf1cbcfa 100644 --- a/scripts/sql-injection.nse +++ b/scripts/sql-injection.nse @@ -19,6 +19,8 @@ require('stdnse') require('strbuf') require('listop') require('comm') +require('http') +require('nsedebug') author = "Eddie Bell " license = "Same as Nmap--See http://nmap.org/book/man-legal.html" @@ -35,52 +37,6 @@ local try = nmap.new_try(catch) portrule = shortport.port_or_service({80, 443}, {"http","https"}) ---[[ -Download a page from host:port http server. The url is passed -straight to the get request, so shouldn't include the domain name ---]] - -local function get_page(host, port, httpurl) - local lines = "" - local status = true - local response = "" - local opts = {timeout=10000, recv_before=false} - - -- connect to webserver - --soc = nmap.new_socket() - --soc:set_timeout(4000) - --try(soc:connect(host.ip, port.number)) - - httpurl = string.gsub(httpurl, "&", "&") - --print(filename .. ": " .. httpurl) - - -- request page - local query = strbuf.new() - query = query .. "GET " .. httpurl .. " HTTP/1.1" - query = query .. "Accept: */*" - query = query .. "Accept-Language: en" - query = query .. "User-Agent: Mozilla/5.0 (compatible; Nmap Scripting Engine; http://nmap.org/book/nse.html)" - query = query .. "Host: " .. host.ip .. ":" .. port.number - --try(soc:send(strbuf.dump(query, '\r\n') .. '\r\n\r\n')) - - soc, response, bopt = comm.tryssl(host, port, strbuf.dump(query, '\r\n') .. '\r\n\r\n' , opts) - while true do - status, lines = soc:receive_lines(1) - if not status then break end - response = response .. lines - end - - soc:close() - return response -end - --- Curried function: so we don't have to pass port and host around -local function get_page_curried(host, port) - return function(url) - return get_page(host, port, url) - end -end - --[[ Pattern match response from a submitted injection query to see if it is vulnerable @@ -88,46 +44,72 @@ if it is vulnerable local function check_injection_response(response) - if not (string.find(response, 'HTTP/1.1 200 OK')) then - return false - end + response = string.lower(response) - response = string.lower(response) + if not (string.find(response, 'http/%d\.%d%s*[25]00')) then + return false + end - return (string.find(response, "invalid query") or - string.find(response, "sql syntax") or - string.find(response, "odbc drivers error")) + return (string.find(response, "invalid query") or + string.find(response, "sql syntax") or + string.find(response, "odbc drivers error")) end --[[ -Parse urls with queries and transform them into potentially -injectable urls. ---]] +Replaces usual queries with malicious querie and return a table with them. +]]-- -local function enumerate_inject_codes(injectable) - local utab, k, v, urlstr, response - local qtab, old_qtab, results +local function build_injection_vector(urls) + local utab, k, v, urlstr, response + local qtab, old_qtab, results + local all = {} - results = {} - utab = url.parse(injectable) - qtab = url.parse_query(utab.query) + 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] .. "'%20OR%20sqlspider" + for k, v in pairs(qtab) do + old_qtab = qtab[k]; + qtab[k] = qtab[k] .. "'%20OR%20sqlspider" + + utab.query = url.build_query(qtab) + urlstr = url.build(utab) + table.insert(all, urlstr) - utab.query = url.build_query(qtab) - urlstr = url.build(utab) - response = get_page_from_host(urlstr) + qtab[k] = old_qtab + utab.query = url.build_query(qtab) + end + end + end + return all +end - if (check_injection_response(response)) then - table.insert(results, urlstr) - end +--[[ +Creates a pipeline table and returns the result +]]-- +local function inject(host, port, injectable) + local all = {} + local pOpts = {} + pOpts.raw = true + for k, v in pairs(injectable) do + all = http.pGet(host, port, v, nil, nil, all) + end + return http.pipeline(host, port, all, pOpts) +end - qtab[k] = old_qtab - utab.query = url.build_query(qtab) - end - return results +--[[ +Checks is 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 --[[ @@ -137,26 +119,26 @@ it in find_links() --]] local function check_redirects(page) - local lpage = string.lower(page) - local _, httpurl = nil + local lpage = string.lower(page) + local _, httpurl = nil - -- meta redirects - if(string.find(lpage, '<%s*meta%s*http%-equiv%s*=%s*"%s*refresh%s*"')) then - _, _, httpurl = string.find(lpage, 'content%s*=%s*"%s*%d+%s*;%s*url%s*=%s*([^"]+)"') - if httpurl then - page = page .. 'href="' .. httpurl .. '"' - end - end + -- meta redirects + if(string.find(lpage, '<%s*meta%s*http%-equiv%s*=%s*"%s*refresh%s*"')) then + _, _, httpurl = string.find(lpage, 'content%s*=%s*"%s*%d+%s*;%s*url%s*=%s*([^"]+)"') + if httpurl then + page = page .. 'href="' .. httpurl .. '"' + end + end - -- http redirect - if(string.find(lpage, 'HTTP/1.1 301 moved permanently')) then - _, _, httpurl = string.find(lpage, 'location:%s*([^\n]+)') - if httpurl then - page = page .. 'href="' .. httpurl .. '"' - end - end + -- http redirect + if(string.find(lpage, 'HTTP/1.1 301 moved permanently')) then + _, _, httpurl = string.find(lpage, 'location:%s*([^\n]+)') + if httpurl then + page = page .. 'href="' .. httpurl .. '"' + end + end - return page + return page end --[[ @@ -165,11 +147,10 @@ away from current site! --]] local function is_local_link(url_parts, host) - if url_parts.authority and - not(url_parts.authority == host.name) then - return false - end - return true + if url_parts.authority and not(url_parts.authority == host.name) then + return false + end + return true end --[[ @@ -179,69 +160,83 @@ added to the inject list, which is returned. --]] local function find_links(list, base_path, page, host) - local httpurl,injectable, url_parts - local i, s, e + local httpurl,injectable, url_parts + local i, s, e - injectable = {} - url_parts = {} + injectable = {} + url_parts = {} - for w in string.gmatch(page, 'href%s*=%s*"%s*[^"]+%s*"') do - s, e = string.find(w, '"') - httpurl = string.sub(w, s+1, string.len(w)-1) - i = 1 + for w in string.gmatch(page, 'href%s*=%s*"%s*[^"]+%s*"') do + s, e = string.find(w, '"') + httpurl = string.sub(w, s+1, string.len(w)-1) + i = 1 - -- parse out duplicates, otherwise we'll be here all day - while list[i] and not(list[i] == httpurl) do - i = i + 1 - end + -- parse out duplicates, otherwise we'll be here all day + while list[i] and not(list[i] == httpurl) do + i = i + 1 + end - url_parts = url.parse(httpurl) + url_parts = url.parse(httpurl) - if list[i] == nil and is_local_link(url_parts, host) and - (not url_parts.scheme or url_parts.scheme == "http") then - httpurl = url.absolute(base_path, httpurl) - table.insert(list, httpurl) - if url_parts.query then - table.insert(injectable, httpurl) - end - end + if list[i] == nil and is_local_link(url_parts, host) and + (not url_parts.scheme or url_parts.scheme == "http") then + httpurl = url.absolute(base_path, httpurl) + table.insert(list, httpurl) + if url_parts.query then + table.insert(injectable, httpurl) end - return injectable + end + end + return injectable end action = function(host, port) - local urllist, results, injectable - local links, i, page + local urllist, injectable + local results = {} + local links, i, page + local injectableQs - i = 1 - urllist = {} - injectable = {} - get_page_from_host = get_page_curried(host, port) + i = 1 + urllist = {} + injectable = {} - -- start at the root - table.insert(urllist, "/") + -- start at the root + if nmap.registry.args['sql-injection.start'] then + table.insert(urllist, "/" .. nmap.registry.args['sql-injection.start']) + else + table.insert(urllist, "/") + end - while not(urllist[i] == nil) and i <= maxdepth do - page = get_page_from_host(urllist[i]) - page = check_redirects(page) - links = find_links(urllist, urllist[i], page, host) - -- store all urls with queries for later analysis - injectable = listop.append(injectable, links) - i = i + 1 - end + -- check for argument supplied max depth + if nmap.registry.args['sql-injection.maxdepth'] then + maxdepth = tonumber(nmap.registry.args['sql-injection.maxdepth']) + stdnse.print_debug("maxdepth set to: " .. maxdepth) + end - if #injectable > 0 then - stdnse.print_debug(1, "%s: Testing %d suspicious URLs", filename, #injectable ) - end + while not(urllist[i] == nil) and i <= maxdepth do + page = http.get(host, port, urllist[i], nil, nil) + page = check_redirects(page.body) + links = find_links(urllist, urllist[i], page, host) + -- store all urls with queries for later analysis + injectable = listop.append(injectable, links) + i = i + 1 + end - -- test all potentially vulnerable queries - results = listop.map(enumerate_inject_codes, injectable) - -- we can get multiple vulnerable URLS from a single query - results = listop.flatten(results); + if #injectable > 0 then + stdnse.print_debug(1, "%s: Testing %d suspicious URLs", filename, #injectable ) + -- test all potentially vulnerable queries + injectableQs = build_injection_vector(injectable) + local responses = inject(host, port, injectableQs) + results = check_responses(injectableQs, responses) + end - if not listop.is_empty(results) then - return "Host might be vulnerable\n" .. table.concat(results, '\n') - end + -- we can get multiple vulnerable URLS from a single query + --results = listop.flatten(results); - return nil + --if not listop.is_empty(results) then + if #results > 0 then + return "Host might be vulnerable\n" .. table.concat(results, '\n') + end + + return nil end