Nmap NSE Hacking, Teil 5: HTTP-Kommunikationen

Nmap NSE Hacking, Teil 5

HTTP-Kommunikationen

Marc Ruef
von Marc Ruef
Lesezeit: 10 Minuten

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 192.168.0.10 in einem Redirect auf einen anderen Server mit der IP-Adresse 192.168.0.11 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.

Über den Autor

Marc Ruef

Marc Ruef ist seit Ende der 1990er Jahre im Cybersecurity-Bereich aktiv. Er hat vor allem im deutschsprachigen Raum aufgrund der Vielzahl durch ihn veröffentlichten Fachpublikationen und Bücher – dazu gehört besonders Die Kunst des Penetration Testing – Bekanntheit erlangt. Er ist Dozent an verschiedenen Fakultäten, darunter ETH, HWZ, HSLU und IKF. (ORCID 0000-0002-1328-6357)

Links

Sie wollen mehr als einen simplen Security Test mit Nessus und Nmap?

Unsere Spezialisten kontaktieren Sie gern!

×
Konkrete Kritik an CVSS4

Konkrete Kritik an CVSS4

Marc Ruef

scip Cybersecurity Forecast

scip Cybersecurity Forecast

Marc Ruef

Voice Authentisierung

Voice Authentisierung

Marc Ruef

Bug-Bounty

Bug-Bounty

Marc Ruef

Sie wollen mehr?

Weitere Artikel im Archiv

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

Sie wollen mehr?

Weitere Artikel im Archiv