Saltar al contenido
Nuevo

DeepSeek V4.1 Flash gratis por 7 días: 10M tokens al día tras cualquier recarga. Hasta el 30/11.

Cómo funciona

Bash básico para trabajo de seguridad

Aprende pipes, redirección, variables, bucles, códigos de salida y set -euo pipefail, y úsalos para contar logins fallidos y hashear una carpeta.

Principiante14 min de lecturaPor NotHoly

Bash es el shell que está detrás de la mayoría de las terminales de Linux. También funciona en macOS, dentro de muchos contenedores y en Git Bash para Windows, que fue el entorno usado en este artículo. Para quien estudia seguridad, es la forma más rápida de hacerle una pregunta a un archivo de log, comprobar si una carpeta cambió o repetir la misma revisión sobre muchos archivos. No necesitas saber programar para empezar. Basta con una terminal, un editor de texto y cerca de una hora.

Este artículo empieza por lo que hace el shell y después cubre los hábitos que hacen confiable el trabajo en la terminal: pipes, redirección, variables, bucles y códigos de salida. Luego endurecerás un script con set -euo pipefail. Después harás dos ejercicios pequeños. En el primero, contarás los inicios de sesión SSH fallidos por dirección IP en un log de muestra. En el segundo, escribirás un script corto que registra un hash SHA-256 de cada archivo de una carpeta.

Todo lo que aparece aquí se ejecuta sobre archivos que tú mismo creas. Nada de este artículo toca otro sistema.

Qué es Bash

Bash significa Bourne Again SHell. Un shell lee las líneas que escribes, ejecuta otros programas e imprime lo que producen. Bash es uno de varios shells, junto a sh, zsh y fish. Es el predeterminado en muchas distribuciones de Linux, y la mayoría de los scripts que encuentras en internet lo suponen.

En cada sección aparecen dos términos. Un comando es el nombre de un programa seguido de sus argumentos, como grep -c Failed sample.log. Un script es un archivo de texto con una lista de comandos, que Bash lee de arriba abajo. Un script te permite repetir un trabajo sin volver a escribirlo, y permite que otra persona vea exactamente lo que hiciste.

La referencia de cada comportamiento de este artículo es el GNU Bash Reference Manual. Cuando un resultado te sorprenda, revisa el manual antes de confiar en una suposición.

Por qué importa en el trabajo de seguridad

La mayor parte de los datos de seguridad es texto. Logs de autenticación, logs de acceso de servidores web, registros de consultas DNS, informes de escáneres y listas de hashes llegan como líneas. Gran parte del trabajo diario consiste en hacerles preguntas a esas líneas. ¿Cuántas veces falló esta dirección al iniciar sesión? ¿Qué archivos cambiaron desde la semana pasada? ¿Qué cuenta apareció por primera vez hoy?

Una hoja de cálculo o un programa en Python también pueden responder esas preguntas. Bash suele responder más rápido, en una sola línea, y cada paso queda visible. Para quien empieza, esa visibilidad pesa más que la velocidad. Cuando un resultado parece incorrecto, ejecutas sola la primera parte del pipeline y ves lo que produjo.

Los comandos de shell también aparecen en guías de instalación, puntos de entrada de contenedores y tareas de automatización. Leerlos forma parte del trabajo, aunque prefieras escribir en Python.

Cómo funciona, paso a paso

Los pipes envían la salida de un programa al siguiente

Un pipe, escrito como |, conecta la salida estándar del comando de la izquierda con la entrada estándar del comando de la derecha. La mayoría de las herramientas de línea de comandos lee líneas, hace una tarea e imprime líneas. El shell las une.

bash
echo "one two three" | wc -w

Salida:

text
3

wc -w cuenta palabras. No necesita nombre de archivo, porque el pipe le entrega el texto que produjo echo. Programas pequeños unidos por pipes son el hábito central del trabajo en el shell.

La redirección decide a dónde va la salida

La redirección cambia desde dónde un comando lee o hacia dónde escribe. > escribe la salida estándar en un archivo y reemplaza su contenido. >> agrega al final. 2> envía la salida de error, el canal donde los programas imprimen sus quejas, a un archivo. 2>&1 envía la salida de error al mismo lugar que la salida estándar.

bash
echo first > notes.txt
echo second >> notes.txt
cat notes.txt

Salida:

text
first
second

El shell abre el archivo y lo vacía antes de ejecutar el comando. Por eso, esta línea destruye los datos que se suponía que iba a ordenar:

bash
sort d.txt > d.txt

Después de esa línea, d.txt queda vacío. En un archivo de prueba, el resultado fueron cero bytes. Escribe en un archivo nuevo, revisa el resultado y solo entonces reemplaza el anterior.

