Saltar al contenido
sudoquest@root
Blue Team

La web tocada

Leer los registros de nginx para saber qué se ha servido por error, a quién y cuánto, y cerrarlo sin tirar la tienda.

35 min de lectura

Son las diez y media en el SOC. Salta una alerta de La Alacena, la tienda online de comida de despensa: srv-web, su servidor, lleva toda la mañana contestando 200 a una ruta que no es de la tienda. Nadie ha entrado en el servidor: lo que pasa es que nginx sirve algo que no debería, y lo sirve a quien lo pida.

Es de los fallos más corrientes: una copia de la base de datos que alguien dejó en la carpeta de la web «un momento», un panel que se quedó sin contraseña, un .zip con el código. No hace falta romper nada para llevárselo: basta con saber la ruta. Tu trabajo es averiguar qué se ha servido, a quién, cuánto y desde cuándo, cerrarlo en nginx sin tirar la tienda y contarlo. Todo sale de los registros de nginx.

La ficha del SOC dice qué es de La Alacena:

tecnico@srv-web:~$ cat ~/ficha-alacena.txt
La Alacena · srv-web (192.168.40.10)
Ficha del SOC, revisada el 5 de octubre de 2026

Lo que es público (nginx, puerto 80):
la tienda (/), las fotos de los productos (/productos/) y el carrito.

Lo que no:
el panel de la tienda (/admin/): solo desde la oficina (192.168.40.50).
las copias de la base de datos: cada noche, a /var/backups/alacena (fuera de la web).

Si se toca nginx: sudo nginx -t antes de recargar. Si la tienda no contesta,
no se vende.

Lo que apunta nginx

nginx apunta cada petición en /var/log/nginx/access.log, una línea por petición, en el formato combined:

tecnico@srv-web:~$ tail -n 3 /var/log/nginx/access.log
100.80.116.164 - - [20/Oct/2026:09:51:30 +0000] "GET /productos/garbanzos.jpg HTTP/1.1" 200 43317 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0"
100.80.116.164 - - [20/Oct/2026:09:51:48 +0000] "GET /productos/atun-claro.jpg HTTP/1.1" 200 53951 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0"
100.80.116.164 - - [20/Oct/2026:09:51:49 +0000] "GET /carrito.html HTTP/1.1" 200 53 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0"

Por orden: quién pidió (la IP), dos guiones (el segundo, el usuario si entró con contraseña), cuándo, la petición (método, ruta y versión), el código con el que contestó (200 bien, 404 no existe, 403 prohibido…), los bytes que mandó, de dónde venía el enlace y con qué programa se pidió. Lo que falla va además a /var/log/nginx/error.log.

Contar lo que se ha servido

Con awk se saca un campo de cada línea ($9, el código; $7, la ruta), y con sort | uniq -c se cuenta cuántas veces sale cada uno:

tecnico@srv-web:~$ awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
     75 200
    4 404
tecnico@srv-web:~$ awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
     19 /
   12 /copias/alacena-2026-10-19.sql.gz
   11 /productos/garbanzos.jpg
   10 /productos/atun-claro.jpg
   10 /productos/arroz-bomba.jpg
    7 /productos/aceite-oliva.jpg
    5 /carrito.html
    4 /favicon.ico
    1 /admin/

La tienda, las fotos, el carrito… y una ruta que no está en la ficha, pedida doce veces: una copia de la base de datos en /copias/. Está ahí de verdad, dentro de la carpeta de la web:

tecnico@srv-web:~$ ls -l /var/www/alacena/copias
total 1792
-rw-r--r-- 1 www-data www-data 1832843 Oct 20 02:15 alacena-2026-10-19.sql.gz

Ejercicio

Qué se ha servido: access.log con awk

En srv-web, la tienda de La Alacena, mira en access.log qué códigos y qué rutas se han servido esta mañana, y encuentra la que no debería estar a la vista (la ficha, en ~/ficha-alacena.txt).

Preparando la máquina…

A quién y cuánto

grep se queda con las líneas de la copia, y awk con la IP de cada una:

tecnico@srv-web:~$ grep copias /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c
      4 198.51.100.177
    8 203.0.113.151

Y awk también suma: cuántas veces se sirvió entera (código 200) y cuántos bytes salieron con ella.

tecnico@srv-web:~$ awk '$7 ~ /copias/ && $9 == 200 {n++; b += $10} END {print n, b}' /var/log/nginx/access.log
12 21994116

Unos 22 MB: la copia, doce veces. La primera vez, la primera línea que la nombra:

tecnico@srv-web:~$ grep -m1 copias /var/log/nginx/access.log
203.0.113.151 - - [20/Oct/2026:07:29:00 +0000] "GET /copias/alacena-2026-10-19.sql.gz HTTP/1.1" 200 1832843 "-" "curl/7.81.0"

Ejercicio

A quién y cuánto: grep, awk, sort y uniq

La copia de la base de datos se ha servido varias veces. Saca desde qué direcciones, cuántos bytes en total y cuándo fue la primera vez.

Preparando la máquina…

Cerrarlo en nginx

El sitio de la tienda está en /etc/nginx/sites-available/alacena:

tecnico@srv-web:~$ cat /etc/nginx/sites-available/alacena
server {
listen 80 default_server;
listen [::]:80 default_server;

root /var/www/alacena;
index index.html;

server_name alacena.lan;

location / {
	try_files $uri $uri/ =404;
}
}

