Bash: l'antisèche ultime

Disclaimer

Faites un bash --version si une ou plusieurs des astuces ne fonctionnent pas chez vous. Certaines pourraient nécessiter Bash 4 même si la grande majorité ont été testées avec bash 3.

Simple, basique

Il faut bien commencer quelque part..

Bash est un acronyme

Bash veut dire “Bourne Again Shell”, ce qui fait référence à “born again shell”. Bash est la version plus récente du vieux Bourne shell sortie en 1979.

Bash est un language de scripting “Unix-versel”

Bash est tellement pratique pour automatiser des tâches que ce soit pour des projets persos, de la maintenace de serveurs ou encore des actions type devOps. Cela peut consister à écrire une série de commandes dans un fichier texte.

Vous pouvez tout à fait taper ces commandes dans le terminal et utiliser grep, sed, find ou d’autres commandes issues de librairies tierces installées sur la machine.

Cependant, l’intérêt est souvent de grouper toutes ces instructions dans des fichiers afin de pouvoir les versionner dans Git et les transférer d’une machine à l’autre ou encore des les utiliser sur de multiples serveurs. En général, les scripts Bash sont des fichiers sans extensions qu’on exécute de cette façon :

bash myscript

On peut aussi modifier les permissions du fichier pour le rendre exécutable et le lancer directement:

chmod +x myscript && ./myscript

Bash n’est pas sh!

Bash et sh ont des syntaxes très similaires mais sh ne supporte pas toutes les commandes Bash. Les lignes qui suivent peuvent très bien marcher mais c’est assez confusant et sujet aux erreurs:

bash mysh.sh
sh myscript

L’importance du Shebang

C’est le fameux #! au début des fichiers. “Bang” fait référence au point d’exclamation “!”:

#!/usr/bin/env bash

Ce n’est pas du tout pour faire joli ! Cela indique le chemin absolu vers l’interpréteur Bash de la machine ou du serveur. /usr/bin/env est un choix assez malin en général car il va chercher l’exécutable dans la variable utilisateur $PATH.

Commenter le code

Utilisez#:

# comment

Variables

Les variables sont très simples à manipuler avec Bash.

Variables natives (liste incomplète)

echo "$1 $2" # positional arguments passed one by one, e.g., bash myscript arg1 arg2
echo "$#" # number of arguments passed
echo "$@" # all arguments passed
echo "You are here: $PWD" # $PWD is the current path

Assigner de nouvelles variables

Utilisez le signe = sign mais surtout aucun espace autour:

MyVar="My string"
MyArray=(all in one)

Utiliser les variables

Écrivez le signe $:

echo $Variable
echo "This is my var: $Variable" # double quotes are required for interpolation
echo "${MyArray[@]:0:3}" # "all in one", because it prints 3 elements starting from the first (0)

# Be careful the following does not display a value
echo ${#MyArray[2]} # "3" as there are 3 chars in the third element of the array, which is the word "one"

Modifier les variables

Utiliser les accollades {}:

MyVar="My variable"
# Length
echo ${#MyVar}

# Substitution
echo ${MyVar//a/O} # "My vOriOble"

# Expansion
echo ${!BASH@} # all variable names than begin with "BASH"
echo ${!BASH*} # all variable names than begin with "BASH"

# Removals
MyFile="list.txt"
echo ${MyFile%.*} # the filename without extension
echo ${MyFile##*.} # the file's extension

# Default value
echo ${AnotherOne:-"Another one"} # displays "Another one" even if AnotherOne is not defined

Dictionnaires

declare -A Movies

Movies[title]="The Dark Knight Rises"
Movies[bestActress]="Anne Hathaway"
Movies[director]="Christopher Nolan"

printf

printf fonctionne un peu comme dans C pour afficher du texte préformaté. Il y a bien plus de possibilités qu’avec echo. On définit des formats et on passe des paramètres, la fonctionne affiche ensuite le texte formaté.

Exemples :

printf "%s\n" "Learn it" "Zip it" "Bash it" # it prints 3 lines
printf "%.*f\n" 2 3.1415926 # prints 3.14

Boucle, boucle, boucle

For

for i in "${MyArray[@]}"; do
    echo "$i"
done

Avec des chaînes de caractère:

MyString="First Second Third"
for n in $MyString
do
   echo "$n line"
done

While

while [ true ]
do
    echo "We loop"
    break
done

Ranges

echo {1..7} # 1 2 3 4 5 6 7
echo {a..g} # a b c d e f g

Until

until [ $counter == 111 ]
do
    echo "Number is: $((counter++))"
done

Inputs utilisateur

echo "6x7?"
read answer
echo answer # hopefully 42 but can be anything the user wants

Conditions

if [[ CONDITION1 || CONDITION2 ]]
then
    # code
elif [[ CONDITION3 && CONDITION4 ]]
then
    # code
else
    # code
fi

On peut aussi utiliser un case statement:

case $COLOR in

  Red)
    echo -n "red"
    ;;

  Blue | Green)
    echo -n "blue or green"
    ;;

  Yellow | "Sunny" | "Gold" | "Fire")
    echo -n "Yellow"
    ;;

  *)
    echo -n "What?"
    ;;
