PowerShell und TCP

Angefangen hat alles mit den gekaperten Routern, von denen Heise im Sep 2013 berichtete und die Zugangsdaten von Personen eingesammelt haben die unverschlüsselt z.B. Mails angerufen haben. Als erste Reaktion habe ich darauf die Seite ForceSSL geschrieben mit der Aufforderung die Zugänge ohne SSL zu verhindern. Bei einer Webseite ist das recht einfach, Zugriffe auf Port 80 auf 443 umzulenken oder dem Anwender eine nette Beschreibung zu liefern. Aber bei POP3/IMAP4 ist es nicht so einfach, da die Leute dann einfach keine Mails mehr bekommen. Daher habe ich mir überlegt exemplarisch mit PowerShell einen POP3/IMAP4-Server zu schreiben, der jede Verbindung zulässt, auch wenn Benutzernamen und Kennworte "falsch" sind und jedem Anwender einfach nur den Abruf einer vorbereiteten Mail erlaubt. Nebenbei ist dies ein kleiner Exkurs, wie man mit PowerShell direkt TCP-Verbindungen umsetzen kann.

Mittlerweile habe ich aber dennoch einen Weg gefunden, auch bei Invoke-Webrequest bei einem 4xx Fehler über die Exception die Rückantwort auszulesen. Siehe dazu auch PowerShell als HTTP-Client. Das ist aber keine Lösung, wenn die Commandlets auf einen 403 Auth Required selbst schon einen zweiten Request mit Credentials senden.

TCP Client

Für meine Analysen der HTTP Authentication brauchte ich eine Möglichkeit einen HTTP-Request zu senden und die Antwort einzufangen ohne dass eine moderne HTTP-Klasse den Inhalt verändert, direkt auf einen 3xx Redirect reagiert, automatisch sich anmeldet oder bei 4xx/500 Rückmeldungen einen Fehler wirft. Letztlich habe ich damit dann meinen ersten einfachen TCP-Client gebaut, der sich per TCP oder HTTP mit einem WebServer verbindet und genau einen Request sendet und die unverfälschte Antwort anzeigt.

# Get-HTTPAnswer
#
# Simple TCP-Class to connect to a HTTP-Server and get initial reply
#
#
# MS16-065: Description of the TLS/SSL protocol information disclosure vulnerability (CVE-2016-0149): May 10, 2016
# https://support.microsoft.com/en-us/kb/3155464 
# The change introduced in Microsoft Security Bulletin MS16-065 causes the first TLS record after the handshake to be split.
# This causes the SslStream, WebRequest (HttpWebRequest, FtpWebRequest), SmtpClient, and HttpClient (where based on HttpWebRequest)
#  streams to return a single byte for the first read, immediately followed by the rest (n-1) bytes in successive reads.
#  This behavior change only occurs for applications that use TLS 1.0 + Cipher Block Chaining, but not when they use TLS 1.1 or TLS 1.2.


param (
   [System.Uri]$url = "http://www.msxfaq.de",
   [string]$method = "GET",
   [string]$Accept ="text/html,application/xhtml+xml,application/xml",
   [string]$Useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
   [string]$AcceptEncoding = "", #"gzip, deflate", 
   [string]$AcceptLanguage = "en,de-DE",
   [string]$Authorization = $null,
   [string]$connection = "keep-alive",   #"Close"
   [int]$readTimeout=10000   # Read timeout in Milliseconds.  -1= infinite
)

[bool]$https=$false
if ($url.Scheme -eq "https") {
    write-host "using HTTPS"
    $https=$true
}

write-host "Start TCPClient"
$TcpClient = New-Object -TypeName System.Net.Sockets.TcpClient
write-Host "Connect to Host $($url.Host):$($url.Port)"
$TcpClient.Connect($url.Host, $url.Port)

if (!$TcpClient.Connected){
    write-host "Unable to Connect to TCP-Port $($url.Port) - Stop"
    exit
}
else {
    Write-host "Connection to TCP successful"
}

Write-host "Getting Stream"
$TCPStream = $TCPClient.GetStream()

