GNU/Linux >> Tutoriels Linux >  >> Linux

Implications pour la sécurité d'oublier de citer une variable dans les shells bash/posix ?

Si vous suivez unix.stackexchange.com depuis un certain temps, vous
devriez savoir maintenant que laisser une variable
sans guillemets dans le contexte de la liste (comme dans echo $var ) dans les shells Bourne/POSIX
(zsh étant l'exception) a une signification très particulière et
ne devrait pas être fait à moins que vous n'ayez une très bonne raison de le faire.

Il est longuement discuté dans un certain nombre de questions-réponses ici (Exemples :Pourquoi mon script shell s'étouffe-t-il avec des espaces ou d'autres caractères spéciaux ?, Quand est-il nécessaire de doubler les guillemets ?, Expansion d'une variable shell et effet de glob et split dessus, Quoted vs extension de chaîne sans guillemets )

Cela a été le cas depuis la sortie initiale du shell Bourne
à la fin des années 70 et n'a pas été changé par le shell Korn
(l'un des plus grands regrets de David Korn
(question 7 )) ou bash qui copie principalement
le shell Korn, et c'est ainsi que cela a été spécifié par POSIX/Unix.

Maintenant, nous voyons encore un certain nombre de réponses ici et même
parfois du code shell rendu public où
les variables ne sont pas entre guillemets. Vous auriez pensé que les gens auraient
appris maintenant.

D'après mon expérience, il y a principalement 3 types de personnes qui omettent de
citer leurs variables :

  • débutants. Ceux-ci peuvent être excusés car il est vrai que c'est une
    syntaxe complètement non intuitive. Et c'est notre rôle sur ce site
    de les éduquer.

  • les oublieux.

  • des gens qui ne sont pas convaincus même après des martèlements répétés,
    qui pensent que l'auteur du shell Bourne n'a sûrement pas
    l'intention de nous citer toutes nos variables
    .

Peut-être pouvons-nous les convaincre si nous exposons le risque associé à
ce type de comportement.

Quelle est la pire chose qui puisse arriver si vous
oubliez de citer vos variables. Est-ce vraiment ça mauvais ?

De quel type de vulnérabilité parlons-nous ici ?

Dans quels contextes cela peut-il être un problème ?

Réponse acceptée :

Préambule

Premièrement, je dirais que ce n'est pas la bonne façon d'aborder le problème.
C'est un peu comme dire "tu ne devrais pas assassiner des gens parce que
sinon tu iras en prison
".

De même, vous ne mettez pas votre variable entre guillemets car sinon
vous introduisez des failles de sécurité. Vous citez vos
variables parce que c'est mal de ne pas le faire (mais si la peur de la prison peut aider, pourquoi pas).

Un petit résumé pour ceux qui viennent de sauter dans le train.

Dans la plupart des shells, laisser une expansion de variable sans guillemets (bien que
cela (et le reste de cette réponse) s'applique également à la substitution de commande
(`...` ou $(...) ) et développement arithmétique ($((...)) ou $[...] )) a une
signification très particulière. La meilleure façon de le décrire est que c'est comme
invoquer une sorte de split+glob implicite opérateur¹.

cmd $var

dans une autre langue s'écrirait quelque chose comme :

cmd(glob(split($var)))

$var est d'abord découpé en une liste de mots selon des
règles complexes impliquant le $IFS paramètre spécial (le split partie)
puis chaque mot résultant de ce fractionnement est considéré comme
un modèle qui est développé en une liste de fichiers qui lui correspondent
(le glob partie).

Par exemple, si $var contient *.txt,/var/*.xml et $IFS contient , , cmd serait appelé avec un certain nombre d'arguments,
le premier étant cmd et les suivants étant le txt fichiers dans le répertoire courant et le xml fichiers dans /var .

Si vous vouliez appeler cmd avec seulement les deux arguments littéraux cmd et *.txt,/var/*.xml , vous écririez :

cmd "$var"

qui serait dans votre autre langue plus familière :

cmd($var)

Qu'entendons-nous par vulnérabilité dans un shell ?

Après tout, on sait depuis la nuit des temps que les scripts shell
ne doivent pas être utilisés dans des contextes sensibles à la sécurité.
Certes, OK, laisser une variable sans guillemets est un bogue, mais cela ne peut pas
/> faire autant de mal, n'est-ce pas ?

Eh bien, malgré le fait que n'importe qui vous dirait que les scripts shell
ne devraient jamais être utilisés pour les CGI Web, ou que, heureusement,
la plupart des systèmes n'autorisent pas les scripts shell setuid/setgid de nos jours,
un shellshock (le bogue bash exploitable à distance
qui a fait la une des journaux en septembre 2014) a révélé que
les shells sont encore largement utilisés là où ils ne devraient probablement pas :
dans les CGI, dans le client DHCP scripts hook, dans les commandes sudoers,
invoqués par (sinon comme ) commandes setuid…

Parfois sans le savoir. Par exemple system('cmd $PATH_INFO') dans un php /perl /python Le script CGI invoque un shell pour interpréter cette ligne de commande (sans
mentionner le fait que cmd lui-même peut être un script shell et son
auteur peut ne jamais s'attendre à ce qu'il soit appelé à partir d'un CGI).

Vous avez une vulnérabilité lorsqu'il existe un chemin d'élévation des privilèges
, c'est-à-dire lorsque quelqu'un (appelons-le l'attaquant )
est capable de faire quelque chose qu'il n'est pas censé faire.

Invariablement, cela signifie l'attaquant fournissant des données, ces données
étant traitées par un utilisateur/processus privilégié qui
fait par inadvertance quelque chose qu'il ne devrait pas faire, dans la plupart des cas à cause
d'un bogue.

Fondamentalement, vous avez un problème lorsque votre code bogué traite
des données sous le contrôle de l'attaquant .

Maintenant, il n'est pas toujours évident d'où ces données peut provenir,
et il est souvent difficile de dire si votre code arrivera un jour à
traiter des données non fiables.

En ce qui concerne les variables, dans le cas d'un script CGI,
c'est assez évident, les données sont les paramètres CGI GET/POST et
des choses comme les paramètres cookies, path, host….

Pour un script setuid (exécuté en tant qu'un utilisateur lorsqu'
appelé par un autre), ce sont les arguments ou les variables d'environnement.

Un autre vecteur très courant est celui des noms de fichiers. Si vous obtenez une
liste de fichiers à partir d'un répertoire, il est possible que des fichiers y aient été
plantés par l'attaquant .

À cet égard, même à l'invite d'un shell interactif, vous
pourriez être vulnérable (lors du traitement de fichiers dans /tmp ou ~/tmp par exemple).

Même un ~/.bashrc peut être vulnérable (par exemple, bash l'interprétera
lorsqu'il sera invoqué sur ssh pour exécuter une ForcedCommand comme dans git déploiements de serveur avec certaines variables sous le
contrôle du client).

Maintenant, un script ne peut pas être appelé directement pour traiter
des données non fiables, mais il peut être appelé par une autre commande qui le fait. Ou votre
code incorrect peut être copié-collé dans des scripts qui le font (par vous 3
ans plus tard ou par l'un de vos collègues). Un endroit où c'est
particulièrement critique se trouve dans les réponses des sites de questions-réponses car vous
ne saurez jamais où les copies de votre code peuvent se retrouver.

Passons aux choses sérieuses ; à quel point est-ce mauvais ?

Laisser une variable (ou une substitution de commande) sans guillemets est de loin
la source numéro un des vulnérabilités de sécurité associées
au code shell. En partie parce que ces bogues se traduisent souvent par
des vulnérabilités, mais aussi parce qu'il est si courant de voir des variables
sans guillemets.

En relation :Pourquoi utiliser install plutôt que cp et mkdir ?

En fait, lorsque vous recherchez des vulnérabilités dans le code shell, la
première chose à faire est de rechercher des variables sans guillemets. Il est facile à
repérer, souvent un bon candidat, généralement facile à retracer jusqu'aux
données contrôlées par l'attaquant.

Il existe un nombre infini de façons dont une variable sans guillemets peut se transformer
en une vulnérabilité. Je vais juste donner quelques tendances communes ici.

Divulgation d'informations

La plupart des gens tomberont sur des bogues associés à des variables
sans guillemets à cause du split partie (par exemple, il est
courant que les fichiers aient des espaces dans leurs noms de nos jours et l'espace
est dans la valeur par défaut d'IFS). Beaucoup de gens négligeront le glob partie. Le monde la partie est au moins aussi dangereuse que la séparation partie.

Le globbbing effectué sur une entrée externe non nettoyée signifie l'
attaquant
peut vous faire lire le contenu de n'importe quel répertoire.

Dans :

echo You entered: $unsanitised_external_input

si $unsanitised_external_input contient /* , cela signifie l'
attaquant
peut voir le contenu de / . Pas grave. Cela devient
plus intéressant cependant avec /home/* qui vous donne une liste de
noms d'utilisateurs sur la machine, /tmp/* , /home/*/.forward pour
des allusions à d'autres pratiques dangereuses, /etc/rc*/* pour les services
activés… Inutile de les nommer individuellement. Une valeur de /* /*/* /*/*/*... listera simplement l'ensemble du système de fichiers.

Vulnérabilités de déni de service.

En poussant le cas précédent un peu trop loin, nous avons un DoS.

En fait, toute variable sans guillemets dans un contexte de liste avec une entrée
non filtrée est au moins une vulnérabilité DoS.

Même les scripteurs shell experts oublient souvent de citer des choses
comme :

#! /bin/sh -
: ${QUERYSTRING=$1}

: est la commande no-op. Qu'est-ce qui pourrait mal tourner ?

C'est destiné à attribuer $1 à $QUERYSTRING si $QUERYSTRING n'était pas réglé. C'est un moyen rapide de rendre un script CGI également appelable depuis
la ligne de commande.

Ce $QUERYSTRING est toujours développé et parce qu'il n'est
pas entre guillemets, le split+glob l'opérateur est appelé.

Maintenant, il y a des globs qui sont particulièrement chers à
étendre. Le /*/*/*/* l'un est déjà assez mauvais car cela signifie répertorier
les répertoires jusqu'à 4 niveaux vers le bas. En plus de l'activité du disque et du processeur
, cela signifie stocker des dizaines de milliers de chemins de fichiers
(40 k ici sur une VM serveur minimale, dont 10 k de répertoires).

Maintenant /*/*/*/*/../../../../*/*/*/* signifie 40k x 10k et /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/* est suffisant pour
mettre à genoux même la machine la plus puissante.

Essayez-le par vous-même (mais soyez prêt à ce que votre machine
plante ou se bloque) :

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

Bien sûr, si le code est :

echo $QUERYSTRING > /some/file

Ensuite, vous pouvez remplir le disque.

Effectuez simplement une recherche Google sur shell
cgi ou bash
cgi ou ksh
cgi, et vous trouverez
quelques pages qui vous montreront comment écrire des CGI dans des shells. Remarquez
comment la moitié de ceux qui traitent les paramètres sont vulnérables.

Même celui de David Korn
propre
est vulnérable (regardez la gestion des cookies).

jusqu'à des vulnérabilités d'exécution de code arbitraires

L'exécution de code arbitraire est le pire type de vulnérabilité,
puisque si l'attaquant peut exécuter n'importe quelle commande, il n'y a aucune limite à
ce qu'il peut faire.

C'est généralement le partage partie qui mène à celles-ci. Ce
fractionnement entraîne la transmission de plusieurs arguments aux commandes
alors qu'un seul est attendu. Alors que le premier d'entre eux sera utilisé
dans le contexte attendu, les autres le seront dans un contexte différent
donc potentiellement interprété différemment. Mieux avec un exemple :

awk -v foo=$external_input '$2 == foo'

Ici, l'intention était d'attribuer le contenu du $external_input variable shell à foo awk variables.

Maintenant :

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

Le deuxième mot résultant du découpage de $external_input n'est pas assigné à foo mais considéré comme awk code (ici qui
exécute une commande arbitraire :uname ).

C'est particulièrement un problème pour les commandes qui peuvent exécuter d'autres
commandes (awk , env , sed (GNU un), perl , find …) surtout
avec les variantes GNU (qui acceptent les options après les arguments).
Parfois, on ne se douterait pas que des commandes puissent en exécuter
d'autres comme ksh , bash ou zsh c'est [ ou printf

for file in *; do
  [ -f $file ] || continue
  something-that-would-be-dangerous-if-$file-were-a-directory
done

Si nous créons un répertoire appelé x -o yes , alors le test
devient positif, car il s'agit d'une
expression conditionnelle complètement différente que nous évaluons.

Pire, si nous créons un fichier appelé x -a a[0$(uname>&2)] -gt 1 ,
avec toutes les implémentations de ksh au moins (ce qui inclut le sh de la plupart des Unix commerciaux et de certains BSD), qui exécute uname parce que ces shells effectuent une évaluation arithmétique sur les
opérateurs de comparaison numérique du [ commande.

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

Idem avec bash pour un nom de fichier comme x -a -v a[0$(uname>&2)] .

Bien sûr, s'ils ne peuvent pas obtenir une exécution arbitraire, l'attaquant peut
se contenter de dommages moindres (ce qui peut aider à obtenir une exécution
arbitraire). Toute commande pouvant écrire des fichiers ou modifier
les autorisations, la propriété ou avoir un effet principal ou secondaire pourrait être exploitée.

Toutes sortes de choses peuvent être faites avec les noms de fichiers.

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

Et vous finissez par faire .. inscriptible (récursivement avec GNU chmod ).

Scripts effectuant le traitement automatique des fichiers dans des zones publiques en écriture comme /tmp doivent être écrits très attentivement.

Qu'en est-il de [ $# -gt 1 ]

C'est quelque chose que je trouve exaspérant. Certaines personnes prennent tout
la peine de se demander si une extension particulière peut être
problématique pour décider si elles peuvent omettre les guillemets.

C'est comme dire. Hé, ça ressemble à $# ne peut pas être soumis à
l'opérateur split+glob, demandons au shell de le split+glob
.
Ou Hey, écrivons un code incorrect simplement parce que le bogue est
peu susceptible d'être touché
.

Maintenant, à quel point est-ce peu probable? D'accord, $# (ou $! , $? ou toute
substitution arithmétique) ne peut contenir que des chiffres (ou - pour
certains²) donc le glob une partie est sortie. Pour la séparation partie pour faire
quelque chose cependant, tout ce dont nous avons besoin est pour $IFS contenir des chiffres (ou - ).

Avec certains shells, $IFS peut être hérité de l'environnement,
mais si l'environnement n'est pas sûr, la partie est finie de toute façon.

En relation :La redirection vers un nom de fichier global échoue ?

Maintenant, si vous écrivez une fonction comme :

my_function() {
  [ $# -eq 2 ] || return
  ...
}

Cela signifie que le comportement de votre fonction dépend
du contexte dans lequel elle est appelée. Ou en d'autres termes, $IFS devient l'une de ses entrées. À proprement parler, lorsque vous
écrivez la documentation de l'API pour votre fonction, elle devrait être
quelque chose comme :

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

Et le code appelant votre fonction doit s'assurer que $IFS ne contient
pas de chiffres. Tout ça parce que vous n'aviez pas envie de taper
ces 2 guillemets doubles.

Maintenant, pour ce [ $# -eq 2 ] bogue pour devenir une vulnérabilité,
vous auriez besoin d'une manière ou d'une autre pour la valeur de $IFS devenir sous
le contrôle de l'agresseur . En théorie, cela ne se produirait normalement
que si l'attaquant réussi à exploiter un autre bogue.

Ce n'est pas inconnu cependant. Un cas courant est lorsque les gens
oublient de nettoyer les données avant de les utiliser dans une expression
arithmétique. Nous avons déjà vu plus haut qu'il peut autoriser
l'exécution de code arbitraire dans certains shells, mais dans tous, il autorise l'attaquant pour donner à n'importe quelle variable une valeur entière.

Par exemple :

n=$(($1 + 1))
if [ $# -gt 2 ]; then
  echo >&2 "Too many arguments"
  exit 1
fi

Et avec un $1 avec la valeur (IFS=-1234567890) , cette évaluation arithmétique
a pour effet secondaire les paramètres IFS et le prochain [ la commande échoue, ce qui signifie que la recherche de trop d'arguments est
contourné.

Qu'en est-il quand le split+glob l'opérateur n'est pas appelé ?

Il existe un autre cas où les guillemets sont nécessaires autour des variables et autres développements :lorsqu'ils sont utilisés comme modèle.

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

ne teste pas si $a et $b sont les mêmes (sauf avec zsh ) mais si $a correspond au modèle dans $b . Et vous devez citer $b si vous voulez comparer en tant que chaînes (même chose dans "${a#$b}" ou "${a%$b}" ou "${a##*$b*}"$b doit être entre guillemets s'il ne doit pas être pris comme modèle).

Cela signifie que [[ $a = $b ]] peut renvoyer true dans les cas où $a est différent de $b (par exemple lorsque $a est anything et $b est * ) ou peuvent retourner false lorsqu'ils sont identiques (par exemple lorsque les deux $a et $b sont [a] ).

Cela peut-il constituer une faille de sécurité ? Oui, comme n'importe quel bug. Ici, l'attaquant peut modifier le flux de code logique de votre script et/ou casser les hypothèses que votre script fait. Par exemple, avec un code comme :

if [[ $1 = $2 ]]; then
   echo >&2 '$1 and $2 cannot be the same or damage will incur'
   exit 1
fi

L'attaquant peut contourner la vérification en passant '[a]' '[a]' .

Maintenant, si ni cette correspondance de modèle ni le split+glob opérateur applique, quel est le danger de laisser une variable sans guillemets ?

Je dois admettre que j'écris :

a=$b
case $a in...

Là, citer ne nuit pas mais n'est pas strictement nécessaire.

Cependant, un effet secondaire de l'omission des guillemets dans ces cas (par exemple dans les réponses aux questions-réponses) est que cela peut envoyer un mauvais message aux débutants :qu'il peut être correct de ne pas citer les variables .

Par exemple, ils peuvent commencer à penser que si a=$b est OK, alors export a=$b serait aussi bien (ce qui n'est pas le cas dans de nombreux shells car c'est dans les arguments de export commande donc dans le contexte de la liste) ou env a=$b .

Qu'en est-il de zsh ?

zsh a corrigé la plupart de ces maladresses de conception. En zsh (du moins lorsqu'il n'est pas en mode d'émulation sh/ksh), si vous voulez splitter , ou globulage , ou correspondance de modèle , vous devez le demander explicitement :$=var à diviser, et $~var à glob ou pour que le contenu de la variable soit traité comme un modèle.

Cependant, le fractionnement (mais pas le globbing) est toujours effectué implicitement lors de la substitution de commandes sans guillemets (comme dans echo $(cmd) ).

De plus, un effet secondaire parfois indésirable de ne pas citer de variable est la suppression des vides . Le zsh le comportement est similaire à ce que vous pouvez obtenir dans d'autres shells en désactivant complètement le globbing (avec set -f ) et fractionnement (avec IFS='' ). Pourtant, dans :

cmd $var

Il n'y aura pas de split+glob , mais si $var est vide, au lieu de recevoir un argument vide, cmd ne recevra aucun argument.

Cela peut provoquer des bogues (comme l'évident [ -n $var ] ). Cela peut éventuellement briser les attentes et les hypothèses d'un script et entraîner des vulnérabilités.

Comme la variable vide peut entraîner la suppression d'un argument , cela signifie que l'argument suivant pourrait être interprété dans le mauvais contexte.

Par exemple,

printf '[%d] <%s>n' 1 $attacker_supplied1 2 $attacker_supplied2

Si $attacker_supplied1 est vide, alors $attacker_supplied2 sera interprété comme une expression arithmétique (pour %d ) au lieu d'une chaîne (pour %s ) et toute donnée non nettoyée utilisée dans une expression arithmétique est une vulnérabilité d'injection de commande dans les shells de type Korn tels que zsh.

$ attacker_supplied1='x y' attacker_supplied2='*'
$ printf '[%d] <%s>n' 1 $attacker_supplied1 2 $attacker_supplied2
[1] <x y>
[2] <*>

bien, mais :

$ attacker_supplied1='' attacker_supplied2='psvar[$(uname>&2)0]'
$ printf '[%d] <%s>n' 1 $attacker_supplied1 2 $attacker_supplied2
Linux
[1] <2>
[0] <>

Le uname commande arbitraire a été exécuté.

Qu'en est-il quand vous faites besoin du split+glob opérateur ?

Oui, c'est généralement lorsque vous voulez laisser votre variable sans guillemets. Mais ensuite, vous devez vous assurer de régler votre split et glob opérateurs correctement avant de l'utiliser. Si vous ne voulez que le split part et non le glob partie (ce qui est le cas la plupart du temps), alors vous devez désactiver le globbing (set -o noglob /set -f ) et corrigez $IFS . Sinon, vous provoquerez également des vulnérabilités (comme l'exemple CGI de David Korn mentionné ci-dessus).

Conclusion

En bref, laisser une variable (ou une substitution de commande ou
une expansion arithmétique) sans guillemets dans les shells peut être très dangereux
en effet, surtout lorsqu'il est fait dans de mauvais contextes, et il est très
difficile de savoir lesquels sont ces mauvais contextes.

C'est l'une des raisons pour lesquelles cela est considéré comme une mauvaise pratique .

Merci d'avoir lu jusqu'ici. Si cela vous dépasse, ne vous inquiétez pas. On ne peut pas s'attendre à ce que tout le monde comprenne toutes les implications de
l'écriture de son code comme il l'écrit. C'est pourquoi nous avons des recommandations de bonnes pratiques , on peut donc les suivre sans
forcément comprendre pourquoi.

En relation :Comment calculer la moyenne des valeurs d'une colonne en tenant compte des informations d'une autre colonne ?

(et si ce n'est pas encore évident, évitez d'écrire
du code sensible à la sécurité dans des shells).

Et veuillez citer vos variables dans vos réponses sur ce site !


Linux
  1. [ :Opérateur inattendu en programmation shell

  2. Alias ​​avec variable dans bash

  3. Shell - Écrire le contenu d'une variable dans un fichier

  4. Portée variable pour les scripts shell bash et les fonctions dans le script

  5. Comment :Historique Bash/shell illimité ?

.bashrc contre .bash_profile

Comment définir une variable d'environnement dans Bash

Bash Beginner Series #2 :Comprendre les variables dans les scripts shell Bash

Qu'est-ce que le sous-shell sous Linux ?

Différence entre les guillemets simples et doubles dans Bash Shell

8 types de shells Linux