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.
local
Le mot-clé 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.
Trap
La commande 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.
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).