Denial of Service


Web Security.

Servers have limits: CPU, memory, bandwidth, open connections, file descriptors. A Denial of Service (DoS) attack doesn't steal data or run code. It just spends those limits until the service stops keeping up with real users.

Sometimes that takes a flood of traffic. Other times it's one cheap request that costs the server far more to answer than it cost you to send. Point enough machines at one target and it becomes a Distributed Denial of Service (DDoS).

Each challenge gives you a small service with a status page pinging it once a second. Keep the service from answering long enough and the page marks it down. What changes from one challenge to the next is how the service is built, and that decides what it takes to knock it over.


victim.internal runs on a single worker with a single thread. It holds exactly one request at a time and won't look at the next until the current one is finished.

The status page checks on it every second by asking for /health, and that check waits in the same line as everything else. So you don't have to flood this server. You just have to be the request it's still stuck on when the check comes around, and keep being it.

Tie it up for a few checks in a row and the page marks it down.

One worker was easy to jam, so this service runs thirty-two of them, each its own process, all answering requests in parallel.

It buys less than it looks. Every worker is still synchronous: one request, start to finish, before it takes another. Thirty-two lanes are still thirty-two, and each one ties up the same way the single worker did last time.

Same goal. Just more of them to keep busy at once.

Every challenge so far gave the server something to do once your request arrived. This one doesn't. There's a single worker with four threads, one endpoint at /health, and nothing behind it worth triggering. No body to parse, no work to run.

But a thread can't answer a request until it has finished reading it, and reading happens at whatever pace the client sends. A thread stuck waiting on bytes that trickle in one at a time isn't free to do anything else, the status page's check included.

There are four threads. You don't have to make the server compute anything. You just have to keep it listening.

This service adds an endpoint: POST /log. Send it JSON with a message field and the handler reads the whole request into memory before it does anything with it.

Nothing limits how big that request can be. However much you send, the server has to hold all of it at once, and a machine only has so much memory. Fill it and the process can't stay up to answer anyone, the status page included.

More threads this time, sixty-four of them, all in one process. Threads are cheap next to whole processes, so the service can juggle far more requests at once without the overhead.

But threads only get you as far as the hardware under them. They still share one disk and one set of file descriptors, and POST /log writes every request it takes to a file. Sixty-four threads writing at the same time don't each get their own disk; they line up for it. Push enough through and they spend their time waiting on I/O instead of answering the status page.

Same /log endpoint as before.

Every challenge before this one let you talk to the service. This one cuts you off: anything your user sends toward victim.internal is dropped before it leaves the box. No slow request, no oversized body, no header to drip. The network path to it is closed.

But you're still on the same machine it is. Same CPU, same memory, same disk, same kernel, and you can run whatever you like as your own user. The service doesn't need you to reach it over the network to feel you.

Starve the box and you starve the service with it. Make /health miss enough checks in a row and you win.


30-Day Scoreboard:

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

Rank Hacker Badges Score