Layer 7 Denial of Service - R.U.D.Y.23 Nov 2015
#Intro to R.U.D.Y. attacks
I’m a huge Mr. Robot fan. In one of the most memorable scenes of the pilot episode, Elliot is called in to work to stop what appears to be a massive DDoS attack against EvilCorp’s servers. When it becomes clear that the attack is not a DDoS at all, and that he’s dealing with a much more subtle threat from within EvilCorp’s infrastructure, he exclaims excitedly “Is this a R.U.D.Y. attack? This is awesome!”
R.U.D.Y. attacks are actually a real thing, and are quite awesome indeed. They were first written about by Hybrid Security, who named their proof of concept r-u-dead-yet after the song by Children of Bodom. R.U.D.Y is an application layer attack. This means that unlike volume or protocol based attacks, the goal of a R.U.D.Y. is to use seemingly legitimate HTTP traffic to crash the target web server.
R.U.D.Y. is what is known as a Slow POST DoS attack. Slow POSTs work by sending a legitimate HTTP header to the target server, then sending the HTTP body slowly enough to consume an entire thread on the target for an extended period of time. R.U.D.Y. attacks do this by setting the Content-Length field in the HTTP header to an extremely high value, then sending the POST data one byte at a time while the target server waits for the entire request to complete.
In this post we will build a tool for launching R.U.D.Y. attacks, in direct homage of the original r-u-dead-yet script, taking a close look at the core logic that makes them so effective. We will then run our tool against an actual target so we can see a R.U.D.Y. attack in action. We will then go over effective mitigation techniques that can be used to protect against these types of attacks.
Building RU-DEAD-YET - Initial Setup
Let’s first create a new project directory called rudydos, along with a new virtualenv within it:
Let’s then activate our virtual environment:
Next we create two new files - pip.req and run.py:
We then open up our pip.req file and add these lines to enumerate dependencies:
requests requesocks beautifulsoup4 SocksiPy
Finally, we use pip to install all of the dependencies we just listed in our pip.req file:
Building RU-DEAD-YET - Core Logic
Let’s open up run.py. The first thing we want to do is create a function to craft the special HTTP headers we’ll be using for this attack. Notice how the Content-Length field is ridiculously long. This is super long Content-Length is one of the keys to getting this DOS attack to work. We also send the start of our POST parameter, but without sending any actual content.
Our launch_attack() function takes three arguments - cid, configs, and headers. The cid (connection id) argument is an id number for the process that we set in the calling function. We use it to identify what process we are currently in as we are printing output to the terminal. The second parameter, configs, is a dict containing the target host, target port, sleep time, and possibly proxy information if proxies are currently enabled. The third parameter contains the specially crafted HTTP headers we made using craft_headers().
The first thing our function does is establish a TCP connection to our target. It then sends the HTTP request headers to the target, with content-length set to one million bytes. It then begins to send the HTTP body one byte at a time, pausing for configs[‘sleep_time’] seconds between each byte.
1,000,000 bytes x 10 seconds / 60 / 60 / 24 / 30 == approx 3.8 months
To illustrate just how slow this is, suppose configs[‘sleep_time’] is set to 10 seconds. Then with content-length set to 1 million, it will take nearly 4 months for the request to complete. This effectively means that we have rendered one of the target server’s worker threads completely useless. If we run this function as its own process (which we will) multiple times in parallel, we can easily tie up every worker thread the target server has, causing a denial of service.
We throw the whole thing in try-catch block so that we can exit cleanly on user interrupt.
The first thing that we’re going to do in our driver code is create a config dictionary using configure(). Our configs dictionary will contain the following 8 items that will be used in our driver code:
configs[‘connections’] - How many simultaneous connections to make to the target.
configs[‘path’] - The relative path of the target url. We need this information in order to craft HTTP headers from scratch.
configs[‘host’] - The domain name of the target URL. Once again, we’ll be using this information to create valid HTTP headers to send to the target.
configs[‘port’] - The port at which to send our HTTP requests.
configs[‘target’] - The URL to which we will be making our slow POST requests. This is obtained by scraping the target URL provided by the user for html forms, then parsing the form’s ‘action’ attribute.
configs[‘cookies’] - Any cookies required by the target server. We obtain these in the configure() function by making a GET request to the target server, then checking the response headers for the ‘Set-Cookie’ field.
configs[‘param’] - We will need a valid POST parameter in which to send our data. As we did with our ‘target’ config, configure() obtains this information by scraping the target for html forms, extracting an html input tag, then parsing the input tag’s ‘name’ parameter.
configs[‘user_agents’] - A list of user agents for use in our http headers. We’ll be setting a different user agent per connection to make our script harder to ban.
Once we’ve created our configs dictionary using configure(), we start spawning child processes to run the launch_attack() function. Each child process gets a unique HTTP header with a randomly selected user agent. Once we have launched all of our child processes, we wait for them to finish running and join with their parent process. Since this will probably take a really long time to happen, we throw the whole block of code in a try-catch block so that the user can cleanly terminate the script by pressing ctrl+c.
Building RU-DEAD-YET - Everything Else
The rest of the code in the script is dedicated to parsing command line arguments and setting configurations. I’m omitting a detailed explanation about how this portion of the script works for the sake of brevity. With that said, feel free to email me if you have any questions about how the script’s setup code works. You can also clone a copy of the complete project here.
Our completed RUDY script should look like this:
#R.U.D.Y. attack demo
Now let’s see our script in action. For this demo I’m going to be running our script against a DokuWiki installation served by Apache2 on Ubuntu Server. Note that I’m running this on a lab network that I have full permission to attack. Be very careful running these kind of tests on networks and servers that you do not have full control over. Attacking your own site on a shared network or server could possibly be a TOS violation or illegal.
The first thing we’re going to do is start the script using the following command:
This will launch our script against the DokuWiki instance at 192.168.99.102/doku.php with 500 concurrent connections. The script will then prompt us to select a form from the target web page. In the screenshot below, I select “Form #2” to select the Login Form.
The script then prompts us to select one of the input tags from “Form #2” to use as a POST parameter. In the screenshot below, I select the third option, which corresponds to the username field in the login form.
Once we select a form and POST parameter to attack, our script will start spawning connections to the target server. The whole attack is shown in the video below. Notice how the web server completely grinds to a halt when our script starts running.
R.U.D.Y. is not an unstoppable DoS, and there are multiple ways of mitigating this kind of attack. An article by Trustwave, which can be found here, outlines how to combine the mod-security and reqtimeout Apache modules as countermeasures to Slow POST attacks. Trustwave’s countermeasures work by using reqtimeout to drop HTTP requests that take longer than 30 seconds to complete, and using mod-security to ban IPs that rack up more than 5 of these timeouts per minute.
Most recent versions of Apache ship with reqtimeout already installed, but you can verify that it is currently enabled like this:
Once we’ve made sure that reqtimeout is enabled, we want to open up our reqtimeout.conf file and add the directive shown on line 28 in the image below.
The ‘RequestReadTimeout header=30 body=30’ directive tells Apache to wait a maximum of 30 seconds for both the request headers and body. Apache will send a 408 response code if this timeout occurs.
Next we want to install and enable mod-security.
We then want to open up our mod-security.conf file and add the directives on lines 11 through 21 in the image below.
The mod-security plugin is just a Web Application Firewall (WAF), and the ‘SecRuleEngine On’ directive tells Apache that it’s ok for us to write our own rules for it. Fun fact - mod-security ships with no rules enabled by default. If you think that you’re magically protecting your server by simply installing mod-security and calling it a day, think again. You still have to configure it.
We then add Trustwave’s mod-security directives, which are shown on lines 17 through 20 in the image above. Trustwave’s mod-security rules tell Apache to keep track of how many 408 errors are issued per IP address per minute, and to ban IP addresses that exceed five 408’s per minute.
Finally, we restart Apache so that our changes take effect:
The next video shows Trustwave’s R.U.D.Y. mitigation in action as we try to run our script against the target and ultimately fail.
As you can see, it’s not a perfect solution. The R.U.D.Y script was able to keep the target server out of commission for well over a minute before it was banned by mod-security. That kind of downtime could very well lead to a significant loss of revenue on a production system. It would not be much of stretch to modify our script to relaunch connections through fresh SOCKS proxies each time a connection gets banned.
Non-blocking servers seem to be the best defense against these kinds of attacks. Check out what happens when we try to DOS the DokuWiki with our R.U.D.Y. script, but this time with NGninx runnig as the web server instead of Apache.
Even though we launched 500 slow POST connections against the target server, NGinx happily kept serving the web page. This is because NGinx serves content asynchronously, so incomplete requests are simply moved to the background while NGinx’s event loop keeps working on other things.
So what’s the verdict? R.U.D.Y. attacks are really hard to mitigate, unless you’re using an event driven web server. Which you should be, because it’s nearly 2016. >:)