La salida de error importa en seguridad porque, muchas veces, el error contiene la respuesta. Un archivo que no existe o un mensaje de permiso denegado son evidencia. Redirigir los errores con 2>/dev/null deja la salida más limpia, pero un archivo inexistente entonces parece igual que un archivo sin coincidencias. Mientras aprendes, mantén visibles los errores.

bash
ls missing-file 2>/dev/null
echo "status=$?"

Salida:

text
status=2

El texto del error fue al descarte, así que la terminal muestra solo el código de estado.

Variables y comillas

Una variable guarda un valor. Se asigna sin espacios alrededor del signo igual y se lee poniendo un signo de dólar delante del nombre.

bash
target="10.0.0.5"
echo "Checking $target"

Pon siempre las variables entre comillas dobles. Sin comillas, el shell parte el valor en los espacios y puede expandir los trozos como comodines. La diferencia aparece en cuanto el valor tiene un espacio:

bash
name="a b"
printf "[%s]\n" $name
printf "[%s]\n" "$name"

Salida:

text
[a]
[b]
[a b]

El primer printf recibió dos palabras, porque el shell dividió el valor. El segundo recibió una. Lo mismo pasa con un archivo llamado mi informe.txt si escribes cat $file sin comillas.

Los scripts reciben argumentos en variables numeradas. $1 es el primer argumento, $2 el segundo. La expresión "${1:-.}" significa "usa el primer argumento si está definido y no está vacío; si no, usa el directorio actual". El script de hashes la usa.

Los bucles repiten un comando por cada elemento

Un bucle for ejecuta su cuerpo una vez por cada palabra de una lista. Una prueba entre corchetes, como [ "$n" -eq 2 ], devuelve 0 cuando la condición es verdadera y 1 cuando es falsa. Un if usa ese estado para elegir una rama.

bash
for n in 1 2 3; do
  if [ "$n" -eq 2 ]; then
    echo "stop at $n"
    break
  fi
  echo "ok $n"
done

Salida:

text
ok 1
stop at 2

break sale del bucle antes de tiempo, así que el tercer elemento nunca se ejecuta.

Los códigos de salida indican si un comando tuvo éxito

Cada comando termina con un estado de salida, un número de 0 a 255. Cero significa éxito. Cualquier otro valor significa fallo, y cada programa define sus propios números. El grep de GNU devuelve 0 cuando selecciona al menos una línea, 1 cuando nada coincide y 2 cuando hay un error. Lee el estado con $? en la línea siguiente.

bash
grep -q Failed sample.log
echo "match=$?"
grep -q nomatch sample.log
echo "nomatch=$?"

Salida:

text
match=0
nomatch=1

La opción -q suprime la salida y solo fija el estado, lo que permite usar grep dentro de un if sin imprimir nada.

Un pipeline tiene un solo estado, y por defecto es el estado de su último comando. La siguiente sección cambia eso.

Modo estricto: set -euo pipefail

Bash sigue ejecutando después de los errores, a menos que le indiques lo contrario. Tres opciones hacen que un script falle pronto. Están documentadas en el comando interno set del manual.

  • set -e detiene el script cuando un comando falla. Los comandos dentro de una prueba if, y los comandos de una lista && o || distintos del último, no lo detienen, porque Bash espera que a veces fallen.
  • set -u trata una variable sin definir como error, así que un error de escritura en el nombre de una variable detiene la ejecución en lugar de volverse una cadena vacía sin aviso.
  • set -o pipefail hace que un pipeline falle si cualquiera de sus comandos falla, y no solo el último.

Primero, el comportamiento por defecto:

bash
bash -c 'false | true; echo "status=$?"'

Salida:

text
status=0

El false falló, pero true fue el último y tuvo éxito, así que el pipeline informó éxito. Con pipefail:

bash
bash -c 'set -o pipefail; false | true; echo "status=$?"'

Salida:

text
status=1

Ahora set -e con un comando que falla:

bash
bash -c 'set -e; echo one; false; echo two'

Salida:

text
one

El script se detuvo antes de two. La llamada bash -c en sí terminó con estado 1.

set -u detecta un argumento faltante:

bash
bash -c 'set -u; echo "$1"' _

Salida:

text
_: line 1: $1: unbound variable

La forma combinada, set -euo pipefail, es lo que la mayoría llama modo estricto. Tiene un costo. Una búsqueda que no encuentra nada es un resultado normal, pero grep lo informa con estado 1, así que en modo estricto el script se detiene. Prueba ese caso antes de depender de él:

bash
bash -c 'set -euo pipefail; grep "Nope" sample.log | wc -l; echo "after"'

Salida:

text
0

El script imprimió el conteo y luego se detuvo sin imprimir after. Si "ninguna coincidencia" es un resultado válido para tu script, agrega || true después del pipeline, o revisa el estado tú mismo.

