-- Send HTTP TRACE method, check if enabled, print modifications

-- Perform TRACE request to confirm support (OPTIONS will typically
-- state TRACE is enabled, but many servers will return 40x or 501 
-- Not Implemented). This script will also show any modifications 
-- made by intermediate servers or proxies between you and the 
-- target host, which you can use for diagnostic purposes (such as 
-- testing for web server or network problems).
-- NB: The HTTP TRACE method returns the contents of client HTTP 
-- requests in the entity-body of the TRACE response. Attackers 
-- could leverage this behavior to access sensitive information, 
-- such as cookies or authentication data, contained in the HTTP 
-- headers of the request. See US-CERT VU#867593 for more details.

-- 14/12/2007

id = "HTTP TRACE"

description = "Send HTTP TRACE method, check if enabled, print modifications"

author = "Kris Katterjohn <katterjohn@gmail.com>, Rob Nicholls <robert@everythingeverything.co.uk>"

license = "Look at Nmap's COPYING"

categories = {"discovery"}

require "shortport"
require "stdnse"

local truncate = function(tab)
	local str = ""
	str = str .. tab[1] .. "\n"
	str = str .. tab[2] .. "\n"
	str = str .. tab[3] .. "\n"
	str = str .. tab[4] .. "\n"
	str = str .. tab[5] .. "\n"
	return str
end

