1. Introducción y propósito en la seguridad de sistemas

Las bases de datos locales de identidades, como /etc/passwd, /etc/shadow y /etc/group, así como buena parte de los registros del sistema y de la salida de órdenes como ss, ps o last, se organizan en registros divididos en campos. awk, diseñado en los Laboratorios Bell por Alfred Aho, Peter Weinberger y Brian Kernighan, es un lenguaje de programación orientado precisamente a recorrer ese tipo de datos, seleccionar registros mediante condiciones sobre campos concretos y generar informes.

En auditoría, awk aporta tres capacidades que grep y sed no ofrecen de forma natural:

  1. Condiciones sobre campos concretos, en lugar de sobre la línea completa, lo que elimina los falsos positivos de búsquedas como grep ':0:'.

  2. Comparaciones numéricas, como UID >= 1000 o días desde el último cambio > 365.

  3. Agregación mediante arrays asociativos, que permite contar, sumar o cruzar datos de varios ficheros en una sola pasada.

1.1. Implementaciones disponibles

La orden awk es en realidad un enlace a una de varias implementaciones:

Implementación Dónde aparece Observaciones

mawk

Instalación por defecto en Debian y Ubuntu

Muy rápida. Las versiones antiguas, como la de Debian 12, no admiten intervalos {n,m} en expresiones regulares

gawk

Instalación por defecto en RHEL, Fedora y Arch; paquete gawk en Debian

Implementación de GNU con extensiones como gensub, IGNORECASE o strftime

busybox awk

Contenedores Alpine y sistemas embebidos

Subconjunto reducido

Puede comprobar cuál utiliza su sistema con readlink -f "$(command -v awk)". Los ejemplos de esta guía se limitan a la sintaxis POSIX, de modo que funcionan con cualquiera de ellas.

2. Estructura y ciclo de ejecución

Un programa awk es una secuencia de reglas patrón { acción }:

awk [opciones] 'patrón { acción } patrón { acción } ...' [fichero...]
awk [opciones] -f programa.awk [fichero...]
  1. Patrón: Expresión que decide si la regla se aplica al registro actual. Puede ser una comparación, una expresión regular entre barras, una combinación lógica o un rango patrón1, patrón2. Si se omite, la acción se aplica a todos los registros.

  2. Acción: Instrucciones entre llaves. Si se omite, la acción por defecto es { print }, que imprime el registro completo.

  3. Bloques especiales:

    • BEGIN { …​ } se ejecuta una vez antes de leer la entrada. Se usa para fijar separadores, inicializar variables o imprimir cabeceras.

    • END { …​ } se ejecuta una vez después del último registro. Se usa para imprimir totales y resúmenes.

Modelo de ejecución de awk: BEGIN
Figura 1. Modelo de ejecución de awk: BEGIN, reglas por registro y END
awk -F: 'BEGIN { print "Cuentas con intérprete interactivo" }
         $7 ~ /(ba|z|da)?sh$/ { n++; print "  " $1 }
         END { print "Total:", n+0 }' /etc/passwd

El programa awk debe escribirse siempre entre comillas simples. Con comillas dobles, la shell sustituiría $1, $7 o $0 por sus propias variables antes de que awk llegue a verlas. Para pasar valores de la shell al programa se usa la opción -v variable=valor.

3. Campos, separadores y variables predefinidas

Por defecto, awk separa los campos por secuencias de espacios y tabuladores e ignora los espacios iniciales, lo que resulta adecuado para la salida de ps, ss o df. Para ficheros con otro delimitador se usa -F o se asigna FS en BEGIN. FS puede ser también una expresión regular, como -F'[:,]'.

  • $0 contiene el registro completo.

  • $1, $2, …, $NF contienen cada campo; $NF es siempre el último y $(NF-1), el penúltimo.

Variable Descripción Ejemplo Explicación técnica

FS

Field Separator

BEGIN { FS=":" }

Separador de campos de entrada, equivalente a -F:.

OFS

Output Field Separator

BEGIN { OFS="\t" }

Separador que print inserta entre argumentos separados por comas. Si se modifica un campo, $0 se reconstruye con OFS; de ahí el modismo $1=$1.

NF

Number of Fields

NF != 7

Número de campos del registro actual. Permite detectar líneas truncadas o manipuladas.

