Hello, bot

Ask pablomurad.com for a .env file and you get a riddle, a joke or a postcard from the end of the world, signed pmurad. A hundred messages, one nginx split_clients block and the scanners that read them all day.

Contents

Open a terminal and ask my site for a file it doesn't have:

curl pablomurad.com/.env

You won't get my secrets. You'll get one line of text, signed:

The oceans rose. The uptime stayed at 99.9%.

... pmurad

Run it again and the line changes:

The sky turned the color of a 404 page. Nobody looked up.

... pmurad

I made this as a joke, an easter egg for crawlers and scanners, and it's been running since late October. Here's what it says, who it's really talking to, and how nginx picks the line.

A hundred lines #

The list has exactly 100 messages, in three moods.

The first 34 are postcards from after the end of the world, written to whoever is still knocking:

The last human logged off on a Tuesday. The crawlers never noticed.
The fires burned for forty days. The cron jobs kept running.
Civilization ended mid-deploy. The build is still pending.
The world did not end with a bang. It ended with a timeout.

The next 33 are jokes, most of them aimed straight at the visitor:

Asking for .env on the first date is bold. I respect the confidence.
I'm not saying you're a bot, but you did ask for xmlrpc.php at 4 a.m.
There are 10 kinds of bots: the ones that read robots.txt and you.
The cloud is just someone else's computer. This one is mine. Go away.

And the last 33 are riddles, which never come with the answer:

What has many teeth but cannot bite? Answer? Ask me.
I have cities but no houses, forests but no trees, water but no fish. Answer? Ask me.
I'm the page you asked for, and I'm not here. What am I? Answer? Ask me.

Every answer ends with the same signature, ... pmurad.

Who gets a message #

Not every missing page. If you mistype a post's address, you get the regular 404: an HTML page in the browser, or the terminal version if you're using curl, which I described in curl pablomurad.com.

The messages are for requests that only make sense if you're looking for something you shouldn't have, like a .env file or a .git folder. And some visitors have already shown what they came for. Those don't get my pages anymore. Whatever they ask for, on any of the sites on this server, the answer is a line from the list and a 403.

Who's actually asking #

To see who reads these messages, I went through the last 24 hours of logs for every site on the server. There were 188,940 requests, and 39,859 of them, about one in five, asked for things like .env, .git/config, wp-login.php, phpmyadmin or server-status. That's on 87 different sites in a single day.

Across the whole server, the favorites were:

/.git/config 845
/wp-admin/install.php 721
/.env 710
/wp-login.php 404

Yes, the WordPress login page was requested 404 times. On pablomurad.com alone there were 3,476 of these requests in the same 24 hours, and the top of the list looks like this:

/wp-login.php ######################## 213
/wp-json/wp/v2/users/me ############ 103
/xmlrpc.php ####### 62
/.git/config #### 34
/.env #### 32
/.env.local ## 19

There's no WordPress here. This blog is a static site built from PaCord. None of these requests could ever succeed, and they arrive anyway, all day, because somewhere a script has a list of paths and an endless supply of addresses to try them on.

That's the real audience. A person almost never types /.git/config into a browser. A program does, thousands of times a day, and nobody reads the answer. These messages are written for that reader.

How nginx picks a line #

nginx has no "pick a random line" directive, but it has something close: split_clients. It was made for A/B tests. You give it a string, it hashes the string with MurmurHash2, and it puts the result into buckets by percentage. The usual trick is to hash the visitor's IP, so each person always lands in the same bucket.

I hash $request_id instead. That's a value nginx creates for every request from 16 random bytes, so every request lands somewhere new. Each message gets 1%, and the hundredth catches whatever is left:

split_clients "${request_id}" $nothing_here {
1% "The servers went quiet in 2031. You are talking to what is left.\n\n... pmurad\n";
1% "The sky turned the color of a 404 page. Nobody looked up.\n\n... pmurad\n";
1% "We saved the last working .env file. It was empty.\n\n... pmurad\n";
...
* "I'm the page you asked for, and I'm not here. What am I? Answer? Ask me.\n\n... pmurad\n";
}

The result is a variable, $nothing_here, and nginx's return can send a variable as the response body. So wherever I want a message, all it takes is this:

default_type "text/plain; charset=utf-8";
return 404 $nothing_here;

There's no application behind it, no database and no extra process. nginx keeps the hundred lines in memory and sends one.

Next to it there's a small script that reads the logs and keeps a list of networks that have earned the 403. I'm leaving out how it decides. The people who write scanners can read blogs too.

Why a joke at all #

The easter egg came first. The setup also has a few practical effects.

It's cheap. A message is between 45 and 110 bytes of plain text. The site's HTML 404 page is 36,848 bytes. If every one of those 3,476 probes on this blog had received the full page, that would be around 128 MB a day of HTML that nobody reads.

It's honest with machines. The status code is always the correct one: 404 when the thing doesn't exist, 403 when you're not welcome. A scanner gets the same answer it would get anywhere else, and the only thing it learns about the server is the word nginx in a header. The text changes and the code doesn't.

It doesn't hand over a fingerprint. Some fuzzing tools sort "found" from "not found" by the size of the response. Here the size changes with every request: 57 bytes, then 70, then 95.

It's an old idea. fortune, the program that prints a random quote, joke or riddle and that many Unix systems run at login, goes back to Version 7 Unix in 1979. In 1998 an April Fools' RFC, RFC 2324, gave HTTP the status code 418, "I'm a teapot". My server has an error page for 418 too. One of the hundred lines is "Error 418: I'm a teapot. Also, go away."

And once in a while, the reader is a person. Someone curious types /.env to see what happens, or reads the output of a scan they ran and finds a riddle where they expected an error. The riddles say "Answer? Ask me." If you want an answer, my contact page is right there.

If you want to see it for yourself:

curl pablomurad.com/.env
curl pablomurad.com/.git/config

Then run it again and see what comes back.