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.*)'
+ 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", } }