NR

Number of Records

NR > 1

Número de registros leídos en total, acumulado entre ficheros. Útil para saltar cabeceras.

FNR

File Number of Records

FNR == 1

Número de registro dentro del fichero actual. La condición NR == FNR identifica el primer fichero.

FILENAME

Fichero actual

{ print FILENAME, $1 }

Nombre del fichero que se está procesando.

RS

Record Separator

BEGIN { RS="" }

Separador de registros de entrada. Con RS="" cada párrafo separado por líneas en blanco es un registro.

# Convertir /etc/passwd en una tabla separada por tabuladores
awk 'BEGIN { FS=":"; OFS="\t" } { $1=$1; print }' /etc/passwd

# Pasar un valor de la shell al programa
awk -F: -v minimo=1000 '$3 >= minimo { print $1 }' /etc/passwd

Con los conceptos explicados hasta aquí, ya puede realizar el Ejercicio 1. Preparación del laboratorio y extracción de campos.

4. Filtrado mediante condiciones y expresiones regulares

4.1. Comparaciones y operadores lógicos

Los patrones admiten los operadores relacionales ==, !=, <, <=, > y >=, y los lógicos &&, || y !:

# Cuentas con UID 0
awk -F: '$3 == 0 { print $1 }' /etc/passwd

# Cuentas de usuarios humanos según los límites definidos en login.defs
awk -F: -v min="$(awk '$1 == "UID_MIN" { print $2 }' /etc/login.defs)" \
        -v max="$(awk '$1 == "UID_MAX" { print $2 }' /etc/login.defs)" \
        '$3 >= min && $3 <= max { print $1, $3 }' /etc/passwd

Leer UID_MIN y UID_MAX del propio sistema en lugar de escribir 1000 y 60000 hace que la auditoría sea correcta también en distribuciones con otros rangos.

awk compara numéricamente cuando ambos operandos parecen números y como cadenas en caso contrario. Si un campo contiene un valor no numérico, como x, la comparación $3 >= 1000 se realiza como cadena y puede dar resultados inesperados. Sumar cero, como en $3+0 >= 1000, fuerza la conversión numérica.

4.2. Expresiones regulares sobre campos

El operador ~ comprueba si un campo coincide con una expresión regular y !~ comprueba lo contrario. awk utiliza el dialecto ERE:

# Cuentas de sistema con intérprete interactivo
awk -F: '$3 > 0 && $3 < 1000 && $7 !~ /(nologin|false|sync|shutdown|halt)$/ { print $1, $7 }' /etc/passwd

Una expresión regular sin operador, como /Failed password/, se aplica sobre $0.

4.3. Estructuras de control

awk dispone de if/else, for, while, next, que pasa al siguiente registro, y exit:

awk -F: '{
    if ($3 == 0)        tipo = "SUPERUSUARIO"
    else if ($3 < 1000) tipo = "servicio"
    else                tipo = "usuario"
    printf "%-20s %-12s %s\n", $1, tipo, $7
}' /etc/passwd

Con los conceptos explicados en esta sección, ya puede realizar el Ejercicio 2. Clasificación y filtrado de cuentas.

5. Arrays asociativos, funciones e informes

5.1. Recuento y agregación

Los arrays de awk se indexan con cadenas, lo que permite contar ocurrencias sin ordenar previamente la entrada:

# Intentos fallidos de autenticación SSH por dirección IP de origen
sudo awk '/Failed password/ {
              for (i = 1; i <= NF; i++) if ($i == "from") intentos[$(i+1)]++
          }
          END {
              for (ip in intentos) printf "%6d  %s\n", intentos[ip], ip
          }' /var/log/auth.log | sort -rn

Buscar la palabra from y tomar el campo siguiente hace que el programa funcione tanto si el mensaje incluye invalid user como si no, ya que en ese caso el número de campos cambia. El orden de recorrido de for (clave in array) no está definido, de ahí que el resultado se ordene con sort.

5.2. Cruce de ficheros con NR == FNR

Cuando se pasan dos ficheros, la condición NR == FNR solo es cierta durante la lectura del primero. Este modismo permite cargar un fichero en memoria y consultarlo al procesar el segundo:

