Security Testing
Tomaso Vasella
So funktioniert Webscraping mit Powershell
Obwohl Websites inzwischen vermehrt APIs anbieten und auch Webapplikationen selbst zunehmend solche verwenden, sind die meisten Webseiten für den menschlichen Gebrauch formatiert. Technisch gesehen sind die Inhaltsdaten, also beispielsweise der Text eines Artikels, mit Steuerdaten, Metadaten, Formatierungsangaben, Bildern und weiteren Daten vermischt. Diese Daten sind zwar notwendig für die Funktionalität und die Darstellung einer Website; aus Sicht der Datenextraktion sind sie aber eher störend. Es braucht also geeignete Werkzeuge, um aus diesem Datengemisch die gewünschten Informationen herauszuschälen.
Gründe für solche Datenextraktionen gibt es viele. So kann man sich auf diese Art etwa selbst einen RSS-Feed aus dem Inhalt einer Website zusammenstellen oder es kann gewünscht sein, öffentlich zugängliche Informationen im Rahmen eines Red Team Engagements zu sammeln. Obwohl Webscraping nach etwas recht Einfachem klingt, kann sich das in der Praxis als erstaunlich mühsam erweisen. Powershell mag für diese Aufgabe vielleicht nicht unbedingt die erste Wahl sein, aber aufgrund seiner Verbreitung lohnt es sich, die entsprechenden Möglichkeiten zu kennen. Es kommt nicht selten vor, dass derartige Aufgaben in stark eingeschränkten Umgebungen durchgeführt werden müssen und Alternativen wie Python oder Perl nicht zur Verfügung stehen.
Die nachfolgende Tabelle gibt eine Übersicht über die gebräuchlichsten Methoden, um mittels Powershell HTTP-Anfragen durchzuführen.
Methode | Technologie | PS Version | Bemerkungen |
---|---|---|---|
Invoke-Webrequest | Powershell cmdlet | Ab Version 3 | Führt umfangreiches Parsing durch, welches jedoch ab Powershell Version 7 nicht mehr erfolgt, da in dieser Version das HTML DOM des Internet Explorers nicht mehr verfügbar ist. |
System.Net.WebClient | .NET | Ab Version 2 | Gemäss Microsoft: Es wird nicht empfohlen, die WebClient Klasse für die neue Entwicklung zu verwenden. Verwenden Sie stattdessen die System.Net.Http.HttpClient Klasse. |
System.Net.HTTPWebrequest | .NET | Ab Version 2 | Gemäss Microsoft: Es wird nicht empfohlen, die HTTPWebrequest Klasse für die neue Entwicklung zu verwenden. Verwenden Sie stattdessen die System.Net.Http.HttpClient Klasse. |
System.Net.Http.HttpClient | .NET | Ab Version 3 |
Neben den beschriebenen Verfahren existieren noch diverse weitere Möglichkeiten, z.B. kann mittels Powershell auch ein Internet Explorer COM Automation Objekt instantiiert werden oder es können andere COM-Objekte wie Msxml2.XMLHTTP verwendet werden.
$ie = new-object -com "InternetExplorer.Application" $ie.navigate('https://www.example.com') $ie.document
Dieses Cmdlet ist das wahrscheinlich am häufigsten angewendete Verfahren, um mit Powershell Daten von einer Website abzurufen:
$res = Invoke-WebRequest -Uri 'http://www.example.com' $res = Invoke-WebRequest -Uri 'http://www.example.com' -Outfile index.html $res = Invoke-WebRequest -Uri 'http://www.example.com' -Method POST -Body "postdata"
Im Unterschied zu den weiteren vorgestellten Methoden wird hier automatisch ein User-Agent-Header gesetzt. Es ist möglich, mit dem Parameter -UserAgent
darauf Einfluss zu nehmen. Zusätzlich existiert der Parameter -Headers
, welcher aber das Setzen gewisser Header wie User-Agent
nicht zulässt. Die volle Kontrolle über die Header lässt sich nur durch die Verwendung der weiter unten beschriebenen .NET Klassen erreichen.
Sofern kein Fehler aufgetreten ist, wird ein Objekt des Typs Microsoft.PowerShell.Commands.HtmlWebResponseObject
zurück geliefert.
Je nach verwendeter Version von Powershell unterscheiden sich die Eigenschaften dieses Objekts: In Version 5 wird der vom Webserver gelieferte Inhalt parsed und steht unter dem Property .ParsedHtml
zur Verfügung. Dieses Property fehlt unter Powershell 7.
Das automatische Parsing kann für schnelles Webscraping sehr nützlich sein, beispielsweise lassen sich mit einem simplen Aufruf alle Links einer Webseite extrahieren:
(Invoke-WebRequest -Uri 'https://www.example.com').Links.Href
Der gesamte Inhalt der gelieferten Seite befindet sich im Property .Content
des Ergebnisobjekts; falls auch die Header gewünscht sind, stehen diese in der Eigenschaft .RawContent
zur Verfügung.
Sollte ein Fehler auftreten, wird kein Objekt zurückgegeben. Man kann jedoch die in der Exception enthaltene Fehlermeldung auslesen:
try { $res = Invoke-WebRequest -Uri 'http://getstatuscode.com/401' } catch [System.Net.WebException] { $ex = $_.Exception } $msg = $ex.Message $status = $ex.Response.StatusCode.value__
Handelt es sich um einen Fehler, bei dem der Webserver einen Inhalt geliefert hat, beispielsweise eine Webseite bei einem 401-Fehler, so kann man auf diesen Inhalt wie folgt über die Exception zugreifen (zur Illustration ist auch der Zugriff auf die Response Header gezeigt):
try { $res = Invoke-WebRequest -Uri 'http://getstatuscode.com/401' } catch { $rs = $_.Exception.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($rs) $content = $reader.ReadToEnd() foreach($header in $rs.Response.Headers.AllKeys) { write-host $('{0}: {1}' -f $header, $rs.Response.Headers[$header]) } }
Beim Abschicken mehrerer Requests, z.B. automatisiert in einem Loop, fällt auf, dass Invoke-Webrequest
sehr langsam sein kann. Dies hängt einerseits mit dem standardmässig aktivierten umfangreichen parsing der Response zusammen, andererseits wird oft für jeden Request ein Fortschrittsbalken angezeigt.
Man kann mit folgenden Parametern bzw. Einstellungen beschleunigen:
-UseBasicParsing
erfolgt eine etwas weniger tiefgehende Verarbeitung der Serverantwort, die Properties Content
, RAWContent
, Links
, Images
und Headers
werden aber trotzdem befüllt.$ProgressPreference = 'SilentlyContinue'
(siehe dazu auch den Artikel von Microsoft).Invoke-Webrequest
folgt standardmässig automatisch bis zu 5 Umleitungen (Redirects). Dieses Verhalten lässt sich mit dem Parameter -MaximumRedirection
steuern.
Invoke-Webrequest
verwendet standardmässig den in den Windows-Einstellungen definierten Proxy. Dies kann mit dem Parameter -Proxy
, der als Argument einen URI annimmt, übersteuert werden. Falls der Proxy eine Authentisierung verlangt, können die nötigen Credentials mit dem Parameter -ProxyCredential
angegeben werden, wobei ein Argument vom Typ PSCredential erforderlich ist. Ein Beispiel würde etwa so aussehen:
$secPw = ConvertTo-SecureString '************' -AsPlainText -Force $creds = New-Object System.Management.Automation.PSCredential -ArgumentList 'username', $secPw $res = Invoke-WebRequest -Uri 'https://www.example.com' -Proxy 'http://127.0.0.1:8080' -ProxyCredential $creds
Alternativ dazu kann der Parameter -ProxyUseDefaultCredentials
spezifiziert werden, wodurch die Credentials des aktuellen Benutzers verwendet werden.
Invoke-Webrequest
unterstützt die Verwendung von Cookies und ermöglicht damit auch entsprechende Sessions durch Angabe des Parameters -SessionVariable
. Die entsprechende Session kann anschliessend in nachfolgenden Requests mit dem Parameter -WebSession
verwendet werden.
Invoke-WebRequest -SessionVariable -Uri 'https://www.google.com' Invoke-WebRequest -WebSession $Session -Uri 'https://www.google.com'
Die Cookies stehen mit dem Property .Cookies
zur Verfügung und es können auch selbst Cookies definiert werden. In diesem Beispiel wird auch das Session-Objekt vorgängig erstellt, was nützlich sein kann, um bereits vor dem ersten Request eigene Cookies zu setzen:
$cookie = New-Object System.Net.Cookie $cookie.Name = "specialCookie" $cookie.Value = "value" $cookie.Domain = "domain" $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession $session.Cookies.Add($cookie) Invoke-WebRequest -WebSession $session -Uri 'https://www.example.com'
Der Zugriff auf Websites ist auch hiermit recht einfach, wie folgende Beispiele zeigen:
$wc = New-Object System.Net.WebClient $res = $wc.DownloadString('https://www.example.com')
Im Unterschied zu Invoke-Webrequest
werden hier keine Header automatisch gesetzt. Man kann diese aber selbst angeben, um beispielsweise den User-Agent festzulegen. Dies ist häufig ratsam, da sich Webseiten manchmal anders verhalten, wenn der User-Agent fehlt.
$wc.Headers.Add('UserAgent', 'Mozilla/5.0 (Windows NT; Windows NT 10.0; de-CH)')
Falls ein Fehler auftritt, so wird auch hier ein allfällig vom Werserver gelieferter Inhalt nicht im Ergebnisobjekt verfügbar gemacht. Um darauf zuzugreifen, kann man ein analoges Verfahren wie oben beschrieben anwenden, was allerdings mit Powershell 2 nicht funktioniert, da diese Version try-catch gar nicht unterstützt.
try { $res = $wc.DownloadString('http://getstatuscode.com/401') } catch [System.Net.WebException] { $rs = $_.Exception.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($rs) $content = $reader.ReadToEnd() }
Auch dieses Verfahren verwendet standardmässig den in den Windows-Einstellungen definierten Proxy, den sich mit [System.Net.WebProxy]::GetDefaultProxy()
abrufen lässt. Man dies ebenfalls übersteuern:
$proxy = New-Object System.Net.WebProxy('http://127.0.0.1:8080') $proxyCreds = New-Object Net.NetworkCredential('username', '************') $wc = New-Object System.Net.WebClient $wc.Proxy = $proxy $wc.Proxy.Credentials = $proxyCreds
Oder alternativ, wenn man die Default-Credentials verwenden will:
$wc.UseDefaultCredentials = $true $wc.Proxy.Credentials = $wc.Credentials
System.Net.WebClient
stellt keine komfortablen Methoden für Cookies zur Verfügung. Somit muss man die entsprechenden Response-Header selbst auslesen und verarbeiten, was rasch ziemlich umständlich werden kann.
$cookie = $wc.ResponseHeaders["Set-Cookie"]
Auch dieses Verfahren ist relativ einfach umzusetzen und folgt den gleichen Prinzipien wie die bereits genannten.
$wr = [System.Net.HttpWebRequest]::Create('http://www.example.com') try { $res = $wr.GetResponse() } catch [System.Net.WebException] { $ex = $_.Exception } $rs = $res.GetResponseStream() $rsReader = New-Object System.IO.StreamReader $rs $data = $rsReader.ReadToEnd()
Auch POST-Requests sind möglich:
$data = [byte[]][char[]]'postdatastring' $wr = [System.Net.HttpWebRequest]::Create('http://ptsv2.com/t/g1eiu-1612179889/post') $wr.Method = 'POST' $requestStream = $wr.GetRequestStream() $requestStream.Write($data, 0, $data.Length); try { $res = $wr.GetResponse() } catch [System.Net.WebException] { $ex = $_.Exception } $rs = $res.GetResponseStream() $rsReader = New-Object System.IO.StreamReader $rs $data = $rsReader.ReadToEnd()
Sofern ein Fehler auftritt, befindet sich der allenfalls vom Server ausgegebene Inhalt ebenfalls nicht im Response-Objekt, sondern in der Exception und kann von dort ausgelesen werden. Dieses Verfahren setzt ebenfalls keine Header automatisch, man kann diese aber selbst angeben, um beispielsweise den User-Agent festzulegen:
$wr.Headers['UserAgent'] = 'Mozilla/5.0 (Windows NT; Windows NT 10.0; de-CH)'
Auch dieses Verfahren verwendet standardmässig den in den Windows Einstellungen definierten Proxy, welcher analog zum obigen Beispiel einfach übersteuert werden kann:
$proxy = New-Object System.Net.WebProxy('http://127.0.0.1:8080') $wr.proxy = $proxy
Cookies lassen sich einfach verwenden, indem ein entsprechender Cookie-Container spezifiziert wird. Dies kann auch bereits vor dem Absenden des Requests mittels Get.Response()
durchgeführt werden.
$cookieJar = New-Object System.Net.CookieContainer $wr.CookieContainer = $cookieJar $res = $wr.GetResponse()
Das .NET Framework bietet mit HttpClient
noch eine weitere Klasse für Web-Requests, die über Add-Type
eingebunden werden kann. Der nachfolgend gezeigte Ausschnitt gibt auch im Fehlerfall (z.B. 404) den vom Server gelieferten Inhalt zurück, sofern vorhanden.
Add-Type -AssemblyName System.Net.Http $httpClient = New-Object System.Net.Http.HttpClient try { $task = $httpClient.GetAsync('http://www.example.com') $task.wait() $res = $task.Result if ($res.isFaulted) { write-host $('Error: Status {0}, reason {1}.' -f [int]$res.Status, $res.Exception.Message) } return $res.Content.ReadAsStringAsync().Result } catch [Exception] { write-host ('Error: {0}' -f $_) } finally { if($null -ne $res) { $res.Dispose() } }
Wie in diesem Beispiel ersichtlich, wird GetAsync()
asynchron ausgeführt, blockiert also nicht und kann daher parallelisiert werden. Will man allerdings den gelieferten Inhalt gleich weiterverarbeiten, so muss man wie oben dargestellt auf das Resultat warten.
Das Senden von Daten ist ebenfalls recht einfach zu bewerkstelligen, indem man PostAsync()
verwendet:
$data = New-Object 'system.collections.generic.dictionary[string,string]' $data.Add('param1', 'value1') $data.Add('param2', 'value2') $postData = new-object System.Net.Http.FormUrlEncodedContent($data) $task = $httpClient.PostAsync('http://www.example.com', $PostData) Auch hier werden keine Header automatisch gesetzt, man kann diese aber einfach festlegen: $httpClient.DefaultRequestHeaders.add('User-Agent', $userAgent)
Auch dieses Verfahren verwendet standardmässig den in den Windows-Einstellungen definierten Proxy. Um diesen manuell zu setzen hat es sich als am zuverlässigsten erwiesen, einen eigenen ClientHandler zu verwenden und die entsprechenden Einstellungen dort vorzunehmen:
Add-Type -AssemblyName System.Net.Http $httpHandler = New-Object System.Net.Http.HttpClientHandler $cookieJar = New-Object System.Net.CookieContainer $httpHandler.CookieContainer = $cookieJar $proxy = New-Object System.Net.WebProxy('http://127.0.0.1:8080') $httpHandler.Proxy = $proxy $httpClient = new-object System.Net.Http.HttpClient($httpHandler)
An obigem Beispiel ist auch ersichtlich, wie sich Cookies und damit auch Cookie-basierte Sessions verwenden lassen, indem man dem HTTP-Handler einen Cookie-Container zuweist.
Falls man mit einem Webserver kommunizieren möchte, der ungültige oder als nicht vertrauenswürdig eingestufte Zertifikate verwendet, kann es nützlich sein, die entsprechende Zertifikatsprüfung zu deaktivieren. Mit dem Befehl Invoke-Webrequest
ist dies ab Powershell Version 6 mit Hilfe des Parameters -SkipCertificateCheck
einfach möglich. Ausserdem können mit dem Parameter -SslProtocol
die zu verwendenden SSL/TLS-Versionen angegeben werden.
Mit tieferen Versionen von Powershell oder bei den anderen Methoden ist dies etwas mühsamer. Im Fall eines SSL/TLS-Zertifikatsfehlers wird intern ein Callback aufgerufen. Man kann die Zertifikatsprüfung durch einen eigenen Callback aushebeln, was durch das Hinzufügen von C#-Code mittels Add-Type
möglich ist:
Add-Type @" using System; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class IgnoreCertValidErr { public static void Ignore() { ServicePointManager.ServerCertificateValidationCallback = delegate ( Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors ) { return true; }; } } "@ [IgnoreCertValidErr]::Ignore()
Es ist auch hier möglich, die gewünschten SSL/TLS-Protokoll-Versionen anzugeben. Die entsprechende Aufzählung (enum) SecurityProtocolType enthält die möglichen Werte dafür.
[System.Net.ServicePointManager]::SecurityProtocol = 'tls12, tls11'
Mit diesem Vorgehen lässt sich die Zertifikatsprüfung bei allen genannten Methoden deaktivieren.
Invoke-Webrequest
nimmt schon eine weitgehende Verarbeitung der abgerufenen Inhalte vor und stellt sie als Eigenschaften des Ergebnisobjekts zur Verfügung. Dies kann in einigen Fällen sehr hilfreich sein, weil sich mit wenigen Zeilen bereits nützliche Befehle ausführen lassen. Allerdings funktionieren diejenigen Methoden, die sich auf die Eigenschaft ParsedHtml
verlassen, nur mit den Powershell-Versionen 3 bis 5. Nachfolgend ein paar Beispiele.
Alle Links auf einer Webseite auflisten:
(Invoke-WebRequest -Uri 'https://www.admin.ch').Links.Href | Sort-Object | Get-Unique
Alle Links zu Bildern auflisten:
(Invoke-WebRequest -Uri 'https://www.admin.ch').Images | Select-Object src
Informationen über Formulare und Eingabefelder extrahieren:
(Invoke-WebRequest 'http://www.google.com').Forms bc. (Invoke-WebRequest 'http://www.google.com').InputFileds
Elemente anhand ihres Klassennamens extrahieren:
(Invoke-WebRequest -Uri 'https://www.meteoschweiz.admin.ch/home.html?tab=report').ParsedHtml.getElementsByClassName('textFCK') | %{Write-Host $_.innertext}
Weitere nützliche Methoden sind getElementById
, getElementsByName
und getElementsByTagName
.
Es lassen sich auch einfache Abläufe damit automatisieren, beispielsweise das Ausfüllen und Senden eines Formulars:
$url = 'https://www.google.com' $html = Invoke-WebRequest -Uri $url -SessionVariable 'Session' -UserAgent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0' $html.Forms[0].Fields['q'] = 'powershell' $action = $html.Forms[0].Action $method = $html.Forms[0].Method $res = Invoke-WebRequest -Uri "$url$action" -Method $method -Body $html.Forms[0].Fields -UserAgent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0' -WebSession $Session
Informationen kann man auch mit Powershell-Bordmitteln direkt aus dem abgerufenen Inhalt extrahieren. Dies ist allerdings nur für die allereinfachsten Fälle ratsam, denn HTML und XML mittels Mustererkennung zu verarbeiten ist fast immer zum Scheitern verurteilt.
$wc = New-Object System.Net.WebClient ($wc.DownloadString('http://www.myip.ch/') | Select-String -Pattern "\d{1,3}(\.\d{1,3}){3}" -AllMatches).Matches.Value $wc = New-Object System.Net.WebClient $res = $wc.DownloadString('https://wisdomquotes.com/dalai-lama-quotes-tenzin-gyatso/') ([regex]'<blockquote><p>(.*?)</p></blockquote>').Matches($res) | ForEach-Object { $_.Groups[1].Value }
Eine komfortablere Methode steht mit dem Powershell-Modul PowerHTML zur Verfügung. Dieses Modul ist eine Powershell-Implementation des HtmlAgilityPack, welches einen vollständigen HTML-Parser bereitstellt. Damit sind leistungsfähige Möglichkeiten vorhanden, beispielsweise die Verwendung des XPath-Syntax. Auch in Fällen, in denen das Internet Explorer HTML DOM nicht zur Verfügung steht, beispielweise bei Invoke-Webrequest unter Powershell 7, leistet es nützliche Dienste.
Beim Installieren von Powershell-Modulen über die Powershell-Gallery ist es ratsam, vor der Installation die Abhängigkeiten zu prüfen, um zu verstehen, welche zusätzlichen und eventuell nicht erwünschten Module auch noch installiert werden. PowerHTML erfordert erfreulicherweise keine Installation weiterer Module.
Die Installation kann z.B. wie folgt durchgeführt und in einem Skript zur Verwendung importiert werden:
function loadPowerHtml { if (-not (Get-Module -ErrorAction Ignore -ListAvailable PowerHTML)) { Write-Host "Installing PowerHTML module" Install-Module PowerHTML -Scope CurrentUser -ErrorAction Stop } Import-Module -ErrorAction Stop PowerHTML }
Als Beispiel nachfolgend der Zugriff auf die Paragraph-Elemente der Seite www.example.com:
$wc = New-Object System.Net.WebClient $res = $wc.DownloadString('http://www.example.com') $html = ConvertFrom-Html -Content $res $p = $html.SelectNodes('/html/body/div/p')
Alle gefundenen <p>
-Elemente sind nun in $p
enthalten und können entsprechend weiterverarbeitet werden:
NodeType Name AttributeCount ChildNodeCount ContentLength InnerText -------- ---- -------------- -------------- ------------- --------- Element p 0 1 156 This domain is for... Element p 0 1 70 More information...
Den in einem Element enthaltenen Text ausgeben:
$p[0].innerText
Ein bestimmtes Element anhand seines Textinhalts finden:
($p | Where-Object { $_.innerText -match 'domain' }).innerText
Extraktion einer Tabelle anhand ihres Klassennamens:
$wc = New-Object System.Net.WebClient $res = $wc.DownloadString('https://www.imdb.com/title/tt0057012/') $html = ConvertFrom-Html -Content $res $table = $html.SelectNodes('//table') | Where-Object { $_.HasClass('cast_list') } $cnt = 0 foreach ($row in $table.SelectNodes('tr')) { $cnt += 1 # skip header row if ($cnt -eq 1) { continue } $a = $row.SelectSingleNode('td[2]').innerText.Trim() -replace "`n|`r|\s+", " " $c = $row.SelectSingleNode('td[4]').innerText.Trim() -replace "`n|`r|\s+", " " $row = New-Object -TypeName psobject $row | Add-Member -MemberType NoteProperty -Name Actor -Value $a $row | Add-Member -MemberType NoteProperty -Name Character -Value $c [array]$data += $row }
Bei stark verschachtelten Elementen kann es schnell schwierig werden, den korrekten XPath-Ausdruck zu finden. Hilfe bietet hierbei der Inspector von Browsern wie Firefox, Chrome oder Edge. Im Inspector kann ein Element angewählt werden, anschliessend steht im Kontextmenu die Funktion Copy Xpath zur Verfügung.
Es gibt verwirrend viele Möglichkeiten, mittels Powershell Inhalte von Websites abzurufen. Für viele Fälle empfiehlt es sich, dafür Invoke-Webrequest
zu verwenden und das automatisch erfolgende Parsing zu nutzen. Will man vollständige Kontrolle ausüben oder erweiterte Möglichkeiten nutzen, so ist man mit System.Net.Http.HttpClient
zurzeit am besten bedient.
Zur anschliessenden Extraktion von Daten braucht man in den einfachsten Fällen nur wenige Powershell-Bordmittel. Umfassende Möglichkeiten sind durch zusätzliche Powershell-Module verfügbar.
Unsere Spezialisten kontaktieren Sie gern!
Tomaso Vasella
Tomaso Vasella
Tomaso Vasella
Tomaso Vasella
Unsere Spezialisten kontaktieren Sie gern!