Wie im Shellcode Guide 2321e8d6b3c3 beschrieben gilt die Regel "Traue keinem Input"
Idealerweise und als Best-Practice definierst du gültige Zeichen, statt ungültige. Also z.B. nimmst du ASCII Buchstaben, Zahlen, Underscore, Space und minus als legitim an und filterst alles weg, was nicht diesen Zeichen entspricht:
tr -dc '0-9a-zA-Z._-'
und vermeide es, überhaupt Pfade übergeben zu müssen. Definiere die gültigen/konformen Dateinamen und Ordnernamen und lass nur solche Namen zu. Dann muss auch kein / vorhanden sein und auch andere problematische Zeichen sind ausgeschlossen.
Das ist suboptimal. Wenn du zwingend nur ungültige Zeichen definieren kannst, z.B. weil die restliche Zeichenmenge der gültigen Zeichen umfangreicher oder unbestimmt sein muss, kann dies gerade bei der Übergabe von Variablen sehr kritisch sein. Insbesondere dann, wenn die diese Variablen für Dateipfade verwendest und ein Angreifer zur Manipulation von Dateipfaden dann Zeichen einstreuen kann.
Es genügt aber nicht, einfach nur
/ \herauszufiltern, da es von der nachfolgenden pipeline abhängt, ob man dort evtl. noch anders Zeichen für Pfade codiert übergeben kann. Wenn du generell keine Dateipfade brauchst, aber deine Zeichenfolge später einen Dateinamen beschreiben könnte, filterst du idealerweise bei der Variablenübergabe mindestens diese fünf Zeichen
/ \ % $ &und das Nullbyte heraus. Also folgender Befehl
tr -d '\0&%/\\$'
Wichtig für tr in diesem Kontext: Verwende einfache Hochkomma, also ' nicht ". Und Auch unter Linux musst du \ herausfiltern. Dazu später mehr.
Ok, wenn du jedoch darauf angewiesen bist, auch Pfade übergeben zu müssen, und daher Slashes nicht wegfiltern kannst, existieren die kombinierbaren kritischen Zeichen . und /
Mit diesen Zeichen wird es möglich, relative Pfade zu bilden in der Form ../../illegitimes_ziel
Nun genügt es aber nicht, wenn du einfach "../" wegfilterst und durch "" ersetzt!
Betrachte diese Testfälle:
....//....//irgendwas/geheimes.txt
Wenn du nur die Sequenz "../" durch eine leere Zeichenfolge ersetzt, bleibt das Problem also bestehen.
echo ....//....//irgendwas/geheimes.txt | sed "s|\.\./||g" # WARNUNG! ../../irgendwas/geheimes.txt
Zum Verständnis des sed befehls: Für die sedline ist es wichtig, den dot mit backslash \ explizit anzugeben, sonst gilt er als "universelles" Zeichen.
Nochmal zum Problem:
Vermeide: sed "s|\.\./||g" # WARNUNG!
Ein einfacher, wenn auch suboptimaler Lösungs-Ansatz besteht darin, einfach nur aufeinanderfolgende Punkte zu filtern. Ohne den Slash!
sed "s|\.\.||g" # WARNUNG. Als alleiniger Filter in der Pipeline nicht hinreichend!
Testen wir...
echo ....//....//irgendwas/geheimes.txt | sed "s|\.\.||g" # WARNUNG! ////irgendwas/geheimes.txt
Hm.. Sieht gut aus? Nicht mehr angreifbar? NEIN ist nicht gut! immer noch angreifbar, auch wenn man keine keine . hat und es jetzt halt ein ungültiger Pfad zu sein scheint, der nicht weiter ausgehebelt werden kann.
Der Grund: sed allein ist hierfür zu gefährlich, da man ja in der Pipeline noch beliebige andere Zeichen injecten kann, die den Schutz aushebeln, weswegen dann ein punkt durchrutscht. Siehe ganz konkret:
echo -e "...\0...\0/...\0./irgendwas/geheimes.txt" | sed "s|\.\.||g" # WARNUNG!
../../irgendwas/geheimes.txt
Dazu später noch mehr. Daher ist für dieses Szenario ein in der Pipeline vorangestelltes tr extrem wichtig, um Zeichen wegzufiltern! Ein sed allein reicht nicht.
Nutze: tr -d '\0&%$\\' | sed "s|\.\.||g" # Sicher aber suboptimal
Kein Sicherheitsproblem. Beachte aber, dass du durch das Wegwerfen zweier aufeinander folgender Punkte nun auch potentiell gültige und unkritische Datei- und Ordnernamen nicht mehr korrekt auswerten kannst:
erbsen..linsen..bohnen.txt
Der Dateiname selbst wäre an sich harmlos, wird damit aber zu einem ungültigen Namen, weil du auch dort die aufeinanderfolgenden Punkte entfernst.
Für derartige Szenarien stünde diese universelle aber hässlich notierte und wegen Escaping leicht zu verwechselende Variante zur Verfügung. Und ich zeige auch hier noch einmal, warum das Nullbyte-Filtering wichtig ist. Im tr lassen wir das Nullbyte mal durch zu Beginn...
tr -d '&%$\\' | sed "s|.\.\/||g;s|\.\./||g" # WARNUNG!
Also...alles gut mit dieser Variante ? Testen wir...
echo "../../../erbsen/../linsen..bohnen/bulgur" | tr -d '&%$\\' | sed "s|.\.\/||g;s|\.\./||g" # WARNUNG! erbsen/linsen..bohnen/bulgur
Nur aufs Erste korrekt oder? Nein! Nicht gut.. Sie ist natürlich noch anfällig für den Nullbyte Angriff. Siehe:
echo -e "..../\0/erbsen/../linsen..bohnen/bulgur" | tr -d '&%$\\' | sed 's|.\.\/||g;s|\.\./||g' # WARNUNG! ../erbsen/linsen..bohnen/bulgur
Und schwub ist man aus dem Pfad ausgebrochen!
Um es zu reparieren, muss du das Nullbyte auch zwingend herausfiltern!
tr -d '\0&%$\\' | sed 's|.\.\/||g;s|\.\./||g'Siehe an unserem Beispiel
echo -e "..../\0/erbsen/../linsen..bohnen/bulgur" | tr -d '\0&%$\\' | sed 's|.\.\/||g;s|\.\./||g' erbsen/linsen..bohnen/bulgur
Wir können das auch ohne tr, direkt in sed schreiben und sparen dabei ein byte und haben dann wirklich wieder einen sed einzeiler. Die obige tr Variante ist aber besser zu lesen und zu verstehen.
sed 's/[\x00&%$\\]//g;s|.\.\/||g;s|\.\./||g' # sed-einzeiler
Noch der Beweis, das auch dieser funktioniert...
echo -e "..../\0/erbsen/../linsen..bohnen/bulgur" | sed 's/[\x00&%$\\]//g;s|.\.\/||g;s|\.\./||g' erbsen/linsen..bohnen/bulgur
Und beachte auch, dass diese Variante dann nicht für backslash gilt.
Du kannst damit also keine Pfade mit Backslash erlauben! Wenn wir das obige jetzt aber weiterverwenden und auch Backslash \ erlauben würden, weil wir auch die Windowswelt oder SMB UNC Pfade versorgen müssten, schaffen wir ein ernstes weiteres Problem!
Unabhängig davon, dass das Konstrukt jetzt sehr hässlich und sperrig wird...
tr -d '\0&%$' | sed "s|.\.\/||g;s|\.\./||g;s|\.\.\\\\||g;s|\.\.\\\\||g" # WARNUNG!
...bleibt es gefährlich, weil du jetzt auch "\" zulässt, was in der anschließenden Pipe für eine Zeicheninjection genutzt werden könnte und dort u.U. wieder mit Nullbyte und anderen Zeichenfolgen hantiert werden kann um den Pfad zu sprengen.
echo -e "..../\0/erbsen/../linsen..bohnen/bulgur" | tr -d '\0&%$' | sed "s|.\.\/||g;s|\.\./||g;s|\.\.\\\\||g;s|\.\.\\\\||g" # WARNUNG! erbsen/linsen..bohnen/bulgur
Sieht gut aus? Nein.
echo "\\\056\\\056/erbsen/../linsen..bohnen/bulgur" | tr -d '\0&%$' | sed "s|.\.\/||g;s|\.\./||g;s|\.\.\\\\||g;s|\.\.\\\\||g" | xargs echo -e # WARNUNG! ../erbsen/linsen..bohnen/bulgur
Wieder gesprengt und ohne Nullbyte einsetzen. Diesmal weil wir ja im Gegensatz zur vorherigen Restriktionen nun auch einen Backslash erlaubt haben. Und mit Backslash können wir den Punkt auch anders übergeben. Unsere Injection läuft also problemlos durch die ganze tr und sed durch und wird dann erst später im xargs (oder anderen scripten) irgendwann zum Problem!
Das zeigt, dass eine Sicherheitsanalyse über alle Teile der Kette erfolgen muss.
Daher ist generell der umgekehrte Weg oft ratsamer: Alles herausfiltern außer wohl definierte saubere Zeichen.
Daher: Baue deine Applikationen daher möglichst immer so, dass du alle Zeichen wegwirfts, so wie ganz zu Beginn beschrieben.
Denn dann fliegen auch \ und & und Nullbyte etc. Alle gleich mit raus. Und wisse über die gefährlichen Zeichen.
Encoding und Double Encoding sind hier ein Problem. z.B. wenn deine Daten mit URLs übergeben werden. Siehe diese Beispiele:
%2e%2e%2f steht für ../
%2e%2e/ steht für ../
..%2f steht für ../
%2e%2e%5c steht für ..\
%2e%2e\ steht für ..\
..%5c steht für ..\
%252e%252e%255c steht für ..\
..%255c steht für ..\
Du könntest also die hexadezimale ASCII Codierung des Zeichens Slash oder Punkt verwenden und damit deinen Replace austricksen. Damit das nicht mehr geht, werfen wir das % Zeichen weg, was damit auch kein Bestandteil eines Ordner- oder Dateinamens sein kann. Beachte auch, dass je nach Implementierung der Backslash \ verwendet werden kann, um die Octal oder x\ um die Hexadezimalnotation zu übergeben. Der Backslash ist hier also auch ein potentiell gefährliches Zeichen.
Auch URL-Encoding (Prozent-Encoding) des Slashes oder Backslashes ist ein Problem. Es wird z.B. beim URL Encoding verwendet und daher u.a. auch in der Webentwicklung relevant:
..%c0%af steht für ../
..%c1%9c steht für ..\
Frage: Warum muss ich \ herausfiltern? Linux nutzt doch / für Pfade?
Antwort: Weil unter linux mit \ ja Oktal oder Hexadezimalcodes für beliebige Zeichen übergeben werden können. Hier konkret:
\057 steht für /
\056 steht für .
\x2f steht für /
\x2e steht für .
Angreifer versuchen daher auch beispielweise so etwas:
..\057..\057etc\057passwd
was dann zu dem Pfad
../../etc/passwdwerden würde. Was es zu verhindern gilt.
Beachte auch, dass insbesondere sed den backslash standardmäßig auswertet, wohingegen man echo noch ein -e flag mitgeben müsste. Hier ein Beispiel:
echo abc | sed "s,a,\x2f,g" /bc
Ampersand wird zum einen gern für Shell Backgroundtasks verwendet, aber das ist hier nicht der Hauptgrund.
Falls die Eingabe aus einer Weboberfläche kommt, mit HTML oder XML Entity Encoding versehen ist, kann man den Slash und den Punkt auch damit codieren. Je nach Kontext und wie dies weiter interpretiert wird, kann das dann Probleme bereiten.
& #47; steht für /
& #46; steht für .
& #x2f; steht für /
& #x2e; steht für .