From 855bdbd2897f60960ffdbc7062b13ce351baaa80 Mon Sep 17 00:00:00 2001 From: kroosec Date: Sun, 20 May 2012 15:42:33 +0000 Subject: [PATCH] Added http-traceroute script which exploits Max-Forwards HTTP header to detect reverse proxies. --- CHANGELOG | 3 + scripts/http-traceroute.nse | 190 ++++++++++++++++++++++++++++++++++++ scripts/script.db | 1 + 3 files changed, 194 insertions(+) create mode 100644 scripts/http-traceroute.nse diff --git a/CHANGELOG b/CHANGELOG index ad59e4093..1e992c832 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script http-traceroute, which exploits the + Max-Forwards HTTP header to detect reverse proxies. [Hani Benhabiles] + o Added the script distcc-CVE-2004-2687 that checks and exploits a remote command execution vulnerability in distcc. [Patrik Karlsson] diff --git a/scripts/http-traceroute.nse b/scripts/http-traceroute.nse new file mode 100644 index 000000000..943d521e5 --- /dev/null +++ b/scripts/http-traceroute.nse @@ -0,0 +1,190 @@ +description = [[ +Exploits the Max-Forwards HTTP header to detect the presence of reverse proxies. + +The script works by sending HTTP requests with values of the Max-Forwards HTTP header varying +from 0 to 2 and checking for any anomalies in certain response values such as the status code, +Server, Content-Type and Content-Length HTTP headers and body values such as the html title. + +Based on the work of: +* Nicolas Gregoire (nicolas.gregoire@agarri.fr) +* Julien Cayssol (tools@aqwz.com) + +For more information, see: + * http://www.agarri.fr/kom/archives/2011/11/12/traceroute-like_http_scanner/index.html +]] + +--- +-- @args http-traceroute.path The path to send requests to. Defaults to /. +-- @args http-traceroute.method HTTP request method to use. Defaults to GET. +-- among other values, TRACE is probably the most interesting. +-- +-- @usage +-- nmap --script=http-traceroute --script-args http-traceroute.verbosity=1 +-- +--@output +-- PORT STATE SERVICE REASON +-- 80/tcp open http syn-ack +-- | http-traceroute: +-- | HTML title +-- | Hop #1: Twitter / Over capacity +-- | Hop #2: t.co / Twitter +-- | Hop #3: t.co / Twitter +-- | Status Code +-- | Hop #1: 502 +-- | Hop #2: 200 +-- | Hop #3: 200 +-- | server +-- | Hop #1: Apache +-- | Hop #2: hi +-- | Hop #3: hi +-- | content-type +-- | Hop #1: text/html; charset=UTF-8 +-- | Hop #2: text/html; charset=utf-8 +-- | Hop #3: text/html; charset=utf-8 +-- | content-length +-- | Hop #1: 4833 +-- | Hop #2: 3280 +-- | Hop #3: 3280 +-- | last-modified +-- | Hop #1: Thu, 05 Apr 2012 00:19:40 GMT +-- | Hop #2 +-- |_ Hop #3 + +author = "Hani Benhabiles" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"discovery", "safe", "default"} + +require 'http' +require 'stdnse' +require 'shortport' + +portrule = shortport.service("http") + +--- Attempts to extract the html title +-- from an HTTP response body. +--@param responsebody Response's body. +local extract_title = function(responsebody) + local title = '' + local titlere = '(?P<title>.*)' + local regex = pcre.new(titlere, 0, "C") + local limit, limit2, matches = regex:match(responsebody) + if limit ~= nil then + title = matches["title"] + end + return title +end + +--- Attempts to extract the X-Forwarded-For header +-- from an HTTP response body in case of TRACE requests. +--@param responsebody Response's body. +local extract_xfwd = function(responsebody) + local xfwd = '' + local xfwdre = '(?PX-Forwarded-For: .*)' + local regex = pcre.new(xfwdre, 0, "C") + local limit, limit2, matches = regex:match(responsebody) + if limit ~= nil then + xfwd = matches["xfwd"] + end + return xfwd +end + +--- Check for differences in response headers, status code +-- and html title between responses. +--@param responses Responses to compare. +--@param method Used HTTP method. +local compare_responses = function(responses, method) + local response, key + local results = {} + local result = {} + local titles = {} + local interesting_headers = { + 'server', + 'via', + 'x-via', + 'x-forwarded-for', + 'content-type', + 'content-length', + 'last-modified', + 'location', + } + + -- Check page title + for key,response in pairs(responses) do + titles[key] = extract_title(response.body) + end + if titles[1] ~= titles[2] or + titles[1] ~= titles[3] then + + table.insert(results, 'HTML title') + for key,response in pairs(responses) do + table.insert(result, "Hop #" .. key .. ": " .. titles[key]) + end + table.insert(results, result) + end + + -- Check status code + if responses[1].status == 502 or + responses[1].status == 483 or + responses[1].status ~= responses[2].status or + responses[1].status ~= responses[3].status then + + result = {} + table.insert(results, 'Status Code') + for key,response in pairs(responses) do + table.insert(result, "Hop #" .. key .. ": " .. tostring(response.status)) + end + table.insert(results, result) + end + + -- Check headers + for _,header in pairs(interesting_headers) do + -- Compare header of different responses + if responses[1].header[header] ~= responses[2].header[header] or + responses[1].header[header] ~= responses[3].header[header] then + + result = {} + table.insert(results, header) + for key,response in pairs(responses) do + if response.header[header] ~= nil then + table.insert(result, "Hop #" .. key .. ": " .. tostring(response.header[header])) + else + table.insert(result, "Hop #" .. key) + end + end + table.insert(results, result) + end + end + + -- Check for X-Forwarded-For in the response body + -- when using TRACE method + if method == "TRACE" then + local xfwd = extract_xfwd(responses[1].body) + if xfwd ~= nil then + table.insert(results, xfwd) + end + end + + return results +end + +action = function(host, port) + local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or "/" + local method = stdnse.get_script_args(SCRIPT_NAME .. '.method') or "GET" + local responses = {} + local detected = "Possible reverse proxy detected." + + for i = 0,2 do + local response = http.generic_request(host, port, method, path, { ['header'] = { ['Max-Forwards'] = i }, ['no_cache'] = true}) + table.insert(responses, response) + end + + -- Check results + local results = compare_responses(responses, method) + if results ~= nil and nmap.verbosity() == 1 then + return stdnse.format_output(true,detected) + else + return stdnse.format_output(true,results) + end +end diff --git a/scripts/script.db b/scripts/script.db index 06a4a1e62..ccbfeb89d 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -165,6 +165,7 @@ Entry { filename = "http-robtex-reverse-ip.nse", categories = { "discovery", "ex Entry { filename = "http-robtex-shared-ns.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "http-title.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "http-trace.nse", categories = { "discovery", "safe", "vuln", } } +Entry { filename = "http-traceroute.nse", categories = { "discovery", "safe", "default", } } Entry { filename = "http-unsafe-output-escaping.nse", categories = { "discovery", "intrusive", } } Entry { filename = "http-userdir-enum.nse", categories = { "auth", "intrusive", } } Entry { filename = "http-vhosts.nse", categories = { "discovery", "intrusive", } }