Geschrieben von: Christoph Wille
Kategorie: Sicherheit
This printed page brought to you by AlphaSierraPapa
Eigentlich dürfte jedem das FileSystemObject ein Begriff sein - mit diesem kann man Dateien auslesen und schreiben, sowie einige wichtige Operationen (löschen zB) auf Dateien und Verzeichnissen ausführen. Allerdings ist kaum bekannt, was Windows (NT/2000) so alles unter Dateien versteht - nämlich keineswegs nur Dateien die auf der Festplatte liegen.
Unter Windows werden neben Dateien die auf der Festplatte liegen auch Devices (COM oder LPT Ports), Konsolen (CON oder AUX) und auch Named Pipes (zB verwendet SQL Server solche) verstanden. Die Frage stellt sich nun, wieso soll mich das ASP Programmierer interessieren? Nehmen wir folgendes harmlos aussehendes Beispiel (readfile.asp):
<% Const ForReading = 1, ForWriting = 2, ForAppending = 8 Set fso = Server.CreateObject("Scripting.FileSystemObject") Set f = fso.OpenTextFile("c:\temp\COM1", ForReading, True) strRetVal = f.ReadLine f.Close Response.Write strRetVal %>
Das mit c:\temp\COM1 ist kein schlechter Scherz von mir, das ist so perfekt gültig unter Windows, da man auf diese Art Devices von der Konsole aus ansprechen kann (das Verzeichnis oder Laufwerk ist übrigens frei wählbar, um es mal so zu formulieren...). Nur was passiert? Sobald ich mich auf diese Device hinverbinde, bleibt der Thread auf dem die ASP Seite läuft hängen, bis ein Timeout vom Port zurückkommt. Da ASP aus einem Threadpool bedient wird, kann ich damit elegant eine DoS (Denial of Service) Attacke gegen einen Server fahren - wenn er verwundbar ist.
Und verwundbar ist man schnell - sicher fällt dem einen oder anderen aus dem Stand eine Website ein, bei der im QueryString Dateinamen mitgegeben werden, um ausgelesen in den Content eingebaut zu werden.
Hinweis Das mit der Datei im QueryString ist gefährlicherweise nicht unüblich, mir ist das schon des öfteren untergekommen. Nur gibt es weitere Angriffspunkte: Sites die beim Upload den User den Dateinamen wählen lassen, Programme die aus Userinput Dateien (und deren Namen) generieren, und so einiges mehr. Die Gefahr ist nicht aus Luft gegriffen, schauen Sie sich einmal Ihren Code genauer an!
Wer jetzt argumentiert daß er doch überprüft ob die Datei existiert, soll sich folgendes Script (fileexists.asp) anschauen:
<% filespec = "c:\temp\COM1" Set fso = Server.CreateObject("Scripting.FileSystemObject") If (fso.FileExists(filespec)) Then msg = filespec & " exists." Else msg = filespec & " doesn't exist." End If Response.Write msg %>
Nur leider - da COM1 eine gültige Datei ist - wird hier für FileExists True zurückgeliefert. Und dann greift man im guten Glauben darauf zu, und schon steht der Webserver!
Nach diesem ersten Aufwärmen worum es hier geht, möchte ich eine Liste von reservierten Wörtern für Devices nachschicken, mit denen man diesen Effekt hervorrufen kann:
In jedem beliebigen Verzeichnis sind diese reservierten Wörter für "echte" Dateinamen ungültig, allerdings per File API's von Windows können sie angesprochen werden. Und um der Sache das Sahnehäubchen aufzusetzen, das folgende geht auch noch:
Response.Write fso.FileExists("c:\temp\NUL.txt") ' liefert True
Wobei die Dateierweiterung völlig beliebig sein darf...
Es gibt einige Möglichkeiten, den übergebenen oder einfach nur zu verwendenden Dateinamen auf Gültigkeit zu überprüfen:
Letztere Version hat einen großen Vorteil - sie schützt uns vor Attacken gegen Named Pipes als auch neu hinzukommende reservierte Wörter (so dies passieren sollte). Aber den grundsätzlichen Check des Dateinamens sollte man auf alle Fälle durchführen. Je genauer wir schauen, desto eher fällt uns nichts durch die Ritzen.
Daß FileExists vom FileSystemObject uns nicht hilft, wissen wir bereits. Und leider bietet uns das FileSystemObject keine Funktion an, den Dateityp herauszubekommen. Deshalb habe ich mich entschlossen, in C++ mittels ATL eine kleine Komponente zu schreiben (Sourcecode im Download des Artikels mit dabei), die den Dateityp herausbekommen kann.
Bevor ich den Code der Komponente SecurityEnhance.FileUtilities zeige, denke ich mir, daß mehr Leser daran interessiert sind zu sehen, wie man mit dieser Komponente arbeitet. Deshalb habe ich ein nettes Testscript gezimmert, das viele verschiedene Dateien durchtestet (filetypecheck.asp):
<% Option Explicit Const RET_FILE_TYPE_UNKNOWN = 1 Const RET_FILE_TYPE_DISK = 2 Const RET_FILE_TYPE_CHAR = 3 Const RET_FILE_TYPE_PIPE = 4 Sub FileCheck(ByVal strFile) Dim objSecurityCheck, nFileType, nErrorCode, strError Set objSecurityCheck = Server.CreateObject("SecurityEnhance.FileUtilities") On Error Resume Next nFileType = objSecurityCheck.GetFileType(strFile) nErrorCode = Err.Number strError = Err.Description On Error GoTo 0 ' re-enabling error handling clear the Err object If (0 = nErrorCode) Then Response.Write strFile & ": is of type " & nFileType Else Response.Write strError End If Response.Write "<br>" & vbCrLf End Sub FileCheck "c:\temp\thisfiledoesnotexist.txt" ' above will return error 2, "The system cannot find the file specified. " FileCheck "c:\temp\COM1" ' above will return type 3 (character file) typically an LPT device or a console. FileCheck "c:\boot.ini" ' above returns error 5, "Access is denied." (hopefully) FileCheck "c:\view.txt" ' above returns type 2, disk file (if file exists of course) FileCheck "c:\COM1.txt" ' also returns type 3 - attention! FileCheck "c:\COM1somemoretext" ' this does not work (2: file not found) FileCheck "c:\somefile.COM1" ' as well as this (2: file not found) %>
Hinweis Obwohl ich die wichtigeren Win32 Fehlernummern im Sourcecode kommentiert habe, könnte es sein, daß andere auftreten. Um von der Fehlernummer zum Beschreibungstext zu kommen, geben Sie auf der Kommandozeile einfach net helpmsg nnnn ein, wobei nnnn die Fehlernummer ist.
Der Output - obwohl durch Kommentare im Script bereits vorweggenommen, sieht so aus:
Im Prinzip gilt für die Methode GetFileType folgendes: alles an Typ anders als 2 (RET_FILE_TYPE_DISK) wird nicht einmal mit spitzen Fingern angegriffen - solche "Dateien" sind aus Sicherheitsgründen als "off limits" zu betrachten. Wenn man das beherzigt, kann einem niemand ein "Datei" unterschieben, die den Server crasht.
Bevor wir nun zum Code der Komponente kommen, und wie diese an den Dateityp kommt - die Komponente ist an sich nur auf Lesezugriff für Dateien ausgelegt (die Datei muß existieren). Allerdings ist dies nicht weiter schlimm, weil Typen anders als 2 (RET_FILE_TYPE_DISK) sich für Lesen und Schreiben gleich verhalten: kommt der Typ ohne Fehler retour und ist ungleich 2, dann wird die Datei nicht geschrieben.
Um es also noch einmal deutlich zu machen: egal ob wir in eine Datei schreiben oder von ihr lesen wollen, wenn der Typ von GetFileType ungleich 2 ist, passiert die Operation aus Sicherheitsgründen nicht.
Als Abschluß für sehr Interessierte (die anderen dürfen zur Schlußbemerkung verzweigen), nun der relevante Code der Komponente. Obwohl für viele C++ ungewohnt sein mag, so kann ich versichern, daß es keineswegs schwierig ist (aus FileUtilities.cpp):
#define E_FILEERROR MAKE_HRESULT(1,FACILITY_ITF,1) #define RET_FILE_TYPE_UNKNOWN 1 #define RET_FILE_TYPE_DISK 2 #define RET_FILE_TYPE_CHAR 3 #define RET_FILE_TYPE_PIPE 4 STDMETHODIMP CFileUtilities::GetFileType(BSTR FileName, long *FileType) { USES_CONVERSION; *FileType = -1; // open the file for generic reading HANDLE hFile = ::CreateFile(W2A(FileName), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL ); if (INVALID_HANDLE_VALUE == hFile) { TCHAR achErrFormatting[256]; wsprintf(achErrFormatting, "The file %s could not be accessed. The Win32 error code is %ld.", W2A(FileName), ::GetLastError()); Error(achErrFormatting, 0, NULL, IID_IFileUtilities, E_FILEERROR); return E_FILEERROR; } else { DWORD dwFileType = ::GetFileType(hFile); switch (dwFileType) { case FILE_TYPE_UNKNOWN: *FileType = RET_FILE_TYPE_UNKNOWN; break; case FILE_TYPE_DISK: *FileType = RET_FILE_TYPE_DISK; break; case FILE_TYPE_CHAR: *FileType = RET_FILE_TYPE_CHAR; break; case FILE_TYPE_PIPE: *FileType = RET_FILE_TYPE_PIPE; break; default: *FileType = -1; } ::CloseHandle(hFile); } return S_OK; }
Die zwei wichtigen Funktionen CreateFile und GetFileType habe ich herausgehoben. Diese beiden erledigen die Arbeit, der Rest ist nur Beiwerk daß die Fehlerfälle korrekt behandelt werden, und alles fein säuberlich aufgeräumt wird. Es wäre nett gewesen, hätte Microsoft an eine solche Funktion beim FileSystemObject gedacht.
Der heutige Artikel beweist wieder einmal auf sehr eindrückliche Weise, daß aller Input den man in seinen Applikationen übernimmt sehr gefährlich sein kann ("All input is evil until proven otherwise", wie schon in der Artikelserie über SQL Injection bemerkt). Auf alle Fälle sollte man die einfache Überprüfung in seine Scripts einbauen, besser und sicherer ist allerdings die Verwendung der Komponente.
This printed page brought to you by AlphaSierraPapa
Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20020131.zip
Dateityp-Ermittlung in Managed C++
http:/www.aspheute.com/artikel/20020201.htm
Gegengifte für SQL Injection
http:/www.aspheute.com/artikel/20011031.htm
Komponentenverwendung einschränken
http:/www.aspheute.com/artikel/20020129.htm
SQL Injection
http:/www.aspheute.com/artikel/20011030.htm
Verhinderung von SQL Injection Marke .NET
http:/www.aspheute.com/artikel/20011203.htm
©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.