Denial of Service


Web Security.

With a denial-of-service (DoS) attack, you're trying to stop legitimate users from using a service. A website that cannot answer its visitors cannot do its job, however carefully it checks passwords or protects their data. Keeping the service available is a security problem of its own.

The machine behind that service has a limited amount of time, memory, and I/O capacity to spend on its clients. Sometimes exhausting those resources takes a flood of traffic. Sometimes the server helpfully does most of the work for you! A small request can be expensive to handle, and waiting on a client can still tie up resources that others need. If you can keep enough of that capacity occupied, the server can stay alive and keep working while everyone else gets nowhere.


Let's start with a server that has almost nothing to do. At https://victim.internal/, the /health handler simply returns OK. We've given it one synchronous Gunicorn worker, which handles one request at a time, from reading it to sending a response.

That includes time spent waiting for you to send the rest of a request. While it waits, the monitor's health check gets stuck behind your request. A client that barely sends anything can keep the whole service to itself! Keep that worker occupied long enough for the challenge status page to mark the service down.

One worker was easy to tie up, so let's give the service 32! Each is a separate synchronous Gunicorn process. Holding one up leaves 31 others ready to answer the monitor, which is a much more sensible arrangement for a web server.

The workers still have the same weakness: each waits on its current client before moving on to another. Now your requests need to overlap, keeping the whole pool occupied. As requests finish or connections close, workers become available again, giving a health check a chance to slip through. Take a look at /challenge/gunicorn.conf.py and adapt what worked last time to keep all 32 waiting on you.

Starting another process is not the only way to let a server handle several clients. A thread pool allows one thread to make progress while another is waiting on I/O, all within the same process. Here, Gunicorn has four request-handling threads, so one slow client does not hold up the whole service.

Before Flask can handle a request, Gunicorn has to read its HTTP headers. A client that sends those headers slowly can keep the thread reading them occupied while using very little bandwidth. The application hasn't even started on the request, and it already costs the server a thread! Keep those threads waiting long enough to deny the monitor its answer.

A request that arrives promptly can still leave the server struggling to serve anyone else. Once the application starts processing the input, it needs memory to hold the data and whatever it builds from it. A short handler can make several substantial allocations before it has an answer ready.

The new POST /log endpoint accepts JSON with a message field and writes the entry to a temporary file. To do that, it reads the body, parses it into Python objects, and serializes those objects back into text. The raw request data, parsed values, and output can coexist in memory, so the number of bytes sent over the connection does not tell the whole story.

Read /challenge/wsgi.py and follow the data through the handler. Choose log entries that make those allocations expensive enough to keep the service from answering the monitor.

The file write at the end of the logging handler has a cost of its own. Even with enough memory to process individual entries, the filesystem has a finite capacity for handling their file operations. Letting more requests run at once does not make that capacity grow with them.

Gunicorn now uses 64 request-handling threads in a single process. Each accepted entry is written to a separate temporary file, so concurrent logging requests all draw on the same storage resources. A thread waiting for a file operation to finish cannot start another request. If enough threads are occupied this way, a quick request to /health can be left waiting for a thread of its own.

Use the logging endpoint to put that shared I/O capacity under sustained pressure. Consider both the work each entry causes and how many entries the server is processing together as you try to delay the monitor's health checks.

Renting a server in the cloud often means renting a share of a physical machine. Several customers may have separate virtual machines on the same host, and storage can be shared across many workloads. This keeps hosting costs down, but it also makes resource limits part of keeping tenants from affecting one another. If one tenant can consume too much of a shared resource, another tenant's application can slow down even though its own traffic has not changed. That is the noisy neighbor problem.

Here, your programs run alongside the victim service on the same system, sharing CPU time, memory, and storage. Direct traffic from your user to victim.internal is blocked, while the monitor is still allowed to reach /health and check whether the service is responding.

The earlier attacks made the service consume resources on your behalf. With local access, you can consume those resources directly from the challenge terminal. Make the victim unable to answer in time by putting pressure on what it needs to run, while leaving the monitor able to perform its checks and report the result.


30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score