Práctica: contar inicios de sesión fallidos por IP

Vas a crear un archivo de log pequeño y contar cuántas veces aparece cada dirección de origen en las líneas de contraseña fallida. El log es sintético. Sus direcciones vienen del rango privado 10.0.0.0/8, y las líneas siguen el formato de los mensajes de OpenSSH en un host de laboratorio. Los logs reales de autenticación contienen direcciones de personas reales, así que trátalos como datos privados y lee solo logs que tengas permiso de leer.

Crea el log de muestra

Crea una carpeta para este laboratorio, entra en ella y escribe el archivo. El 'EOF' entre comillas evita que Bash expanda cualquier cosa dentro del texto.

bash
cat > sample.log <<'EOF'
Jan 10 10:00:01 lab sshd[1]: Failed password for root from 10.0.0.5 port 5000 ssh2
Jan 10 10:00:03 lab sshd[2]: Failed password for admin from 10.0.0.5 port 5001 ssh2
Jan 10 10:01:00 lab sshd[3]: Accepted publickey for alice from 10.0.0.9 port 5002 ssh2
Jan 10 10:02:00 lab sshd[4]: Failed password for bob from 10.0.0.7 port 5003 ssh2
EOF

Comprueba el número de líneas con wc -l sample.log. Debe informar 4.

Cuenta los fallos en un solo pipeline

bash
grep 'Failed password' sample.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nr

Salida:

text
      2 10.0.0.5
      1 10.0.0.7

Lee el pipeline paso a paso. Ejecuta los dos primeros comandos solos para ver qué aporta cada etapa.

  1. grep 'Failed password' sample.log conserva las tres líneas de fallo y descarta la línea Accepted. Confirma el conteo con grep -c 'Failed password' sample.log, que imprime 3.
  2. awk '{print $(NF-3)}' imprime un campo de cada línea. NF guarda el número de campos de la línea actual. Cada línea termina con from DIRECCIÓN port PUERTO ssh2, así que la dirección es el cuarto campo contando desde el final, que es $(NF-3). Compruébalo en la primera línea:
bash
sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'

Salida:

text
14 10.0.0.5

La línea tiene 14 campos, y el campo 11 es la dirección.

  1. sort pone las direcciones iguales una al lado de otra. uniq -c cuenta los duplicados contiguos e imprime el conteo al principio. Sin el sort, uniq solo contaría repeticiones vecinas, por eso la ordenación siempre va primero.
  2. sort -nr ordena los conteos numéricamente, del mayor al menor.

El número del campo en awk pertenece a este formato de log. Si apuntas el mismo comando a otro log, revisa primero la posición del campo con la línea de sed de arriba. Que un número haya funcionado una vez no prueba que sirva para el siguiente archivo.

El mismo resultado puede salir de un solo programa awk, porque los arreglos de awk cuentan por ti:

bash
grep 'Failed password' sample.log | awk '{c[$(NF-3)]++} END {for (ip in c) print c[ip], ip}'

Salida:

text
2 10.0.0.5
1 10.0.0.7

awk no garantiza un orden para for (ip in c), así que ordena el resultado si el orden importa. La GNU Awk User's Guide explica los arreglos y la regla END.

Práctica: calcular el hash de cada archivo de una carpeta

Un hash es una huella digital de tamaño fijo del contenido de un archivo. Si registras hoy el hash SHA-256 de cada archivo de una carpeta, puedes ejecutar el mismo comando la próxima semana y ver qué archivos cambiaron. Un hash no demuestra quién hizo el cambio ni que el archivo sea seguro. Solo muestra si los bytes cambiaron desde que los registraste.

Escribe el script

Crea hashall.sh en la misma carpeta, con el mismo patrón de heredoc entre comillas.

bash
cat > hashall.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do [ -f "$f" ] && sha256sum "$f"; done
EOF