# Cuentas presentes en /etc/passwd sin entrada en /etc/shadow
sudo awk -F: 'NR == FNR { sombra[$1] = 1; next }
              !($1 in sombra) { print "Sin entrada en shadow:", $1 }' /etc/shadow /etc/passwd

5.3. Funciones de cadena e informes formateados

Función Efecto

length(s)

Longitud de la cadena

substr(s, i, n)

Subcadena de n caracteres desde la posición i

index(s, t)

Posición de t en s, o 0 si no aparece

split(s, a, sep)

Divide s en el array a y devuelve el número de elementos

sub(re, r, s) / gsub(re, r, s)

Sustituye la primera o todas las coincidencias en s

match(s, re)

Posición de la coincidencia; fija RSTART y RLENGTH

tolower(s) / toupper(s)

Conversión de mayúsculas y minúsculas

printf formato, …​

Salida con formato, igual que en C

# Identificador del algoritmo de hash de cada cuenta, extraído con split
sudo awk -F: '$2 ~ /^\$/ { split($2, partes, "$"); printf "%-20s %s\n", $1, partes[2] }' /etc/shadow

Con los conceptos explicados en esta sección, ya puede realizar el Ejercicio 3. Agregación de eventos de autenticación.

6. Recetario de awk para la auditoría de cuentas

  1. Cuentas con UID 0, que en un sistema bastionado solo debe ser root: Control del CIS Benchmark Ensure root is the only UID 0 account.

    awk -F: '$3 == 0 && $1 != "root" { print "UID 0 no autorizado:", $1 }' /etc/passwd
  2. Integridad estructural de /etc/passwd y /etc/shadow: Cada línea debe tener exactamente 7 y 9 campos respectivamente.

    awk -F: 'NF != 7 { print "passwd, línea " NR ": " NF " campos" }' /etc/passwd
    sudo awk -F: 'NF != 9 { print "shadow, línea " NR ": " NF " campos" }' /etc/shadow
  3. Cuentas sin contraseña: Un campo vacío permite el acceso sin autenticación en los servicios que lo admitan. El CIS Benchmark exige que no exista ninguna.

    sudo awk -F: '$2 == "" { print "Cuenta sin contraseña:", $1 }' /etc/shadow
  4. Clasificación del algoritmo de hash de cada cuenta: Las cuentas con MD5-crypt o DES deben forzar un cambio de contraseña para que se regeneren con el algoritmo actual del sistema.

    sudo awk -F: '{
        h = $2; estado = ""
        if (h == "")              { print $1, "SIN CONTRASEÑA"; next }
        if (h ~ /^[!*]+$/)        { print $1, "sin contraseña utilizable"; next }
        if (h ~ /^!/)             { estado = " [bloqueada]"; sub(/^!+/, "", h) }
        if      (h ~ /^\$y\$/)    alg = "yescrypt"
        else if (h ~ /^\$gy\$/)   alg = "gost-yescrypt"
        else if (h ~ /^\$6\$/)    alg = "SHA-512-crypt"
        else if (h ~ /^\$5\$/)    alg = "SHA-256-crypt"
        else if (h ~ /^\$2[abxy]\$/) alg = "bcrypt"
        else if (h ~ /^\$1\$/)    alg = "MD5-crypt OBSOLETO"
        else if (length(h) == 13) alg = "DES OBSOLETO"
        else                      alg = "desconocido"
        print $1, alg estado
    }' /etc/shadow

    yescrypt es el algoritmo por defecto en Debian 11 y posteriores y en Ubuntu 22.04 y posteriores, mientras que SHA-512-crypt sigue siendo aceptable según el CIS Benchmark.

  5. Cuentas bloqueadas: Recuerde que el prefijo ! impide la autenticación por contraseña, pero no el acceso mediante claves SSH ni otros mecanismos que no consulten /etc/shadow.

    sudo awk -F: '$2 ~ /^!/ { print "Contraseña bloqueada:", $1 }' /etc/shadow
  6. Fecha del último cambio de contraseña en el futuro: Control del CIS Benchmark Ensure all users last password change date is in the past. El tercer campo de /etc/shadow se expresa en días desde el 1 de enero de 1970.

    sudo awk -F: -v hoy="$(( $(date +%s) / 86400 ))" '$3 > hoy { print "Cambio en el futuro:", $1 }' /etc/shadow
  7. Contraseñas que no se cambian desde hace más de un año:

    sudo awk -F: -v hoy="$(( $(date +%s) / 86400 ))" \
        '$2 ~ /^\$/ && $3 != "" && hoy - $3 > 365 { print $1, hoy - $3, "días" }' /etc/shadow
  8. Miembros de los grupos de administración: El cuarto campo de /etc/group solo contiene los miembros secundarios; los usuarios cuyo grupo principal es sudo o wheel deben buscarse además por GID en /etc/passwd.

    awk -F: '$1 == "sudo" || $1 == "wheel" { print $1 " (GID " $3 "): " $4 }' /etc/group
  9. Puertos en escucha y proceso asociado:

    sudo ss -tulpnH | awk '{ print $1, $5, $7 }' | sort -u

