openSSH commands unter Windows absichern

Seit Windows Server 2019 kann der openSSH Server über die Features oder per Powershell einfach nach installiert werden. Siehe Windows Server 2019 openSSH. Aber auch für Windows Server 2016 und älter besteht die Möglichkeit openSSH als Server Variante einfach zum laufen zu bekommen: Windows Server 2016 openSSH. In dem Beitrag geht es um das Einschränken und absichern von Befehlen, welche über openSSH commands ausgeführt werden, sowie die damit verbundenen Sicherheitsrisiken und Fallstricke wenn die Variable SSH_ORIGINAL_COMMAND zum Einsatz kommt.

Vor allem im Zusammenhang mit (remote) Automatisierungen kann openSSH als einfache alternative zur Remote Powershell dienen. Die Konfiguration ist entsprechend einfach und über verschiedene User und Keys können beliebige Sicherheitsebenen etabliert werden.

Inhalt

  1. Automatisches ausführen eines definierten Befehles
  2. Parsen von Parametern und deren Sicherheitsrisiko
  3. Einschränken auf ein definiertes Befehlsset mithilfe von Powershell
  4. Einschränken mittels from
  5. Ändern der default Shell auf Powershell

Vorraussetzungen

Die folgenden Anleitungen beziehen sich auf die Windows openSSH Version 8.1.0.0p1beta.

Die sshd Konfig (C:\ProgramData\ssh) muss so angepasst werden, dass die Authentifizierung mittels Pubkey erlaubt wird.

PubkeyAuthentication yes

Für den User, unter welchem der oder die Befehle später ausgeführt werden sollen, muss mittels ssh-keygen ein KeyPair erstellt werden. Für automatisierte Aufrufe sollte hier kein Kennwort festgelegt werden.

Der Puplic Key muss in der authorized_keys Datei C:\Users\<username>\.shh\authorized_keys gespeichert werden. Der private Key wird auf den Client kopiert und wird dort dann mittels

ssh [email protected] -i priv_key

für die Authentifizierung verwendet. Kann man sich mit dem Server (ggf. sogar ohne Kennwort) verbinden kann es an die Einschränkungen gehen.

Definieren einesopenSSH commands Befehel

Fangen wir mit dem einfachsten an: Möchte man immer nur einen bestimmten Befehl remote ausführen und alles andere verbieten, kann dies recht einfach realisiert werden. In der authorized_key Datei des Users kann der gewünschte Befehl einfach per openSSH commands (command=) vor dem eigentlichen Key hinterlegt werden. Der restrict Parameter verhindert zudem, dass weitere Befehle des Users verarbeitet werden oder dieser eine Session (tty) bekommt.

Soll z.B. immer nur die aktuelle Uhrzeit des Servers ausgegeben werden, kann dies recht einfach über folgenden Befehl realisiert werden

command="time /t",restrict ssh-rsa AAAAB3NzaC....x6N1vEN+J+GTY05

Nach der Authentifizierung erhält der Client folgende Ausgabe

Über diese Einschränkung hat der User keinen Möglichkeit andere Befehle an den SSH Server zu schicken.

Möchte man nun weitere Befehle zulassen könnte man z.B. neue KeyValue Paare erzeugen und für jeden Befehl entsprechend einen anderen privateKey verwenden.

Beispiel, wir erzeugen zwei Key Paare, eines für die Uhrzeit und eines für das Datum.

In der Datei werden die beiden Public Keys eingetragen und die entsprechenden Befehle gesetzt.

command="time /t",restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB.....biPT6rRc933aG
command="date /t",restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB....6yTaf7VEI1m1P

Mit den beiden private Keys time und date auf dem Client können nun die Befehle am Server ausgeführt werden.

Spätestens wenn man Befehle mit Paramenten hat wird diese Methode aber recht schnell mühsam, muss man doch für jeden einzelnen Befehl jeweils neue Keys erzeugen und diese auch auf allen Clients bereitstellen. Für einzelne z.B. Backup Befehle kann diese Lösung aber durchaus hilfreich sein, da keine zusätzlichen Skripte oder sonstiges benötigt wird.

Parsen von Parametern und dessen Sicherheitsrisiko

Soll der Befehl gesetzt sein, jedoch Parameter noch frei vom Client definiert werden können, kann die Verwendung von openSSH commands mit der Variable SSH_ORIGINAL_COMMAND zum Einsatz kommen.