local validate = function(response, original, allegedservice)
	local start, stop
	local body
	local output
	local v

	v = nmap.verbosity()

	-- I have disabled this check until I find a decent way of making sure that nmap won't 
	-- try and use https against http (i.e. on a badly configured server, when nmap is 
	-- run without a version scan, so nmap simple grabs "https" from nmap-services
	-- this can there cause a false positive, as the check below can't differentiate 
	-- between a disconnect by the server or a failed connection (as trying to use ssl
	-- when we should be using tcp).
	-- Some web servers (e.g. Hauppauge EPG) simply disconnect if a TRACE request 
	-- is sent (or anything other than GET is sent).
	--if response == "" then
	--	if v > 2 then
	--		return "TRACE is not enabled\n"
	--	else
	--		return
	--	end
	--end

	-- Specific case for 301. As TRACE failed, we can return the inconclusive message.
	-- This appears to happen when IIS is configured to redirect to another location 
	-- (e.g. redirect from HTTP to HTTPS on the same server) as this appears to occur 
	-- before it'll confirm or deny support for TRACE. Ideally we'd follow the redirect 
	-- and perform a TRACE against that, but we have no  easy way of knowing if the 
	-- redirect is to a location that's still on this server.
	if response:match("HTTP/1.[01] 301 Moved Permanently") then
		if v > 1 then
			return "Result of TRACE check is inconclusive (301 Moved Permanently)\n"
		else
			return
		end
	end

	-- Specific case for 302. As TRACE failed, we can return the inconclusive message.
	-- This typically happens when Apache is configured to redirect to another location
	-- as this occurs before it'll confirm or deny support for TRACE. If IIS7 is configured
	-- to redirect (302) requests for the root and TRACE is disabled, it appears to return
	-- a 404, so you should never see a 302 if using IIS7 and TRACE is disabled. I think.
	-- Ideally we'd follow the redirect and perform a TRACE against that, but we have no 
	-- easy way of knowing if the redirect is to a location that's still on this server.
	if response:match("HTTP/1.[01] 302") then
		if v > 1 then
			return "Result of TRACE check is inconclusive (302 Redirect)\n"
		else
			return
		end
	end

	-- 307 should only be seen with HTTP/1.1 request, we're performing HTTP/1.0
	-- presumably to avoid having to send a valid Host header, so we should never
	-- see this error code, which is theoretically what we should get instead of
	-- the 302 redirects that are typically returned by servers.

	-- 400 should mean it didn't like the verb, as the request should be valid; 501 
	-- is IIS6's way of telling us that it's not supported (for the root, it may be 
	-- enabled on specific sub-directories). 501 is also returned by some consumer 
	-- devices, such as the web interface on Netgear routers.
	if response:match("HTTP/1.[01] 400 Bad Request") or
	   response:match("HTTP/1.[01] 501 Not Implemented") then
		if v > 2 then
			return "TRACE is not enabled\n"
		else
			return
		end
	end

	-- 403 usually means mod_rewrite is catching the request and rewriting
	-- it to return forbidden. Ideally we need to check if a GET request
	-- returns 200 OK, so we can conclusively prove TRACE is disabled, as we
	-- might see this if you'd normally get a 403 when performing a GET.
	if response:match("HTTP/1.[01] 403 Forbidden") then
		if v > 2 then
			return "TRACE appears to be forbidden\n"
		else
			return
		end
	end

	-- 404 appears (only tested against www.microsoft.com at this point) to be returned by 
	-- IIS7 when performing TRACE against the root. Check specifically for HTTP/1.1
	-- as this is returned by IIS7 even if the initial TRACE request was HTTP/1.0.
	-- NB: Not entirely sure how accurate this is, but I can't think of any other servers
	-- that return like this.
	if response:match("HTTP/1.1 404 Not Found") then
		if v > 2 then
			return "TRACE is not enabled\n"
		else
			return
		end
	end

	-- check that our TRACE request looks legitimate, otherwise return a more generic 
	-- message about TRACE support.
	if not response:match("HTTP/1.[01] 200") or
	   not response:match("TRACE / HTTP/1.0") then
		if v > 2 then
			if
				allegedservice == "https"
			then
				return "TRACE does not appear to be enabled (NB: you may want to perform a version scan)\n"
			else
				return "TRACE does not appear to be enabled\n"
			end
		else
			return
		end
	end

	start, stop = response:find("\r\n\r\n")
	body = response:sub(stop + 1)

	output = "TRACE is enabled, this is bad practice\n"

	if original ~= body then
		output = output .. "Response differs from request.  "

		if body:match("^TRACE / HTTP/1.0\r\n") then
			local extra = body:sub(19) -- skip TRACE line
			local tab = {}

			-- Skip extra newline at the end (making sure it's there)
			extra = extra:gsub("\r\n\r\n$", "\r\n")

			tab = stdnse.strsplit("\r\n", extra)

			if #tab > 5 then
				output = output .. "First 5 additional lines:\n"
				return output .. truncate(tab)
			end

			output = output .. "Additional lines:\n"
			return output .. extra .. "\n"
		end

		-- This shouldn't happen

		output = output .. "Full response:\n"
		return output .. body .. "\n"
	end

	-- so that we still display the message about TRACE being enabled
	-- even if the body is the same as the original
	return output

end

-- make it run on any port that looks like it's running http or https
-- NB: https is detected as "ssl" on Windows after a version scan, default 
-- scan settings will show "https" from nmap-services
portrule = function(host, port) 
	-- this might have trouble if someone decided to run https on
	-- port 80 and a service scan isn't performed. should we allow
	-- other http services, such as 
	if 
		port.service == "http"
		or port.service == "http-alt"
		and port.protocol == "tcp" 
		and port.state == "open"
	then
		return true
	end

	-- check for service scan results
	if 
		port.service == "http"
		and port.version.service_tunnel == "ssl"
		and port.state == "open" 
	then
		-- Don't bother running on SSL ports if we don't have SSL.
		if not nmap.have_ssl() then
			return false
		else
			-- if we got this far, SSL is supported
			return true
		end
	end

	-- if service scan not performed, we'll see https (even if it's not https)
	if 
		port.service == "https"
		or port.service == "https-alt"
		and port.state == "open" 
	then
		-- Don't bother running on SSL ports if we don't have SSL.
		if not nmap.have_ssl() then
			return false
		else
			-- if we got this far, SSL is supported
			return true
		end
	end


	-- no matches, stop gracefully
	return false

end


action = function(host, port)
	local cmd, response
	local socket
	local protocol

	socket = nmap.new_socket()

	-- make sure that anything that resembles ssl is going to use ssl protocol
	-- NB: without a service scan to confirm, https could really be http (or anything!)
	if
		port.service == "https"
		or port.service == "https-alt"
		or port.version.service_tunnel == "ssl"
	then
		protocol = "ssl"
	else
		protocol = "tcp"
	end

	socket:connect(host.ip, port.number, protocol)

	cmd = "TRACE / HTTP/1.0\r\n\r\n"
	-- NB: If support is added to this script so that the hostname can be passed
	-- using --script-args, we could also run checks using HTTP/1.1 followed by 
	-- the Host header, and if the script ever checked the Location redirect, it 
	-- might be able to determine that it's to a file on the same host, and TRACE 
	-- could be performed against that when testing is otherwise inconclusive 
	-- (e.g. Apache redirects before mod_rewrite can return 403 Forbidden).

	socket:send(cmd)

	response = ""

	while true do
		local status, lines = socket:receive_lines(1)

		if not status then
			break
		end

		response = response .. lines
	end

	socket:close()

	return validate(response, cmd, port.service)
end