7. Ejercicios prácticos

Los ejercicios deben realizarse en orden sobre una máquina virtual Debian o Ubuntu de pruebas. Trabajan sobre ficheros de muestra creados en ~/lab_filtrado/awk, incluida una copia ficticia de /etc/shadow, de modo que no es necesario leer ni modificar las credenciales reales del sistema. El último ejercicio elimina el laboratorio.

Nº Ejercicio Competencia que desarrolla

1

Preparación del laboratorio y extracción de campos

Usar separadores, campos y variables predefinidas

2

Clasificación y filtrado de cuentas

Combinar condiciones numéricas, lógicas y expresiones regulares

3

Agregación de eventos de autenticación

Contar eventos con arrays asociativos y generar informes

4

Auditoría de credenciales

Detectar debilidades en shadow y cruzar ficheros

5

Programa de auditoría reutilizable

Encapsular controles en un programa awk -f

6

Limpieza del entorno

Revertir el laboratorio

7.1. Ejercicio 1. Preparación del laboratorio y extracción de campos

Realice los siguientes pasos:

  1. Cree el laboratorio con una copia de /etc/passwd a la que se añaden cinco cuentas, una de ellas con un registro dañado, y un fichero shadow ficticio. Las cuentas originales del sistema reciben en él una entrada sin contraseña utilizable, de modo que no se copia ningún hash real:

    mkdir -p ~/lab_filtrado/awk && cd ~/lab_filtrado/awk
    cp /etc/passwd passwd
    cat >> passwd << 'EOF'
    operador:x:1001:1001:Operador de sistemas:/home/operador:/bin/bash
    soporte:x:0:0:Soporte tecnico:/root:/bin/bash
    webapp:x:998:998:Aplicacion web:/srv/webapp:/bin/sh
    antiguo:x:1002:1002:/home/antiguo:/bin/bash
    tecnico:x:1003:1003:Tecnico de guardia:/home/tecnico:/bin/bash
    EOF
    awk -F: '$1 != "root" && $1 != "daemon" { print $1 ":*:20700:0:99999:7:::" }' /etc/passwd > shadow
    cat >> shadow << 'EOF'
    root:$y$j9T$Fq0lU2xJb3m1kQyH9yGz7/$9d3eH1RrN3vV1c0yQyE0sQm4QHkQe6lW2xTqZkqf8A7:20700:0:99999:7:::
    daemon:*:20700:0:99999:7:::
    operador:$6$Xb2k9sLq$8sJf3kQ0lYqv5m3o2pP1aR7uB6nC4dE9fG0hI1jK2lM3nO4pQ5rS6tU7vW8xY9zA0bC1dE2fG3hI4jK5lM6n:19800:0:99999:7:::
    soporte:$1$Ab3dE5gH$wQ9eR8tY7uI6oP5aS4dF3.:20700:0:99999:7:::
    webapp::20700:0:99999:7:::
    antiguo:!$y$j9T$Lk8mN7bV6cX5zA4sD3fG2/$h1J2k3L4z5X6c7V8b9N0mQ1wE2rT3yU4iO5pA6sD7fG:19000:0:99999:7:::
    invitado:aBcDeFgHiJkLm:22000:0:99999:7:::
    EOF
  2. Muestre el nombre, el UID y el intérprete de cada cuenta de passwd, separados por tabuladores.

  3. Muestre el número de línea y el nombre de las cinco últimas cuentas, usando NR. Muestre después el último campo de cada línea con $NF.

  4. Indique qué implementación de awk utiliza su sistema.