The command originally supplied by the client is available in the SSH_ORIGINAL_COMMAND environment variable. Note that this option applies to shell, command or subsystem execution. Also note that this command may be superseded by a sshd_config(5) ForceCommand directive

Doku: https://man.openbsd.org/OpenBSD-current/man8/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT

Aber Vorsicht, das parsen von Parametern stellt ein hohes Sicherheitsrisiko dar.

Zuerst jedoch zum Prinzip

Ein Beispiel: ich möchte den Energsiesparmodus eines Rechners Remote ändern oder eine bestimmte Hyper-V VM starten.

Die Implementierung ist einfach, in der Dokumentation von sshd lernen wir, es gibt einen Parameter, welcher die Eingabe vom SSH Client enthält. Der SSH Server führt dann den definierten Befehl inkl. Parameter in der default Shell (cmd.exe) aus.

command="powercfg /SETACTIVE %SSH_ORIGINAL_COMMAND%",restrict ssh-rsa AAAAB3Nza....

bzw. zum starten einer VM:

command="%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe start-vm %SSH_ORIGINAL_COMMAND%",restrict ssh-rsa AAAAB3NzaC1yc2EAAAA...

Schaut einfach aus, ist jedoch in dieser Form maximal kritisch, denn es ergibt sich hierüber die Möglichkeit beliebigen Code nach dem eigentlichen Parameter an den Server zu senden.

Analyse

Ein paar Beispiele und Tests:

Auf dem Server liegt im Verzeichnis des Users (administrator) eine test.txt. Wir nehmen unser Beispiel von oben und definieren per command den Befehl more jedoch mit der Möglichkeit noch einen Parameter mit zu geben.

command="more %SSH_ORIGINAL_COMMAND%",restrict ssh-rsa AAAAB3NzaC1....

Der User auf dem Client ist admin der auf dem Server administrator

Folgendes Beispiel zeigt, gebe ich den Parameter ohne Anführungszeichen an, wird der zweite Befehl lokal ausgeführt. Sobald ich den Parameter aber in Anführungszeichen setzte, wird der gesamte Befehl vom Server interpretiert. Die Datei wird gelesen und anschließend der zweite Befehl ausgeführt.

Ein weiterer Test:

ich definiere die Variable explizit als String in der authorized_key Datei, dafür muss ich die Anführungszeichen maskieren.

\"%SSH_ORIGINAL_COMMAND%\"
command="more \"%SSH_ORIGINAL_COMMAND%\"",restrict ssh-rsa AAAAB3N....

Ergebnis:

Wir sehen, die Code injection von vorher funktioniert nicht mehr. Der Befehl „& whoami“ wird als String mit an den ersten Befehl gehängt. Windows gibt eine entsprechende Fehlermeldung zurück. Das normale Ausführen ohne weitere Parameter klappt weiterhin. Schaut soweit sicher aus, könnte man meinen.

Leider nein:

Somit ist übrigens auch ohne Probleme der Aufruf einer neuen Shell Möglich und wir haben trotz restrict eine Session

Das Problem sind die Anführungszeichen. Das escapen des zweiten Befehls mithilfe \" oder """ veranlasst den Server, den Code nicht mehr als String zu interpretieren. Somit ist auch ohne Probleme der Aufruf einer neuen Shell Möglich und wir haben trotz restrict eine Session.

String replace

Die Anführungszeichen müssen also am besten aus der Variable herausgefiltert werden.

Das Problem ist, in der authorized_keys Datei lässt sich keine Variable setzen, für ein string replace innerhalb von batch ist dies jedoch meistens notwendig. Ich habe eine Ausnahme gefunden, welche ich nach einigen Versuchen auf das Beispiel Anwenden konnte. Hier ein Beispiel von StackOverflow wie man z.B. ein ! ersetzten kann

set string=hello!
set string=%string:!=%
echo %string%
hello

Hier wird zwar ein set verwendet (was ohne session und daher in unserem Fall nicht funktioniert) aber der Befehl zum ersetzten :<fromChar>=<toChar> spielt sich innerhalb der Variable ab. Nach ein paar Versuchen bin ich auf folgende Syntax gestoßen die zu funktionieren scheint: \"%SSH_ORIGINAL_COMMAND:\"=%\"

command="more \"%SSH_ORIGINAL_COMMAND:\"=%\"",restrict ssh-rsa AAAAB3NzaC