Lee el script línea por línea:

  • La primera línea es el shebang. Indica al sistema que ejecute el archivo con Bash, encontrado mediante env.
  • set -euo pipefail activa el modo estricto, como vimos antes.
  • "${1:-.}"/* es la carpeta que pasas, o la carpeta actual si no pasas nada. El * es un glob. El shell lo reemplaza por los nombres de esa carpeta, ordenados. Los nombres que empiezan con punto se omiten por defecto, como describe la sección de expansión de nombres de archivo del manual.
  • [ -f "$f" ] && sha256sum "$f" calcula el hash solo de los archivos regulares. sha256sum sobre una carpeta imprimiría un error, así que la prueba deja fuera las carpetas.

Hazlo ejecutable y ejecútalo sobre la carpeta actual:

bash
chmod +x hashall.sh
./hashall.sh .

chmod +x agrega el permiso de ejecución. El ./ indica al shell que ejecute el archivo de esta carpeta, porque la carpeta actual no está en PATH por defecto.

Salida en una carpeta que tenía solo hashall.sh y sample.log:

text
288797e3d2baa72b086c5fca9821282d06726cb44272647d41128ababf255c94 *./hashall.sh
2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893 *./sample.log

Git Bash imprime un asterisco antes de cada nombre, que marca el modo binario en GNU coreutils. Otros sistemas pueden mostrar un espacio. El valor del hash no cambia. Si los tuyos son distintos, revisa que los archivos sean idénticos byte a byte, incluidos los finales de línea.

Comprueba un valor a mano:

bash
sha256sum sample.log

Imprime para sample.log el mismo hash que imprimió el script.

Mira cambiar un hash

Agrega una línea al log y vuelve a calcular el hash.

bash
echo "x" >> sample.log
sha256sum sample.log

En la prueba, el hash pasó a ser 3084d4c5a7659a2f3636b6079636b081b7ab1b6fca6a5e691c3afdd160959687, en lugar de 2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893. El archivo creció dos bytes y la huella cambió con él. Restaura el archivo original desde tu copia antes de seguir.

Errores comunes

Variables y rutas sin comillas. cat $file se rompe con nombres que tienen espacios. Usa comillas por defecto y quítalas solo cuando sepas que el valor no puede tener espacios ni comodines.

Sobrescribir un archivo de entrada con >. sort d.txt > d.txt vacía el archivo antes de que sort lo lea. Escribe en un archivo nuevo, revisa la salida y luego muévelo a su lugar.

Confiar en un número de campo sin probarlo. $(NF-3) funcionó con la muestra. Prueba la posición del campo en unas cuantas líneas reales antes de construir un informe sobre ella.

Modo estricto que detiene el script ante una búsqueda sin resultados. Como se mostró arriba, que grep devuelva 1 termina un pipeline con pipefail. Decide si "ninguna coincidencia" es un error para tu script y trátalo a propósito.

Un bucle que informa el estado equivocado. El hashall.sh de arriba calcula el hash de cada archivo regular. Sin embargo, cuando el último nombre de la carpeta es una subcarpeta, el script imprime todos los hashes y después sale con estado 1. El estado venía de la última prueba [ -f ], que falló. La forma explícita con if hace el mismo trabajo y sale con 0:

bash
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do
  if [ -f "$f" ]; then sha256sum "$f"; fi
done

En una carpeta de prueba con una subcarpeta como último elemento, esta versión imprimió el hash de cada archivo y salió con estado 0. La versión con && también sale con estado 1 en una carpeta vacía, así que la forma con if es mejor opción por defecto.

Ejecutar scripts que no escribiste tú. Un script puede borrar o enviar archivos con la misma facilidad con la que calcula hashes. Lee el archivo completo antes, revisa cada comando que llama y ejecútalo sobre una copia de tu carpeta.

Cómo seguir

Lee las secciones del GNU Bash Reference Manual sobre pipelines, redirecciones, expansión de parámetros y el comando set. Cubren la mayor parte de lo que viste aquí. Usa la parte sobre códigos de salida como referencia cuando un script se comporte de forma inesperada.

Después, lee el manual de GNU grep. Prueba grep -i, grep -v y grep -E sobre tu log de muestra y observa cómo cada opción cambia el estado de salida.

La GNU Awk User's Guide cubre campos, patrones y arreglos. El contador de una línea de arriba es un ejemplo pequeño de lo que puede hacer awk. Cuando te resulte natural, escribe un segundo patrón que cuente los inicios de sesión aceptados por usuario.

Mantén los scripts cortos. Un script de diez líneas que puedas explicar línea por línea es más útil que uno de cien líneas copiado que no sabes modificar. Cuando un script pase de una pantalla, sepáralo en funciones o considera un lenguaje con mejores estructuras de datos para la tarea.

Fuentes

Fuentes

  1. GNU Bash Reference Manual
  2. GNU grep manual
  3. GNU Awk User's Guide

Más en Linux y herramientas de desarrollo

Linux y herramientas de desarrollo

Permisos en Linux: usuarios, grupos, chmod y umask

Propietarios, grupos, bits rwx, chmod, umask y setuid en Linux, con un laboratorio que prueba el acceso de otro usuario y busca modos riesgosos.

Principiante13 min de lectura

Linux y herramientas de desarrollo

Claves SSH: cómo funcionan y cómo usarlas con seguridad

Cómo funcionan las claves SSH de host y de usuario, por qué Ed25519 con frase de paso es el estándar y cómo proteger el acceso en una VM de laboratorio.

Principiante11 min de lectura