Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/03491.jsonl.gz/13

MITRE ATT&CK
Marc Ruef
In den ersten Teilen dieser Serie haben wir die grundlegende Funktionsweise des NSE-Skripting kennengelernt: Wir haben die allgemeine Funktionsweise von NSE illustriert, ein erstes derivatives Plugin geschrieben und dieses um die Analyse der Version Info erweitert. Im vierten Teil haben wir erstmals besprochen, wie sich eigene Netzwerkanfragen generieren lassen, um zusätzliche Informationen einholen und Tests durchführen zu können.
Im fünften Teil wollen wir uns nun auf die Funktionen bezüglich HTTP-Kommunikationen fokussieren. NSE stellt mit
http eine entsprechende Bibliothek für das genannte Anwendungsprotokoll zur Verfügung. Sodann können mit verschiedenen Methoden HTTP-Anfragen sehr einfach durchgeführt und die jeweiligen Rückantworten direkt dissektiert werden.
Die wohl zentralste Methode lautet
http.get(). Sie wird eingesetzt, um reguläre HTTP GET-Anfragen an den Zielport eines Zielsystems durchführen zu lassen. Dadurch können Zugriffe auf freigegebene Ressourcen umgesetzt werden. Diese Methode erwartet mindestens drei Argumente: Die IP-Adresse oder den Hostnamen des Zielsystems, die nummerische Darstellung des Zielports und die aufzurufende Ressource.
local response = http.get(host, port, "/")
Die Rückgabe dieser Methode sind die jeweiligen Elemente der durch die Anfrage provozierten HTTP-Rückantwort. So wird durch
response.rawheader der Zugriff auf die einzelnen HTTP-Header-Zeilen möglich und mit
response.body kann auf den Body zugegriffen werden.
Das Bereitstellen des Rawheaders erfolgt als Table, wobei eine jede Zeile ein Element darstellt. Dass erstmalig auf diese Datenstruktur zurückgegriffen wird, erschliesst einen zentralen Vorteil: Dadurch kann nämlich separat auf die einzelnen Zeilen zugegriffen werden, ohne sich zuerst um ein Splitting der Einträge kümmern zu müssen (das könnte durch
myrawheader = stdnse.strsplit("\r?\n", header) bewerkstelligt werden). Durch
response.rawheader[1] wird beispielsweise auf das erste Element in der Table, also die erste Header-Zeile, zugegriffen.
Soll zum Beispiel geprüft werden, ob die die Header-Zeile
Server existiert, kann dies mit einer einfachen
for-Schleife realisiert werden:
for i=1, #response, 1 do if string.find(response.rawheader[i], "^Server: ") then return true end end
Eine
for-Schleife erwartet, wie bei den meisten Programmiersprachen üblich, drei Argumente. Zuerst findet eine Initialisierung der Zählervariablen statt. In diesem Fall wird
i=1 verwendet. Das Zählen der Elemente in einer Table beginnt in Lua immer mit 1 und nicht mit 0, wie bei vielen anderen höheren Programmiersprachen üblich (z.B. C, Java, PHP). Als zweites Argument wird der Ausdruck definiert, der zu einem Beenden des Durchlaufens der Schleife führt. In diesem Fall wird
#response verwendet, wodurch die Anzahl der Elemente in der Table, sie alle also durchgegangen, angegeben werden. Und als drittes erfolgt optional das Stepping, also die Definition des Hochzählens. Üblicherweise wird die Zählervariable, so wie hier auch, um
1 inkrementiert.
Bei jedem Durchlauf wird nun geprüft, ob die jeweils eingelesene Zeile mit der Zeichenkette
Server beginnt. Hierzu kommt die Methode
string.find() zum Tragen. Sie erwartet minimal zwei Argumente. Das erste Argument bringt den Haystack, der durchsucht werden soll. Hier werden ausschliesslich Strings erwartet. Und das zweite Argument definiert das Pattern, welches gesucht werden soll. Für das Pattern können reguläre Ausdrücke, in diesem Fall wird das Sonderzeichen
^ zur Definition des Beginns einer Zeile genutzt, eingesetzt werden.
In ähnlicher Weise kann verfahren werden, wenn nicht nur die Existenz einer Header-Zeile determiniert werden will, sondern wenn auch gleich der Wert einer solchen extrahiert werden soll. Stattdessen benutzen wir nun einfach
string.match(), das vom Prinzip her ähnlich wie
string.find() funktioniert. Durch den regulären Ausdruck können wir nun den Wert nach dem Zeilennamen extrahieren und zurückgeben (Zum reinen Suchen ist
string.find() jedoch immer schneller als
string.match()). Verhältnismässig unkompliziert kann damit nun die Ankündigung des Webservers extrahiert werden:
for i=1, #response, 1 do servermatch = string.match(haystack[i], “^Server: (.*)”) if type(servermatch) == “string” and servermatch ~= “” then return servermatch end end
Die Rawheader liefern jedoch nicht alle reinen Zeilen des HTTP-Headers zurück. Die Statuszeile fehlt. Auf diese kann als ganzes mit
response['status-line'] zurückgegriffen werden. Sie enthält das unterstützte Protokoll, die Protokollversion, den dreistelligen Statuscode und den Statustext (z.B.
HTTP/1.0 404 Not Found). Auf den Statuscode kann unkompliziert mit
response.status zugegriffen werden.
Der Zugriff auf den Body einer HTTP-Rückantwort gestaltet sich ein bisschen einfacher, da dieser üblicherweise in
response.body als reiner String zurückgeliefert wird. Doch auch hier lassen sich die gleichen Weiterverarbeitungen, zum Beispiel das Finden von Zeichenketten, applizieren. Dies kann zum Beispiel für das Identifizieren von Standardinstallationen von Webapplikationen genutzt werden (die Anzahl der Pattern ist der Übersichtlichkeit halber stark reduziert worden):
response = http.get(host, port, “/”)
if type(response.body) == “string” and response.body ~= “” then local mt = { {pattern=“Test Page for Apache Installation”, product=“Apache httpd”}, {pattern=“NT 4.0 Option Pack provides”, product=“MS IIS 4.0”}, {pattern=“you can start deploying your J2EE”, product=“Oracle”} }local result = “” for i=1, #mt, 1 do if string.find(response.body, mt[i].pattern) then resultdata = “Pattern:\t” .. mt[i].pattern .. “\n” .. Product:\t” .. mt[i].product if result nil or result “” then result = resultdata else result = result .. “\n\n” .. resultdata end end end return result end
Verschiedene Eigenschaften von HTTP als Anwendungsprotokoll erschweren jedoch den Umgang. Zum Beispiel gibt es verschiedene Statuscodes, die von einem Webserver zurückgeliefert werden, um die neuerliche Lokation einer Ressource mitzuteilen. Webadministratoren sind durch solche Redirects darum bemüht, dass auch alte Links noch zu den gewünschten Ressourcen führen. In den Spezifikationen von HTTP sind vorgesehen, für Umleitungen die Statuscodes im Bereich
3xx zu verwenden (RFC 1945, Absatz 9.3).
Im Rahmen eines Scans kann es erforderlich sein, dass solche Redirects berücksichtigt und ihnen gefolgt wird. Das verwundbare Webforum findet sich nicht mehr unter
/forum.php, sondern neu unter
/newforum.asp gesucht werden. Ein Server schickt dann in der Zeile
Location, sie wird standardmässig in
response.header.location abgelegt, die neue URL der Ressource zurück. Ein Skript muss also derartige HTTP-Redirects erkennen und diesen folgen können. Dies kann und darf jedoch nicht ohne zusätzliche Prüfung erfolgen. Denn so ist es durchaus möglich, dass ein Server mit der IP-Adresse
<ip-pii> in einem Redirect auf einen anderen Server mit der IP-Adresse
<ip-pii> verweist. Da bei professionellen Vulnerability Scans die Zielsystems oftmals klar definiert und Zugriffe ausserhalb dieser Spezifikation unerwünscht sind, muss das Resultat der
Location-Zeile vor einem weiterführenden Zugriff validiert werden (jenachdem gilt es den Zielport auch nicht zu verlassen).
Selbst bietet nmap keine Funktion an, um diese Aufgabe komfortabel wahrnehmen zu können. Auch in der Library für HTTP findet sich kein direkt nutzbarer Code. Die bisher mit nmap ausgelieferten HTTP-Skripte nutzen jedoch eigene Implementierungen, um diese Hürden angehen zu können. Hierbei kommen relativ simple Prüfungen zum Zug, bei denen die
Location auf Plausibilität hin untersucht wird. Nur wenn diese sich auf dem gleichen Zielsystem befindet, wie das gegenwärtigen Systems, wird ein weiterer HTTP-Zugriff durchgeführt. Längerfristig wäre es von Vorteil, wenn diese Funktionalität Einzug in die
http-Bibliothek halten würde. Dadurch könnte auf eine schwierig zu verwaltende dezentrale Implementierung verzichtet werden.
Dennoch werden einige weitere nützliche Funktionen im Umgang mit HTTP-Servern angeboten. Durch
http.page_exists() kann beispielsweise komfortabel und genau überprüft werden, ob eine Seite existiert. Damit lassen sich entsprechend sehr einfach Checks im Sinn eines klassischen CGI-Scanners (z.B. Nikto) implementieren. Oder mit
http.clean_404() kann eine dynamische 404 Not Found-Fehlerseite so angepasst werden, dass nur noch die statischen Inhalte vorhanden sind. Eine klare Unterscheidung zwischen Fehlermeldungen wird so mit einfachem Pattern-Matching (ohne komplexe reguläre Ausdrücke) möglich.
Im nächsten Teil werden wir einige weitere Funktionen von Lua kennenlernen. Diese werden dabei helfen, ein eigenes Version Detection, gänzlich unabhängig von der gleichnamigen Funktionalität in nmap, umzusetzen. Wir werden dabei die Analyse des HTTP-Headers eines Webservers vornehmen, um damit ein HTTP-Fingerprinting umzusetzen. Dies ist zugleich die Grundfunktionalität, die wir in der angekündigten NSE-Portierung von httprecon erreichen wollen.
Unsere Spezialisten kontaktieren Sie gern!
Marc Ruef
Marc Ruef
Marc Ruef
Unsere Spezialisten kontaktieren Sie gern!
Weitere Artikel im Archiv