esac

Erreurs & stratégies d’exit

Il vaut mieux élever un peu votre jeu et renvoyer des erreurs pour éviter toute mauvaise utilisation de vos scripts.

Exit si une commande échoue

#!/usr/bin/env bash
set -e

Exit N

Exit avec un code spécifique:

#!/usr/bin/env bash
if [ CONDITION ]; then
    exit 0 # 0 1 2 3 ... N
fi

0 indique le succès et {1,2,3, N} sont réservés aux erreurs par convention.

Tester les erreurs

# $? is the exit status of last command
if [[ $? -ne 0 ]] ; then
	echo "You failed!"
    exit 1
else
    echo "You are the best!"
fi

Débug

#!/usr/bin/env bash
set -x # set -n can also be used to check syntax errors
ARGS=("$@") # store all script args in a var

On peut aussi passer l’option -x dans le terminal:

bash -x myscript

Système de fichiers & répertoires

Toutes les commandes habituelles type cp, rm, ls, mv ou encore mkdir fonctionnent.

Par exemple, vous pouvez vous assurer que le fichier est exécutable avant de faire quoi que ce soit :

if [ ! -x "$PWD/myscript" ]; then
    echo "File is not executable!"
    exit 1
fi

Utilisez -f pour les fichiers, -d pour les répertoires, -e pour tester l’existence, etc. Il y a des tonnes d’options possibles, vous pouvez même tester des liens symboliques avec -L ou enore comparer des fichiers avec les options -nt (plus récent que) and -ot (plus vieux que).

Usages un peu plus avancés

Au-delà du basique.

Profitez de la souplesse de la syntaxe

Les brace expansions permettent de construire rapidement des affichages relativement complexes :

echo {1..10}{0,5}h # bash 3
echo {10..120..10}km # requires bash 4

Définir des fonctions

function test ()
{
    echo "$1 $2" # displays positional arguments passed to the function ("All" "In")
    return 0 # if you return a value, it !must be numerical!
}
test "All" "In"

Les scopes

On n’a pas besoin de déclarer une variable pour l’utiliser dans Bash. Par défaut, aucune erreur n’est renvoyée quand une variable n’est pas définie, seulement une valeur vide à la place

Variables d’environnement

Les variables d’env sont les plus disponibles, elles existent partout au niveau système. Utilisez printenv pour les lister ou inspecter l’une d’entre elles en particulier :

printenv USER # if you don't specify a variable, all variables will be displayed

Variables shell

Comme suit :

TEST="My test"

Elles s’utilisent partout dans un même script, y compris dans les fonctions. Cependant, elles ne sont pas passées aux processus enfant à moins d’utiliser explicitement export pour transmettre l’info.

Le mot-clé local

S’utilise uniquement dans une fonction :

function test ()
{
    local MyVar="My string" # local variable
}
test

On ne peut pas exporter des variables locales.

Bonnes pratiques

Il vaut mieux limiter le nombre de variables shell et utiliser des variables locales dans des fonctions pour éviter les écrasements involontaires et les confusions parfois à l’origine d’erreurs importantes.

Boucler sur le résultat d’une commande

for r in $(ls $PWD)
do
	# task using the result of the command => r
	echo "$r"
done

Utiliser l’affichage d’une fonction

function hello() {
	echo "Hello Ethan"
}

echo "This is the result of the function : $(hello)"

Stocker le résultat d’une commande dans une variable

Users=$(cat users.txt)

Capturer les inputs yes/no

read -p "Continue? [y/n]: " -n 1 -r
echo # extra line
if [[ $REPLY =~ ^[Yy]$ ]]
then
    echo "ok"
elif [[ $REPLY =~ ^[Nn]$ ]]
then
	echo "Too bad!"
else
	echo "Sorry, I did not understand your answer :("
fi

Capturer la sélection d’un utilisateur

Ce code affiche une sélection à l’utilisateur et capture son choix :

select w in "zsh" "bash" "powershell" 
do
  echo "You prefer $w"
  break  
done

Alias et écrasements

On peut créer des alias facilement :

alias lz='ls -alh'

On peut aussi tout simplement redéfinir des commandes.

Chaîner les commandes

wget -O /tmp/logo.jpg 'https://dev-to-uploads.s3.amazonaws.com/uploads/logos/resized_logo_UQww2soKuUsjaOGNB38o.png' && echo "echo only if the first hand is true"
wget -O /tmp/logo2.jpg 'https//example.com/logo.jpg' || echo "echo only if the first hand is wrong"
wget -O tmp/logo3.jpg 'https//example.com/logo.jpg' ; echo "echo whatever happens"

Passer le jeu en mode difficle

Ces concepts sont beaucoup plus avancés que les précédents.

Exécuter à nouveau la dernière commande exécutée

sudo !!

Redirections, stderr, stdout