7.2. Ejercicio 2. Clasificación y filtrado de cuentas

Realice los siguientes pasos:

  1. Muestre las cuentas con UID 0 distintas de root y las cuentas de usuario humano según UID_MIN y UID_MAX de /etc/login.defs.

  2. Detecte las cuentas de servicio con intérprete interactivo. Justifique el riesgo que supone webapp.

  3. Localice los registros con un número de campos distinto de 7 e indique qué campo falta en la cuenta antiguo.

  4. Genere un informe con printf que clasifique todas las cuentas como SUPERUSUARIO, servicio o usuario, alineado en columnas.


7.3. Ejercicio 3. Agregación de eventos de autenticación

Realice los siguientes pasos:

  1. Cree el registro de muestra auth.log:

    cat > auth.log << 'EOF'
    Oct 18 08:01:12 srv01 sshd[1201]: Failed password for root from 203.0.113.45 port 51234 ssh2
    Oct 18 08:01:15 srv01 sshd[1201]: Failed password for root from 203.0.113.45 port 51234 ssh2
    Oct 18 08:01:19 srv01 sshd[1201]: Failed password for root from 203.0.113.45 port 51234 ssh2
    Oct 18 08:01:20 srv01 sshd[1203]: Failed password for invalid user admin from 198.51.100.23 port 40022 ssh2
    Oct 18 08:01:24 srv01 sshd[1203]: Failed password for invalid user admin from 198.51.100.23 port 40022 ssh2
    Oct 18 08:01:31 srv01 sshd[1207]: Failed password for invalid user oracle from 198.51.100.23 port 40030 ssh2
    Oct 18 08:01:40 srv01 sshd[1209]: Failed password for operador from 192.0.2.10 port 50098 ssh2
    Oct 18 08:02:01 srv01 sshd[1210]: Accepted password for operador from 192.0.2.10 port 50100 ssh2
    Oct 18 08:05:02 srv01 sshd[1250]: Failed password for root from 203.0.113.45 port 51302 ssh2
    Oct 18 08:05:09 srv01 sshd[1250]: Failed password for root from 203.0.113.45 port 51302 ssh2
    Oct 18 08:07:12 srv01 sshd[1290]: Accepted password for root from 203.0.113.45 port 51400 ssh2
    EOF
  2. Muestre el campo $9 de las líneas Failed password y explique por qué no siempre contiene el nombre de usuario.

  3. Cuente los intentos fallidos por dirección IP con un array asociativo y ordene el resultado de mayor a menor.

  4. Amplíe el programa para que, en el bloque END, muestre las direcciones que acumulan al menos tres fallos y un acceso aceptado posterior. Interprete el resultado.


7.4. Ejercicio 4. Auditoría de credenciales

Realice los siguientes pasos:

  1. Aplique el programa de clasificación de algoritmos del recetario al fichero shadow del laboratorio. Indique qué cuentas requieren actuación y qué medida aplicaría a cada una.

  2. Detecte las cuentas cuya fecha de último cambio está en el futuro y las que no cambian la contraseña desde hace más de 365 días.

  3. Cruce passwd y shadow con NR == FNR para obtener las cuentas de passwd sin entrada en shadow y las de shadow sin entrada en passwd.


7.5. Ejercicio 5. Programa de auditoría reutilizable

Escriba el programa auditoria.awk, que se invoca con awk -F: -f auditoria.awk passwd shadow y que, en una sola ejecución, informa de:

  • Cuentas con UID 0 distintas de root.

  • Registros de passwd con un número de campos distinto de 7.

  • Cuentas de shadow sin contraseña o con algoritmos obsoletos.

  • Un resumen final con el número total de hallazgos.


7.6. Ejercicio 6. Limpieza del entorno

  1. Elimine el directorio del laboratorio, que contiene el fichero shadow ficticio.

8. Recursos y referencias

9. Licencia

Licencia CC BY-NC-ND 4.0

Esta página forma parte del curso Bastionado de Redes y Sistemas © 2026 de José Juan Sánchez Hernández y su contenido se distribuye bajo una licencia Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International.

Esta licencia exige que quienes reutilicen el material otorguen el debido crédito al autor. Permite copiar y redistribuir el material en cualquier medio o formato, únicamente en su forma original, y solo para fines no comerciales.