Un bloque location aplica a las rutas que empiezan por lo que dice; dentro, deny all prohíbe todo. Se añade antes de la última llave, la que cierra el server:

tecnico@srv-web:~$ tail -n 5 /etc/nginx/sites-available/alacena
	}
location /copias/ {
	deny all;
}
}

Sin editor, sed -i '$i …' mete una línea justo antes de la última: con sudo sed -i '$i location /copias/ { deny all; }' /etc/nginx/sites-available/alacena queda el mismo bloque en una sola línea. Y si algo sobra, cat -n dice en qué línea está y sudo sed -i '<n>d' la borra.

Antes de recargar, siempre nginx -t: comprueba la configuración sin tocar lo que está en marcha.

tecnico@srv-web:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
tecnico@srv-web:~$ sudo systemctl reload nginx

Ahora la copia contesta 403 y la tienda sigue contestando; error.log apunta cada intento:

tecnico@srv-web:~$ curl -sI http://localhost/copias/alacena-2026-10-19.sql.gz
HTTP/1.1 403 Forbidden
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 20 Oct 2026 10:30:15 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
tecnico@srv-web:~$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost/
200
tecnico@srv-web:~$ tail -n 1 /var/log/nginx/error.log
2026/10/20 10:30:15 [error] 406#406: *80 access forbidden by rule, client: 127.0.0.1, server: alacena.lan, request: "HEAD /copias/alacena-2026-10-19.sql.gz HTTP/1.1", host: "localhost"
tecnico@srv-web:~$ sudo nginx -t
nginx: [emerg] unexpected "}" in /etc/nginx/sites-enabled/alacena:15
nginx: configuration file /etc/nginx/nginx.conf test failed
tecnico@srv-web:~$ sudo systemctl reload nginx
Job for nginx.service failed.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
tecnico@srv-web:~$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost/
200

Ejercicio

Cerrarlo en nginx: location, deny all, nginx -t y reload

Que nginx no sirva nada de /copias/ a nadie, sin tirar la tienda. Comprueba la configuración antes de recargar.

Preparando la máquina…

Sacarla de la web

Cerrarlo en nginx es lo urgente; lo bueno es que la copia no esté en la web. find busca si hay más:

tecnico@srv-web:~$ sudo find /var/www -name '*.sql*'
/var/www/alacena/copias/alacena-2026-10-19.sql.gz

Según la ficha, las copias van a /var/backups/alacena. Se mueve, no se borra: es la copia de anoche.

tecnico@srv-web:~$ sudo mv /var/www/alacena/copias/*.sql.gz /var/backups/alacena/
tecnico@srv-web:~$ sudo ls -l /var/backups/alacena
total 3580
-rw-r--r-- 1 root     root     1828632 Oct 19 02:15 alacena-2026-10-18.sql.gz
-rw-r--r-- 1 www-data www-data 1832843 Oct 20 02:15 alacena-2026-10-19.sql.gz

Ejercicio

Sacar la copia de la carpeta pública: find y mv

La copia no pinta nada en la web: según la ficha, las copias van a /var/backups/alacena. Busca si hay más dentro de /var/www y llévalas allí (sin borrarlas).

Preparando la máquina…

El panel, solo para la oficina

La ficha dice que el panel es solo para la oficina, y access.log dice que alguien de fuera lo vio:

tecnico@srv-web:~$ grep /admin/ /var/log/nginx/access.log
192.0.2.229 - - [20/Oct/2026:07:16:00 +0000] "GET /admin/ HTTP/1.1" 200 75 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0"

allow y deny se miran en orden y la primera que casa decide: se deja pasar a la oficina y se prohíbe al resto. Si la oficina tiene más de un ordenador, un allow por cada uno, todos antes del deny all.

tecnico@srv-web:~$ tail -n 9 /etc/nginx/sites-available/alacena
	}
location /copias/ {
	deny all;
}
location /admin/ {
	allow 192.168.40.50;
	deny all;
}
}
tecnico@srv-web:~$ curl -sI http://localhost/admin/
HTTP/1.1 403 Forbidden
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 20 Oct 2026 10:30:32 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive

403 también para ti: localhost no es la oficina. Otra forma es pedir contraseña: htpasswd crea el archivo de usuarios, y en el location, auth_basic y auth_basic_user_file.

tecnico@srv-web:~$ sudo htpasswd -cb /etc/nginx/.htpasswd oficina Alacena-2026
Adding password for user oficina

Ejercicio

El panel, solo para la oficina: allow y deny (o auth_basic)

El panel de la tienda (/admin/) contesta a cualquiera, y la ficha dice que es solo para la oficina (192.168.40.50). Ciérralo para el resto sin tocar la tienda.

Preparando la máquina…

En la sala: la tienda abierta

En la sala estás en el SOC a las diez y media, con la alerta de La Alacena en la pared y la terminal de srv-web en tu puesto. Mientras la copia siga a la vista, quien la pedía la sigue pidiendo (y, al rato, alguien más): cada quince minutos la alerta vuelve con las veces que se ha servido, y lo servido cuenta en la nota. A las doce y media, si sigue a la vista, el día se cierra así. Bloquear con ufw a quien se la lleva no basta; hay que cerrarlo en nginx, la copia y el panel. Si la tienda deja de contestar (un restart con un error, el 80 cerrado), no vende: es servicio caído. La nota mira además que la copia acabe fuera de la web, guardada, y que la oficina siga entrando al panel.