COMMAND > out.txt   # write in out.txt 
COMMAND >> out.txt  # append in out.txt 
COMMAND 2> test.log # stderr to test.log
COMMAND 2>&1        # stderr to stdout
COMMAND &>/dev/null # stdout and stderr to (null)

Remplacez COMMAND avec votre commande.

La commande Trap

Permet une gestion avancée des erreurs mais à ne pas confondre avec les exceptions qu’on peut retrouver dans d’autres langages. L’idée est d’exécuter du code dans des conditions très spécifiques :

#!/usr/bin/env bash

trap 'catch' EXIT

catch() {
  echo "Exit game!"
}

echo "ok"
echo "---"

exit 0

“Exit game!” s’affiche quand exit 0 est déclenché.

Dans la vraie vie, on voit souvent ce genre de code :

#!/usr/bin/env bash

trap "rm /tmp/scan.txt" EXIT

Exécuter des scripts dans un script Bash

Utiliser la commande source :

#!/usr/bin/env bash

source ./otherScript

Subshells

A subshell is a separate instance of the command processor. A shell script can itself launch subprocesses. These subshells let the script do parallel processing, in effect executing multiple subtasks simultaneously.

Source: tldp.org

En gros, toutes les commandes qui sont mises entre parenthèses, ce qui justifie l’utilisation de export pour passer les variables utiles aux sous-processus.

On retrouve parfois le terme de “forks”. C’est la même idée.

pipefail

On rentre dans une gestion des erreurs plus avancées et pro :

#!/usr/bin/env bash

set -eu # u is to exit if using undefined variables
set -o pipefail

Non seulement on active le “mode erreurs” mais aucune erreur dans les pipelines ne sera ignorée, ce qui est le comportement par défaut sans l’option -o pipefail.

Mes astuces favorites pour le terminal

C’est plus lié aux système Unix mais pas complètement hors-sujet ici non plus🤞🏻.

bash

À entrer dans le terminal pour passer en mode Bash quelque soit le système en place (ex : zsh) :

bash

Pressez ensuite entrée et vous pouvez ensuite taper man bash pour explorer l’aide.

\

\ améliore la lisibilité et évite la perte de temps en découpant les lignes de commande trop longues en plusieurs lignes moins larges.

&

nohup bash script &

On peut exécuter notre bash en arrière-plan tout en capturant le signal hangup.

Divers

Des astuces de la vie courante sous Bash :

Tester si une commande existe avant de l’utiliser

if ! [ -x "$(command -v git)" ]; then
    echo "Git is not installed!"
    exit 1
fi

Lister les sous-répertoires dans un répertoire

SUBDIRS=$(ls -d */)
for sub in $SUBDIRS
do
    echo $sub
done

Renommer un fichier rapidement

mv /project/file.txt{,.bak} # rename file.txt.bak

Exécuter un script sur un serveur distant

ssh REMOTE_HOST 'bash -s' < myScript

Trouver rapidement les fichiers volumineux (exemple ici avec plus de 10 MB)

find . -size +10M -print

Créer de multiples fichiers rapidement

Vous pouvez toujours faire ça :

touch {script1,script2,script3,script4,script5,script6,script7}

Mais cela reste très pénible à écrire donc faites plus ça :

touch script{1..7}

Bash pour les hackers

Les hackers aiment bien le Bash en général. Même si Python est pratique pour le pen-testing, bash permet d’automatiser beaucoup de tâches. Les professionnels s’en servent pour accélérer les phases d’analyse notamment.

Hello world: scanning

Il vaut mieux grouper des instructions finalement assez courantes dans des fichiers réutilisables plutôt que de répéter sans arrêt les mêmes lignes dans le terminal.

Vous pouvez passer par read pour rendent le tout plus agréable ou simplement passer des arguments. Par exemple, écrivons un binaire rapidement, on nommera le fichier scanning:

#!/usr/bin/env bash

echo "Welcome aboard"
echo "What's the targeted IP?"
read TargetIp

if [[ ! $TargetIp ]]
    then echo "Missing targeted IP!"
    exit 1
fi

echo "What port do you want to scan?"
read PortNumber

if [[ ! $PortNumber ]]
    then echo "Missing port number!"
    exit 1
fi

nmap $TargetIp -p $PortNumber >> scan.txt # append results in scan.txt

Ensuite, il nous reste simplement à lancer bash scanning. Bon, j’admets que ce n’est pas très réaliste car il existe déjà bon nombre de solutions pour automatiser et enrichire nmap et l’exemple ici est très incomplet mais vous voyez l’idée j’espère.

Alias

Les alias peuvent faire gagner un temps fou :

alias whatsMyIp="echo $(ifconfig -a | grep broadcast | awk '{print $2}')"
whatsMyIp # IP local

J’admets que les distributions de Pen-testing ont des auto-complétions plus performantes qui renderaient cet exemple inutile mais on n’est pas toujours sur ce genre d’environnements sur-optimisés donc ça reste judicieux.

Hacker le Bash

Tout est piratable ou presque, en premier lieu les langages informatique. Lisez le post de hacktricks (en).

Aller plus loin

Votre voyage peut débuter ici