Die folgenden Tests haben gezeigt, dsas somit kein „string escapen“ mehr möglich ist. Wenn der zweite Befehl „whoami“ ausgeführt wurde, dann immer nur lokal.

Die weitere Analyse macht deutlich was genau hier passiert. Zum testen habe ich in der authorized_key Datei den command auf

command="echo \"%SSH_ORIGINAL_COMMAND%\""

angepasst. Die Ausgabe zeigt, Anführungszeichen werden nicht gefiltert.

Mit diesem Befehl jedoch schon.

command="echo \"%SSH_ORIGINAL_COMMAND:\"=%\""

Das Ersetzten des „&“ Zeichens reicht übrigens nicht aus, da z.B. über Pipe | auch eine cmd.exe gespawnt werden kann.

Ist das nun also die Lösung für die sichere Verwendung von SSH_ORIGINAL_COMMAND . Ein ganz klares Jaein.

Folgende Nachteile bzw. Sicherheitslücken bleiben.

  • 1. Mehrere Parameter werden ggf. (abhängig von der Applikation) nicht mehr erkannt, da die kompletten Parameter als ein String mit umschlossenen Anführungszeichen übergeben werden.
Wir sehen, ein Parameter funktioniert, zwei nicht mehr
  • 2. Ein Verwenden von Anführungszeichen, auch wenn diese für einen Parameter notwendig wären, ist nicht mehr möglich.
  • 3. Falls als Parameter ein Pfad angegeben werden muss, werden „path traversal attacks“ nicht abgefangen
  • 4. Wenn die Powershell aufgerufen wird kann mit dem Trennzeichen ; code injiziert werden.
  • 5. Es ist nicht abschließend auszuschließen das es trotzdem noch Möglichkeiten gibt, ggf. durch die Verwendung eines anderen Clients oder anderen Sonderzeichen eine code injection durchzuführen.

Die Empfehlung geht daher ganz klar zu einem eigenen Skript.

Einschränken und filtern der Eingaben mit PowerShell

Die oberen Beispiele haben gezeigt, dass die Verwendung von openSSH commands mit der Variable SSH_ORIGINAL_COMMAND ohne Prüfung und Bereinigung ein erhöhtes Sicherheitsrisiko darstellt. Innerhalb der authorized_keys sind die Möglichkeiten solcher Prüfungen leider zu stark eingeschränkt.

Die Lösung, welche auch unter Linux Systemen die gängige Praxis sein sollte, ist ein eigenes Skript zu erstellen, in welchem die Eingabe verifiziert werden kann bevor sie ausgeführt wird. Ich habe mich hier für ein PowerShell Skript entschieden, welches ich unter dem Namen Commands.ps1 im User Verzeichnis abgelegt habe. Die Berechtigung dieses Skriptes sollte natürlich entsprechend eingeschränkt werden.

Die authorized_keys Key Datei wird wie folgt angepasst

command="%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe C:\Users\Administrator\commands.ps1",restrict ssh-rsa AAAAB3Nz.....aC7ZU1e6

Wichtig: wir dürfen unserem Skript hier NICHT die Variable SSH_ORIGINAL_COMMAND als Parameter übergeben (da dies wieder zu den gleichen Sicherheitsrisiken führen würde) sondern, wir holen uns die Variable direkt im Powershell Skript.

$input = $env:SSH_ORIGINAL_COMMAND
Invoke-Expression $input

Da die Eingabe in dieser Form direkt ausgeführt wird, ist das Skript immer noch anfällig, in diesem Fall jetzt für Powershell Injections

Daher muss der Inhalt der Variable vorher geprüft werden.

Prüfen auf einen speziellen Befehl

$input = $env:SSH_ORIGINAL_COMMAND

# single command
if ( $input -eq "more test.txt")
{
	Invoke-Expression $input
	
} else {
	echo "wrong command: $($input)"
	return
}

Prüfen auf eine Sammlung von Befehlen

$input = $env:SSH_ORIGINAL_COMMAND

# defined command set
$commands = "more test.txt", "Get-Date", "Get-Date -Format ""dddd MM/dd/yyyy HH:mm K"""

if ( $commands -contains $input )
{
	Invoke-Expression $input    
} else {
	echo "command not in set: $($input)"
	return
}

Damit wird die Eingabe explizit geprüft und es werden nur die Befehle ausgeführt, die vorher auch definiert wurden

whoami wird nicht mehr ausgeführt

Je nach Anwendung, kann das Skript natürlich komplexer werden, sollen z.B. die Parameter frei definiert werden können, muss die Prüfung dynamischer erfolgen.

