A quiet server with a loud log
A company's web server looks fine. The site loads, nobody has complained. But last night something got in.
The server wrote down every visit and every sign-in attempt. The story of the attack is in those logs, if you know how to read them.
The one rule
Warning
This lab is a copy of one server's logs, for you alone, with no way out to anything else. Read the evidence here only. Looking at real logs from a system that is not yours, without permission, is a crime under the Computer Misuse Act 1990.
By the end of this lesson
Note
I can find an attack in a log.
Key words
| Word | What it means | Example |
|---|---|---|
| Log | a file where a program writes down what it did, line by line | |
| Timeline | events put in the order they happened | |
| Brute force | trying many passwords until one works | |
| Status code | the number a web server answers with | 404 |
| Noise | log lines that look busy but mean nothing |
The cells students fill in are left blank in this preview.
Two logs tell two parts of the story
This server keeps two logs. The auth log records every sign-in over SSH: who tried, from where, and whether it worked.
The access log records every page a visitor asked for, and the status code the server answered. Together they show both the knocking on the door and the walking around inside.
Before you start
Note
Press Start the lab on the bar at the top. Your case is a folder called case in your home folder. The files are read-only, so the evidence stays as it was. Each Run only reads them.
What is in the case?
ls caseShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
How big is the auth log?
wc -l case/var/log/auth.logShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
The first line of the access log
head -n 1 case/var/log/nginx/access.logShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Worked example: one access log line
| Part | Value | Means |
|---|---|---|
| Address | 192.0.2.77 | Who asked |
| Time | 14/Oct/2026:00:01:49 | When |
| Request | GET /login | The page they wanted |
| Status | 200 | Here it is |
The cells students fill in are left blank in this preview.
Counting turns thousands of lines into facts
You will not read 1366 lines. You count them. grep -c counts the lines that hold a word.
A failed sign-in says Failed password. A successful one says Accepted. A quick count of each tells you at once whether somebody was guessing.
Count the failed sign-ins
grep -c "Failed password" case/var/log/auth.logShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Count the successful sign-ins
grep -c "Accepted" case/var/log/auth.logShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Count the web log by status code
awk '{print $9}' case/var/log/nginx/access.log | sort | uniq -cShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the counts tell you
There are 333 failed sign-ins and 6 accepted. That many failures is a sign of guessing, but it does not say who got in.
The status codes show 259 of 200, then 166 of 404, which is a page not found. A burst of 404s from one address is often somebody looking for a way in.
A common mistake
Warning
Many people blame the address with the most failed sign-ins. That address is often just noise: a bot that never gets in. The real intruder may have only a few failures, then one Accepted. Look for the success, not the loudest failure.
Count failures by address
# A few lines like the auth log. Each is address then the word fail or ok.
lines = ["198.51.100.23 fail", "198.51.100.23 fail", "203.0.113.5 ok", "198.51.100.23 fail"]
counts = {}
for line in lines:
parts = line.split(" ")
address = parts[0]
if parts[1] == "fail":
if address in counts:
counts[address] = counts[address] + 1
else:
counts[address] = 1
print(counts)
Going further
Count the pages that gave a 404 and sort them: awk '$9==404 {print $7}' case/var/log/nginx/access.log | sort | uniq -c | sort -rn | head.
This counts pages, not addresses, so it does not answer the case. Write down the three most-wanted missing pages and say what an attacker might have hoped to find at each.