if ($https) {
    write-host "Found HTTPS"
    $Callback = { param($sender, $cert, $chain, $errors) return $true }
    $SSLStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList @($TcpStream, $true, $Callback)
    try {
        write-host "Try SSL Connection"
        #$SSLStream.AuthenticateAsClient($url.Host,$null,[System.Security.Authentication.SslProtocols]::Tls12)
        $SslStream.AuthenticateAsClient($url.Host)
        $Certificate = $SslStream.RemoteCertificate
        Write-host "Cert Subject: $($Certificate.Subject)"
        Write-host "Cert Issuer : $($Certificate.Issuer)"
    }
    catch{
        Write-host "Unable to connect via SSL"
        Write-host "Error: $($_.Exception.Message)"
        exit
    }
    $DataStream = $SSLStream
}
else {
    $DataStream = $TCPStream
}

Write-host "Prepare Request"
[string]$request = ""
if ($https) {
    $request+= "$($method) $($url) HTTP/1.1`r`n"
}
else {
    $request+= "$($method) $($url.LocalPath) HTTP/1.1`r`n"
}
$request+= "User-Agent: $($Useragent)`r`n"
$request+= "Accept: $($Accept)`r`n"
$request+= "Accept-Encoding: $($Acceptencoding)`r`n"
$request+= "Accept-Language: $($Acceptlanguage)`r`n"
$request+= "Connection: $($connection)`r`n"
if($Authorization) {
    write-host "Adding Authorization: $($Authorization)"
    $request+= "Authorization: $($Authorization)`r`n"
 }
$request+= "Host: $($url.Host)`r`n"
$request+= "`r`n"
Write-host "----- Request ------" -BackgroundColor green -ForegroundColor black
Write-Host $Request

$Data = [text.Encoding]::Ascii.GetBytes($request)    
Write-Host "Sending Request"
$DataStream.Write($Data,0,$Data.length)
$DataStream.Flush()

Write-Host "Retrieve response"
[string]$response= ""
[byte[]]$byte = New-Object byte[] 64512
$DataStream.ReadTimeout=$readTimeout
# Read two times due # MS16-065: https://support.microsoft.com/en-us/kb/3155464 
$byteCount = $DataStream.Read($byte, 0, $byte.Length)
Write-host "Bytes received: $($bytecount)"
$response += [text.encoding]::ASCII.GetString( (1..$byteCount | ForEach-Object { $byte[$_-1] } ) )
if ($https) {
    $byteCount = $DataStream.Read($byte, 0, $byte.Length)
    Write-host "Bytes received: $($bytecount)"
    $response += [text.encoding]::ASCII.GetString( (1..$byteCount | ForEach-Object { $byte[$_-1] } ) )
}

Write-host "---- Response ----" -BackgroundColor Green -ForegroundColor black
write-host $response
#$header = $response.substring(0,$response.indexof("`r`n`r`n"))
#Write-Host $header

if ($https) {
    $SslStream.Dispose()
}
$TcpClient.Close()
$TcpClient.Dispose()

Write-host "----- Done -------"

Der Code ist sicher keine Schönheit aber erfüllt seien Zweck. da ja einiges schief gehen konnte, habe ich auch noch viele "Write-Host"-Zeilen drin. Bei der Verwendung SSL gibt es durchaus auch Tücken zu überlisten. Ich habe mir auch hier das Leben einfach gemacht und lesen zweimal den Buffer ein. Ich kann mit dem Code aber nie mehr als die 64kbyte lesen. Ansonsten müssen Sie die Leseroutine über einen Schleife erweitern und das Ende der Daten erkennen.

MS16-065: Description of the TLS/SSL protocol information disclosure vulnerability (CVE-2016-0149): May 10, 2016
The change introduced in Microsoft Security Bulletin MS16-065 causes the first TLS record after the handshake to be split. This causes the SslStream, WebRequest (HttpWebRequest, FtpWebRequest), SmtpClient, and HttpClient (where based on HttpWebRequest) streams to return a single byte for the first read, immediately followed by the rest (n-1) bytes in successive reads. This behavior change only occurs for applications that use TLS 1.0 + Cipher Block Chaining, but not when they use TLS 1.1 or TLS 1.2.
Quelle https://support.microsoft.com/en-us/kb/3155464

Der kleine TCP-Server