Praktisches Beispiel, ich möchte das starten von VMs erlauben, ohne jedoch jede VM im Skript hinterlegen zu müssen.

Das folgende Skript, trennt mit Hilfe von Split den Befehl von den Parametern (Split anhand von Leerzeichen). Der $command muss in der Liste mit den definierten $allowedCommands enthalten sein. Daher müssen wir hier keine extra Filterung durchführen. Jedes Element im Array, was nach dem Befehl kommt wird mittels RegeEx gefiltert –> alles was kein Buchstabe oder eine Zahl ist (Ausnahme _ und - ) wird entfernt.

Beim Ausführen des Befehls werden die Parameter noch in Anführungszeichen gesetzt, damit VMs mit Leerzeichen auch gestartet werden können.

$input = $env:SSH_ORIGINAL_COMMAND

# defined command set
$allowedCommands = "start-vm"

# split command from parameters
$inputArray = -split $input 
$command = $inputArray[0]
$parameters = $inputArray[1..$inputArray.GetUpperBound(0)]

# filter special charaters
$parameters = $parameters -replace '[^\w\-]', ''

if ( $allowedCommands -contains $command )
{
	Invoke-Expression "$($command) ""$($parameters)"""
} else {
	echo "command not in set: $($input)"
	return
}

Eine alternative wäre gewesen, sich alle aktuell verfügbaren VMs zu holen und zu prüfen ob die Eingabe mit dem Namen einer dieser übereinstimmt und dann erst die Ausführung zu erlauben.

Im Alltag muss das Skript auf entsprechenden Anwendungen jeweils angepasst werden, Pfade z.B. können mit dem oberen Skript nicht übermittelt werden. Wichtig ist immer im Hinterkopf zu behalten, dass wenn Befehle oder Parameter an die Shell übertragen werden, diese stets vorher geprüft werden sollten.

Wenn das Sript in PowerShell geschrieben wird, ist die Arbeit mit RegEx sinnvoll, hierzu sind folgende Seiten sehr zu empfehlen

Clientzugriff pro Key einschränken

Ganz unabhängig von der Verwendung von hinterlegten openSSH commands, kann der Zugriff auf den SSH Server auch auf Userebene bzw. sogar Befehlsebene eingeschränkt werden.

The purpose of this option is to optionally increase security: public key authentication by itself does not trust the network or name servers or anything (but the key); however, if somebody somehow steals the key, the key permits an intruder to log in from anywhere in the world. This additional option makes using a stolen key more difficult (name servers and/or routers would have to be compromised in addition to just the key).

https://man.openbsd.org/sshd.8#from=pattern-list

Hierzu wird der Parameter from=“pattern-list“ in die authorized_keys Datei eingetragen, dies kann somit wieder pro Key erfolgen.

command="%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe C:\Users\Administrator\commands.ps1",restrict,from="192.168.178.5" ssh-rsa AAA

Mehr Informationen direkt in der Dokumentation

Ändern der default Shell auf Powershell

Im letzten Thema möchte ich nur kurz beschreiben wie man die default Shell von openSSH ändert, damit für jeden Key nicht immer erst der Pfad zur PowerShell angegeben werden muss.

Mit folgenden PowerShell Befehl wird die Default Shell festgelegt

New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force

Somit genügt die angebe des Scriptes in der authorized_keys Datei

command="C:\Users\Administrator\commands.ps1",restrict,from="!192.168.178.1,*" ssh-rsa AAA....

Achtung, wenn man die Default Shell auf PowerShell ändert, hilft ein entfernen der Anführungszeichen aus der openSSH commands Variable SSH_ORIGINAL_COMMAND nicht mehr um code Injections zu verhindern!

Fazit

Die hier gezeigten Beispiele, zeigen ganz klar auf, dass ein verwenden der openSSH commands Variable SSH_ORIGINAL_COMMAND Variable innerhalb eines commands, ein hohes Sicherheitsrisiko darstellen. Die Empfehlung ist daher, lieber ein Skript zu schreiben, welches nur valide Eingaben an die darunter liegende Shell weiterleitet.

Quellen und Links

https://www.linuxjournal.com/article/8257

https://man.openbsd.org/

https://docs.microsoft.com/de-de/powershell/scripting/learn/remoting/ssh-remoting-in-powershell-core?view=powershell-7.1

0

Schreibe einen Kommentar