Pleasant Password Server: Passwörter per PowerShell abrufen – mit aktivierter 2FA
Ein Skript soll ein Passwort aus Pleasant Password Server holen, obwohl der Benutzer 2FA aktiviert hat. Der Token-Abruf scheitert zunächst und mit dem Code allein sogar mit HTTP 500; erst der zusätzliche Header für den 2FA-Anbieter bringt den Token. Danach lassen sich Ordner auflisten, Einträge suchen und Passwörter lesen.
$ git log --graph Der Weg zur Lösung
-
Problem
Pleasant-Passwörter im Skript, aber 2FA ist aktiv
Ein Skript soll Zugangsdaten per REST-API aus Pleasant holen. Der Benutzer hat Zwei-Faktor-Authentifizierung.
-
Versuch
Token per Passwort-Grant anfordern
POST an /OAuth2/Token mit Benutzername und Passwort. Der Server lehnt ab und verlangt einen 2FA-Code.
-
Irrweg
Code nur im Header X-Pleasant-OTP mitgeschickt
Beim zweiten Versuch mit dem Code antwortete der Server mit HTTP 500 und ohne brauchbare Meldung.
-
Erkenntnis
Der Server nennt den 2FA-Anbieter
Beim ersten Fehlversuch kommt der Anbieter (X-Pleasant-OTP-Provider, meist totp). Er muss mit dem Code zurückgeschickt werden.
-
Lösung
Token, dann Ordner, Suche und Eintrag lesen
Mit Code und Anbieter kommt der Token. Damit lassen sich Ordner auflisten, Einträge suchen und Benutzername samt Passwort eines Eintrags lesen.
Ich wollte Zugangsdaten aus einem Pleasant Password Server in einem PowerShell-Skript verwenden, statt sie lokal zu speichern. Die REST-API (v5) ist dafür da, aber sobald der Benutzer Zwei-Faktor-Authentifizierung aktiviert hat, reicht die einfache Anmeldung nicht mehr. Hier steht, wie der Ablauf mit 2FA aussieht und woran ich zunächst gescheitert bin.
Das Problem
Der Token-Abruf ist ein OAuth2-Passwort-Grant gegen /OAuth2/Token. Hat der Benutzer 2FA, antwortet der Server beim ersten Versuch mit einem Fehler und verlangt einen Code. Schickt man den Code danach nur im Header X-Pleasant-OTP mit, antwortete der Server bei mir mit HTTP 500 – ohne brauchbare Fehlermeldung.
Die Lösung
Der Server nennt beim ersten, fehlgeschlagenen Versuch den 2FA-Anbieter (Header X-Pleasant-OTP-Provider, bei einer Authenticator-App typischerweise totp). Dieser Anbieter muss zusammen mit dem Code im zweiten Versuch zurückgeschickt werden. Damit bekommt man den Token.
1. Token holen, inklusive 2FA
$base = 'https://pleasant-server:10001'
function Get-HeaderWert($fehler, $name) {
# Windows PowerShell 5.1 liefert eine WebHeaderCollection, PowerShell 7 HttpResponseHeaders
$antwort = $fehler.Exception.Response
if (-not $antwort) { return $null }
if ($antwort.Headers -is [System.Net.WebHeaderCollection]) { return $antwort.Headers[$name] }
$werte = $null
if ($antwort.Headers.TryGetValues($name, [ref]$werte)) { return ($werte -join ',') }
}
function Get-PleasantToken {
param([pscredential]$Credential, [string]$Otp, [string]$OtpProvider)
$body = @{ grant_type = 'password'; username = $Credential.UserName; password = $Credential.GetNetworkCredential().Password }
$headers = @{ Accept = 'application/json' }
if ($Otp) { $headers['X-Pleasant-OTP'] = $Otp.Trim() }
if ($OtpProvider) { $headers['X-Pleasant-OTP-Provider'] = $OtpProvider }
try {
return (Invoke-RestMethod -Uri "$base/OAuth2/Token" -Method Post -Body $body -Headers $headers -ErrorAction Stop).access_token
} catch {
$otpNoetig = (Get-HeaderWert $_ 'X-Pleasant-OTP') -eq 'required' -or $_.ErrorDetails.Message -match 'otp|two_factor|2fa'
if ($otpNoetig -and -not $Otp) {
$anbieter = Get-HeaderWert $_ 'X-Pleasant-OTP-Provider'
if (-not $anbieter) { $anbieter = 'totp' }
$code = Read-Host "2FA-Code ($anbieter)"
return Get-PleasantToken -Credential $Credential -Otp $code -OtpProvider $anbieter
}
throw "Anmeldung fehlgeschlagen: $($_.Exception.Message) $($_.ErrorDetails.Message)"
}
}
$sitzung = Get-PleasantToken -Credential (Get-Credential -Message 'Pleasant-Anmeldung')
$h = @{ Authorization = "Bearer $sitzung"; Accept = 'application/json' }
2. Ordner auflisten und Einträge suchen
Die Ordner kommen als Baum (Children), die Suche liefert die Einträge mit Id, Name, Pfad und Benutzername – nie das Passwort.
Invoke-RestMethod "$base/api/v5/rest/folders" -Headers $h
$treffer = Invoke-RestMethod "$base/api/v5/rest/search" -Method Post -Headers $h -ContentType 'application/json' -Body (@{ Search = 'suchbegriff' } | ConvertTo-Json)
$treffer.Credentials | Select-Object Id, Name, Path, Username
3. Benutzername und Passwort eines Eintrags lesen
$id = 'eintrags-id'
$eintrag = Invoke-RestMethod "$base/api/v5/rest/entries/$id" -Headers $h
$geheim = ([string](Invoke-RestMethod "$base/api/v5/rest/entries/$id/password" -Headers $h)).Trim().Trim('"')
$cred = New-Object System.Management.Automation.PSCredential($eintrag.Username, (ConvertTo-SecureString $geheim -AsPlainText -Force))
Der Passwort-Endpunkt liefert einen JSON-String, deshalb entferne ich die Anführungszeichen. $cred kann direkt als -Credential an andere Befehle gehen.
Warum das funktioniert
Der Server prüft den Code offenbar nur zusammen mit dem Anbieter. Ohne X-Pleasant-OTP-Provider lief die Anmeldung bei mir in einen Serverfehler (500), mit dem Header kam der Token zurück. Ob die 500 genau daran liegt, habe ich nicht weiter untersucht – die Änderung, nach der es lief, war dieser Header.
Stolperfallen
- Token läuft ab: Er gilt eine feste Zeit ab Ausstellung (
expires_inin der Antwort, vom Pleasant-Administrator per Richtlinie festgelegt). Einen Refresh-Token gibt es nach meinem Kenntnisstand nicht, nach Ablauf muss man sich mit neuem 2FA-Code erneut anmelden. Das stammt aus Hinweisen zu einer anderen Anwendung, die dieselbe API nutzt, und ist von mir nicht selbst nachgeprüft. - Token nicht im Klartext ablegen: Will man ihn über mehrere PowerShell-Fenster behalten, lässt er sich als
SecureStringmitExport-Clixmlspeichern (nur für denselben Windows-Benutzer auf demselben Rechner lesbar). - Fehlercodes: Bei 401 ist der Token ungültig, dann hilft eine Neuanmeldung. Bei 403 fehlt die Berechtigung für diesen Eintrag, ein neuer Token ändert daran nichts und kostet nur einen weiteren 2FA-Code.
- Windows PowerShell 5.1: Dort ist TLS 1.2 nicht immer voreingestellt. Hilfreich ist
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12vor dem ersten Aufruf. Das Skript habe ich auf 5.1 nicht ausdrücklich getestet. - Passwort im Speicher:
ConvertTo-SecureString -AsPlainTexthält das Passwort kurz als normalen String. Für die meisten Skripte ist das vertretbar, sollte aber bekannt sein.