Ich habe also erst mal mit die Grundlagen für die TCP-Klassen des .NET-Frameworks beschafft und gelernt, dass man zuerst einen Endpunkt braucht, den man dann im TCP-Listener verwendet kann, der dann auf neue Verbindungen wartet. Sobald aber eine Verbindung kommt, landet diese in einem Streamobjekt über das man an die Verbindungsdaten kommt-

# Versuch eines einfachen POP3-Servers, der Verbinden annimmt und immer eine standardMail retour gibt

$port=110
$endpoint = new-object System.Net.IPEndPoint([system.net.ipaddress]::any, $port)
$listener = new-object System.Net.Sockets.TcpListener $endpoint
$listener.start()   # warte auf eingehende Verbindungen 
                    # halte sie, bis sie von einem Accept abgeholt werden.

write-host "Warte auf neue Verbindungen"
$conn=$listener.AcceptTcpClient() # Blockiert das Skript bis eine Verbindung ankommt

write-host "Neue Verbindung von: " $conn.Client.RemoteEndPoint.Address.IPAddressToString
$stream = $conn.GetStream()        # Binde Datenstrom

while ($true) {
   while ($stream.dataavailible) {
      $data = $stream.readbyte()
      write-host $data -nonewline
      $stream.writebyte($data)
   }
}

$datastream = $stream.read()       # Lese Daten
$stream.close()   # Verbindung beenden

$buffer = new-object system.byte[] 1024
$enc = new-object system.text.asciiEncoding

$line=""
while($datastream.DataAvailable) {  
   $read = $datastream.Read($buffer, 0, 1024)    
   $line+= $enc.GetString($buffer, 0, $read)
   start-sleep -m 100  ## Allow data to buffer für a bit
}
$line

TCP-Client mit StreamWriter

Wer statt Bytes lieber Texte sendet und empfängt, kann das mit dem StreamReader und StreamsWriter vereinfachen:

$stream = $conn.GetStream()        # Binde Datenstrom
[system.io.streamreader]$streamreader = New-Object System.IO.StreamReader($stream,[system.text.encoding]::ASCII)
[system.io.streamwriter]$streamwriter = New-Object System.IO.StreamWriter($stream,[system.text.encoding]::ASCII)
$streamwriter.AutoFlush=$true

$streamwriter.WriteLine("Willkommen. Sende eine Nachricht und beende Sie mit CR/LF")
Write-host " Wait for Data"
$line = $streamreader.readline()
Write-host " Got $($line)
$streamwriter.WriteLine("Your message was: $($line)")
$streamwriter.WriteLine("Bye")
$streamwriter.close()
$streamreader.close()
$stream.close

Das ist natürlich alles nur trivial" und nicht Multithreading. Ein böser Client könnte sich verbinden und ohne Eingabe einfach hängen bleiben. Ein zweiter Client kann sich so nicht verbinden.

Parallelisieren

Die beiden Beispiele beschränken sich auf genau eine Verbindung. Das mag beim Client ja noch ausreichend sein aber wenn Sie einen Server betreiben, dann sollte er schon auch damit umgehen können, mehrere Verbindungen parallel zu betreiben. Technisch bedeutet das den Einstieg in Threading und Multitasking in Powershell. Schon der "$listener.AcceptTcpClient()" stört die parallel Verbindung da diese Zeile den Skriptablauf blockt. Hier gibt es aber schon sehr viele Beispiele im Internet, so dass ich das nicht selbst nochmal schreiben muss.

Denken Sie daran, das dieser Aufwand nur für TCP-Connections erforderlich ist. Wenn sie HTTP nutzen und eigentlich nur einen WebServer/WebService benötigen, dann reicht auch der HTTP-Listener, der im Hintergrund die ganze Arbeit erledigt. Ihr Skript muss dann nur noch die fertigen Daten abholen und bearbeiten. Wenn die Verarbeitung schnell genug ist, kann das zumindest anfangs auch seriell vonstatten gehen. Siehe dazu PowerShell als HTTPServer.

NetCat in PowerShell

Wer nun eine Übungsaufgabe sucht, kann ja mal NETCAT quasi in PowerShell nachbauen. Also ein Modul, welches Daten über die Pipeline an einen TCP-Port sendet und Rückmeldungen über die Pipeline zur Weiterverarbeitung wieder ausgibt.

Weitere Links