From 5b6575e7aad4b912e66fdda85f10a35b41251a9d Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Tue, 8 Sep 2026 14:00:48 +0900 Subject: [PATCH] wolfsshd: complete the Windows user profile fallback - _GetHomeDirectoryEx sets PROFILEINFO.dwSize, loads the profile only on its loadProfile argument, and reads the home directory with GetUserProfileDirectoryW instead of expanding %USERPROFILE%. - _GetHomeDirectory and _GetHomeDirectoryPreAuth wrap it with that argument set and clear; CheckPublicKeyWIN takes the pre-auth one. - WOLFSSHD_AUTH holds the loaded hive; wolfSSHD_AuthCloseToken unloads it before closing the token, calling RegCloseKey when the unload fails. - The Windows shell cleanup calls RevertToSelf() before closing the auth token rather than after. - windows-sftp.yml gains a no_profile job: testuser is created with net user alone, and an exec session builds the profile, runs as testuser, and releases the hive. Issue: F-13326 --- .github/workflows/windows-cert-store-test.yml | 45 ++- .github/workflows/windows-sftp.yml | 314 +++++++++++++++++- apps/wolfsshd/auth.c | 84 +++-- apps/wolfsshd/wolfsshd.c | 3 +- 4 files changed, 392 insertions(+), 54 deletions(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index b18a32092..08a805ccc 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -671,9 +671,6 @@ jobs: # This is a test user and not a sensitive password. $pw = 'T3stP@ss!xY9' - New-Item -ItemType Directory -Path $homeDir -Force | Out-Null - New-Item -ItemType Directory -Path $sshDir -Force | Out-Null - # Create local user testuser (net user avoids New-LocalUser password policy issues in CI) $o = net user testuser $pw /add /homedir:$homeDir 2>&1 if ($LASTEXITCODE -ne 0) { @@ -685,6 +682,36 @@ jobs: } } + # Log the user on once so Windows builds a real profile: a directory + # with NTUSER.DAT plus the matching ProfileList entry. + $sec = ConvertTo-SecureString $pw -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential("testuser", $sec) + # -WorkingDirectory has to be readable by testuser. + Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "exit" ` + -Credential $cred -WorkingDirectory "C:\" -Wait -ErrorAction Stop + + foreach ($i in 1..120) { + if (Test-Path "$homeDir\NTUSER.DAT") { break } + Start-Sleep -Milliseconds 500 + } + if (-not (Test-Path "$homeDir\NTUSER.DAT")) { + Write-Host "ERROR: no profile was built for testuser" + Get-ChildItem -Path "C:\Users" + exit 1 + } + + # Later steps use $homeDir literally, so the profile has to be there. + $sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value + $profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid" + $imagePath = (Get-ItemProperty -Path $profKey -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath + if ($imagePath -ne $homeDir) { + Write-Host "ERROR: testuser's profile is at '$imagePath', expected '$homeDir'" + exit 1 + } + Write-Host "testuser profile built at $imagePath" + + New-Item -ItemType Directory -Path $sshDir -Force | Out-Null + # X509 auth verifies the client cert against the CA; authorized_keys # is not used but the file should exist. "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline @@ -697,18 +724,6 @@ jobs: # wolfsshd serves SFTP from the home directory while impersonating # testuser; the SFTP tests assert this name appears in the listing. "marker" | Out-File -FilePath "$homeDir\wolfssh_sftp_marker.txt" -Encoding ASCII - icacls $homeDir /grant "testuser:(OI)(CI)RX" /T /q - if ($LASTEXITCODE -ne 0) { - Write-Host "ERROR: icacls failed on $homeDir" - exit 1 - } - - # Set ProfileImagePath so SHGetKnownFolderPath(FOLDERID_Profile) returns $homeDir - # for testuser (GetHomeDirectory in wolfsshd uses that; otherwise it can fail for new users). - $sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value - $profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid" - if (-not (Test-Path $profKey)) { New-Item -Path $profKey -Force | Out-Null } - Set-ItemProperty -Path $profKey -Name "ProfileImagePath" -Value $homeDir -Force - name: Create wolfSSHd config file working-directory: ${{ github.workspace }}\wolfssh diff --git a/.github/workflows/windows-sftp.yml b/.github/workflows/windows-sftp.yml index 36fcf332c..02586fbe9 100644 --- a/.github/workflows/windows-sftp.yml +++ b/.github/workflows/windows-sftp.yml @@ -5,6 +5,8 @@ name: Windows wolfsshd SFTP Test # 2. Recursive SCP test: pull a directory tree with "scp -O -r" # 3. Large file test: WOLFSSH_NO_SFTP_TIMEOUT, WOLFSSH_MAX_SFTP_RW=10485760, # WOLFSSH_MAX_CHN_NAMESZ=4200 - get and put a 3GB file +# 3. No-profile test: a user with no Windows profile at all, so wolfsshd has to +# build one with LoadUserProfileW instead of reading ProfileList on: push: @@ -120,6 +122,8 @@ jobs: artifact_name: wolfssh-windows-build - test_type: large_rw artifact_name: wolfssh-windows-build-large-rw + - test_type: no_profile + artifact_name: wolfssh-windows-build steps: - uses: actions/checkout@v6 @@ -139,6 +143,7 @@ jobs: path: . - name: Create Windows user testuser and authorized_keys + if: matrix.test_type != 'no_profile' working-directory: ${{ github.workspace }}\wolfssh shell: pwsh run: | @@ -147,9 +152,6 @@ jobs: $authKeysFile = "$sshDir\authorized_keys" $pw = 'T3stP@ss!xY9' - New-Item -ItemType Directory -Path $homeDir -Force | Out-Null - New-Item -ItemType Directory -Path $sshDir -Force | Out-Null - $o = net user testuser $pw /add /homedir:$homeDir 2>&1 if ($LASTEXITCODE -ne 0) { if ($o -match "already exists") { @@ -161,20 +163,38 @@ jobs: } Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw" - "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline - icacls $authKeysFile /grant "testuser:R" /q - - # Grant testuser full control of their home directory. - # New-Item creates the directory owned by the runner account; Windows - # only sets correct user ACLs during the normal profile-creation flow. - # Without this, ImpersonateLoggedOnUser succeeds but CreateFile fails - # with ACCESS_DENIED when wolfsshd tries to write files as testuser. - icacls $homeDir /grant "testuser:(OI)(CI)F" /T /q + # Log the user on once so Windows builds a real profile: a directory + # with NTUSER.DAT plus the matching ProfileList entry. + $sec = ConvertTo-SecureString $pw -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential("testuser", $sec) + # -WorkingDirectory has to be readable by testuser. + Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "exit" ` + -Credential $cred -WorkingDirectory "C:\" -Wait -ErrorAction Stop + + foreach ($i in 1..120) { + if (Test-Path "$homeDir\NTUSER.DAT") { break } + Start-Sleep -Milliseconds 500 + } + if (-not (Test-Path "$homeDir\NTUSER.DAT")) { + Write-Host "ERROR: no profile was built for testuser" + Get-ChildItem -Path "C:\Users" + exit 1 + } + # Later steps use $homeDir literally, so the profile has to be there. $sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value $profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid" - if (-not (Test-Path $profKey)) { New-Item -Path $profKey -Force | Out-Null } - Set-ItemProperty -Path $profKey -Name "ProfileImagePath" -Value $homeDir -Force + $imagePath = (Get-ItemProperty -Path $profKey -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath + if ($imagePath -ne $homeDir) { + Write-Host "ERROR: testuser's profile is at '$imagePath', expected '$homeDir'" + exit 1 + } + Write-Host "testuser profile built at $imagePath" + + New-Item -ItemType Directory -Path $sshDir -Force | Out-Null + + "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline + icacls $authKeysFile /grant "testuser:R" /q - name: Create SCP test key and directory tree if: matrix.test_type == 'basic' @@ -200,6 +220,44 @@ jobs: Set-Content -Path "$src\nested\gamma.txt" -Value "gamma contents" icacls $src /grant "testuser:(OI)(CI)F" /T /q + # No home directory, no ACL grant and no ProfileList entry, which is what a + # user that has never logged on to the machine looks like. + - name: Create Windows user testuser with no profile + if: matrix.test_type == 'no_profile' + shell: pwsh + run: | + $pw = 'T3stP@ss!xY9' + Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw" + + # testuser2 is for the concurrent-session test, which needs a user + # whose profile no earlier step has built. + foreach ($u in @('testuser', 'testuser2')) { + $o = net user $u $pw /add 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "net user $u failed: $o" + exit 1 + } + + $sid = (New-Object System.Security.Principal.NTAccount($u)).Translate([System.Security.Principal.SecurityIdentifier]).Value + $profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid" + if (Test-Path $profKey) { + Write-Host "ERROR: $u already has a ProfileList entry" + exit 1 + } + if (Test-Path "C:\Users\$u") { + Write-Host "ERROR: C:\Users\$u already exists" + exit 1 + } + Write-Host "$u created with no profile, SID $sid" + + if ($u -eq 'testuser') { + Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_SID=$sid" + } + else { + Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER2_SID=$sid" + } + } + - name: Create wolfSSHd config file working-directory: ${{ github.workspace }}\wolfssh shell: pwsh @@ -243,6 +301,12 @@ jobs: exit 1 } + $clientExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "client.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1 + if ($clientExe) { + Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_PATH=$($clientExe.FullName)" + } + - name: Copy wolfSSL DLL to executable directory working-directory: ${{ github.workspace }} shell: pwsh @@ -290,6 +354,7 @@ jobs: Add-Content -Path $env:GITHUB_ENV -Value "SSHD_SERVICE_NAME=$serviceName" - name: Test SFTP get non-existent file (no hang, correct error) + if: matrix.test_type != 'no_profile' working-directory: ${{ github.workspace }}\wolfssh shell: pwsh timeout-minutes: 1 @@ -327,8 +392,208 @@ jobs: } Write-Host "PASS: SFTP get non-existent file did not create file, reported error correctly, did not hang" + # First connection for this user, so it is the one that builds the profile. + # An exec session goes through the shell subsystem, whose cleanup unloads + # it. + - name: Test exec session (no profile) + if: matrix.test_type == 'no_profile' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 2 + run: | + if (-not $env:CLIENT_PATH) { + Write-Host "ERROR: client.exe not found" + exit 1 + } + "" | Out-File -FilePath exec_stdin.txt -Encoding ASCII + + # Nothing may have built the profile before this connection, or the + # checks below would not be about the exec session. + if (Test-Path "C:\Users\testuser\NTUSER.DAT") { + Write-Host "ERROR: testuser already has a profile before the exec session" + exit 1 + } + + $proc = Start-Process -FilePath $env:CLIENT_PATH ` + -ArgumentList "-u", "testuser", "-P", $env:TESTUSER_PASSWORD, "-h", "localhost", "-p", "${{env.TEST_PORT}}", "-c", "whoami" ` + -RedirectStandardInput "exec_stdin.txt" ` + -RedirectStandardOutput "exec_out.txt" ` + -RedirectStandardError "exec_err.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "=== exec output ===" + if (Test-Path exec_out.txt) { Get-Content exec_out.txt } + Write-Host "=== exec error ===" + if (Test-Path exec_err.txt) { Get-Content exec_err.txt } + + if ($proc.ExitCode -ne 0) { + Write-Host "ERROR: exec session failed with exit $($proc.ExitCode)" + exit 1 + } + if ((Get-Content exec_out.txt -Raw) -notmatch "testuser") { + Write-Host "ERROR: exec output does not name testuser" + exit 1 + } + + # NTUSER.DAT only exists if a real profile was built for the user. + if (-not (Test-Path "C:\Users\testuser\NTUSER.DAT")) { + Write-Host "ERROR: the exec session did not build a profile" + Get-ChildItem -Path "C:\Users" + exit 1 + } + + # The server thread tears the connection down after the client exits. + $loaded = $true + foreach ($i in 1..120) { + if (-not (Test-Path "Registry::HKEY_USERS\$env:TESTUSER_SID")) { + $loaded = $false + break + } + Start-Sleep -Milliseconds 500 + } + if ($loaded) { + Write-Host "ERROR: testuser's hive is still loaded after the exec session" + exit 1 + } + Write-Host "PASS: exec session built the profile and released the hive" + + # Session A builds the profile and holds the only handle to the hive, + # session B finds the profile already there. Ending A must not unmount the + # hive while B is still running against it. + - name: Test concurrent sessions for one profile-less user + if: matrix.test_type == 'no_profile' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 5 + run: | + if (-not $env:CLIENT_PATH) { + Write-Host "ERROR: client.exe not found" + exit 1 + } + + # Each session holds itself open by pinging. No spaces in the paths, so + # -c stays a single argument. The commands end on their own, which + # leaves no child process holding the hive open. + Set-Content -Path "C:\Users\Public\hold_a.cmd" -Value "ping -n 25 127.0.0.1 > nul" -Encoding ASCII + Set-Content -Path "C:\Users\Public\hold_b.cmd" -Value "ping -n 90 127.0.0.1 > nul" -Encoding ASCII + "" | Out-File -FilePath hold_stdin.txt -Encoding ASCII + + $hive = "Registry::HKEY_USERS\$env:TESTUSER2_SID" + + function Show-Hives($why) { + Write-Host "--- $why" + Write-Host "loaded hives:" + (Get-ChildItem Registry::HKEY_USERS -ErrorAction SilentlyContinue).Name + Write-Host "profile dir: $(Test-Path 'C:\Users\testuser2\NTUSER.DAT')" + } + if (Test-Path "C:\Users\testuser2\NTUSER.DAT") { + Write-Host "ERROR: testuser2 already has a profile" + exit 1 + } + + function Test-Running($proc) { + return $null -ne (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue) + } + + function Start-Hold($script, $tag) { + return Start-Process -FilePath $env:CLIENT_PATH -ArgumentList @( + "-u", "testuser2", "-P", $env:TESTUSER_PASSWORD, + "-h", "localhost", "-p", "${{env.TEST_PORT}}", "-c", $script) ` + -RedirectStandardInput "hold_stdin.txt" ` + -RedirectStandardOutput "hold_${tag}_out.txt" ` + -RedirectStandardError "hold_${tag}_err.txt" ` + -NoNewWindow -PassThru + } + + $procA = Start-Hold "C:\Users\Public\hold_a.cmd" "a" + + # Watch from the start: the hive can appear and go away again inside a + # fixed sleep, which reads the same as never appearing. + $sawHive = $false + foreach ($i in 1..40) { + if (Test-Path $hive) { + $sawHive = $true + Write-Host "hive appeared after $($i * 500) ms" + break + } + Start-Sleep -Milliseconds 500 + } + if (-not (Test-Path "C:\Users\testuser2\NTUSER.DAT")) { + Write-Host "ERROR: session A did not build the profile" + Show-Hives "no profile" + Get-Content hold_a_out.txt, hold_a_err.txt -ErrorAction SilentlyContinue + exit 1 + } + if (-not $sawHive) { + Write-Host "ERROR: testuser2's hive never appeared during session A" + Show-Hives "hive never seen" + Get-Content hold_a_out.txt, hold_a_err.txt -ErrorAction SilentlyContinue + exit 1 + } + if (-not (Test-Running $procA)) { + Write-Host "ERROR: session A ended before the hive check" + Show-Hives "A gone" + Get-Content hold_a_out.txt, hold_a_err.txt -ErrorAction SilentlyContinue + exit 1 + } + if (-not (Test-Path $hive)) { + Write-Host "ERROR: testuser2's hive went away while session A was live" + Show-Hives "hive dropped under A" + exit 1 + } + Write-Host "session A built the profile and holds the hive" + + $procB = Start-Hold "C:\Users\Public\hold_b.cmd" "b" + Start-Sleep -Seconds 10 + if (-not (Test-Running $procB)) { + Write-Host "ERROR: session B ended early" + Get-Content hold_b_out.txt, hold_b_err.txt + exit 1 + } + Write-Host "session B is running alongside session A" + + Wait-Process -Id $procA.Id -Timeout 120 -ErrorAction SilentlyContinue + if (Test-Running $procA) { + Write-Host "ERROR: session A did not end" + exit 1 + } + # This asserts the hive does not go away, so settle rather than poll. + Start-Sleep -Seconds 15 + + if (-not (Test-Running $procB)) { + Write-Host "ERROR: session B ended before the hive check" + Get-Content hold_b_out.txt, hold_b_err.txt + exit 1 + } + if (-not (Test-Path $hive)) { + Write-Host "ERROR: ending session A unmounted the hive under session B" + Show-Hives "hive dropped under B" + exit 1 + } + Write-Host "hive survived session A ending" + + Wait-Process -Id $procB.Id -Timeout 240 -ErrorAction SilentlyContinue + if (Test-Running $procB) { + Write-Host "ERROR: session B did not end" + exit 1 + } + $loaded = $true + foreach ($i in 1..120) { + if (-not (Test-Path $hive)) { + $loaded = $false + break + } + Start-Sleep -Milliseconds 500 + } + if ($loaded) { + Write-Host "ERROR: testuser2's hive is still loaded after both sessions ended" + Show-Hives "hive still loaded" + exit 1 + } + Write-Host "PASS: the hive outlived session A and went away with session B" + - name: Test SFTP connection (basic) - if: matrix.test_type == 'basic' + if: matrix.test_type == 'basic' || matrix.test_type == 'no_profile' working-directory: ${{ github.workspace }}\wolfssh shell: pwsh run: | @@ -400,6 +665,25 @@ jobs: } Write-Host "Recursive SCP test passed" + - name: Verify the SFTP session used testuser's profile + if: matrix.test_type == 'no_profile' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $output = Get-Content sftp_output.txt -Raw + + if ($output -match "systemprofile") { + Write-Host "ERROR: session landed in the service account's profile" + Write-Host $output + exit 1 + } + if ($output -notmatch "testuser") { + Write-Host "ERROR: 'testuser' missing from pwd output" + Write-Host $output + exit 1 + } + Write-Host "PASS: SFTP session used testuser's own home directory" + - name: Create 3GB test file and run SFTP get/put if: matrix.test_type == 'large_rw' working-directory: ${{ github.workspace }}\wolfssh diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 87e3099b8..bc074625e 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -143,6 +143,7 @@ struct WOLFSSHD_AUTH { const WOLFSSHD_CONFIG* conf; #if defined(_WIN32) HANDLE token; /* a users token */ + HANDLE profile; /* hive */ #endif int gid; int uid; @@ -2022,27 +2023,25 @@ extern BOOL WINAPI LogonUserExExW(LPTSTR usr, #define MAX_USERNAME 256 -static int _GetHomeDirectory(WOLFSSHD_AUTH* auth, const char* usr, WCHAR* out, int outSz) +static int _GetHomeDirectoryEx(WOLFSSHD_AUTH* auth, const char* usr, + WCHAR* out, int outSz, int loadProfile) { int ret = WS_SUCCESS; WCHAR usrW[MAX_USERNAME]; wchar_t* homeDir; HRESULT hr; size_t wr; + DWORD outSzW; + PROFILEINFO pInfo = { 0 }; /* convert user name to Windows wchar type */ mbstowcs_s(&wr, usrW, MAX_USERNAME, usr, MAX_USERNAME-1); - hr = SHGetKnownFolderPath((REFKNOWNFOLDERID)&FOLDERID_Profile, - 0, wolfSSHD_GetAuthToken(auth), &homeDir); - if (SUCCEEDED(hr)) { - wcscpy_s(out, outSz, homeDir); - CoTaskMemFree(homeDir); - } - else { - PROFILEINFO pInfo = { 0 }; - - /* failed with get known folder path, try with loading the user profile */ + /* The hive is machine wide, so every session takes its own handle on it. + * A session holding none would have the hive unloaded underneath it when + * another session for the same user ends. */ + if (loadProfile && auth->profile == NULL) { + pInfo.dwSize = sizeof(pInfo); pInfo.dwFlags = PI_NOUI; pInfo.lpUserName = usrW; if (LoadUserProfileW(wolfSSHD_GetAuthToken(auth), &pInfo) != TRUE) { @@ -2051,24 +2050,53 @@ static int _GetHomeDirectory(WOLFSSHD_AUTH* auth, const char* usr, WCHAR* out, i (unsigned long)GetLastError(), usr); ret = WS_FATAL_ERROR; } - - /* get home directory env. for user */ - if (ret == WS_SUCCESS && - ExpandEnvironmentStringsW(L"%USERPROFILE%", out, outSz) == 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Error getting user %s's home path", usr); - ret = WS_FATAL_ERROR; + else { + /* keep loaded for the session */ + auth->profile = pInfo.hProfile; } + } - /* @TODO is unload of user needed here? - UnloadUserProfileW(wolfSSHD_GetAuthToken(conn->auth), pInfo.hProfile); - */ + if (ret == WS_SUCCESS) { + hr = SHGetKnownFolderPath((REFKNOWNFOLDERID)&FOLDERID_Profile, + 0, wolfSSHD_GetAuthToken(auth), &homeDir); + if (SUCCEEDED(hr)) { + wcscpy_s(out, outSz, homeDir); + CoTaskMemFree(homeDir); + } + else { + outSzW = (DWORD)outSz; + if (GetUserProfileDirectoryW(wolfSSHD_GetAuthToken(auth), out, + &outSzW) != TRUE) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error %lu getting user %s's home path", + (unsigned long)GetLastError(), usr); + ret = WS_FATAL_ERROR; + } + } } return ret; } +/* Home directory of an authenticated user, building the profile when the user + * has none yet. */ +static int _GetHomeDirectory(WOLFSSHD_AUTH* auth, const char* usr, WCHAR* out, + int outSz) +{ + return _GetHomeDirectoryEx(auth, usr, out, outSz, 1); +} + + +/* Home directory for a caller that has not authenticated the user yet. Never + * builds a profile, so it fails for a user that has none. */ +static int _GetHomeDirectoryPreAuth(WOLFSSHD_AUTH* auth, const char* usr, + WCHAR* out, int outSz) +{ + return _GetHomeDirectoryEx(auth, usr, out, outSz, 0); +} + + int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int outSz) { return _GetHomeDirectory(auth, wolfSSH_GetUsername(ssh), out, outSz); @@ -2083,11 +2111,21 @@ HANDLE wolfSSHD_GetAuthToken(const WOLFSSHD_AUTH* auth) return auth->token; } -/* Close and clear the impersonation token. Safe to call more than once. */ +/* Unload the profile and close the token. Safe to call more than once. */ void wolfSSHD_AuthCloseToken(WOLFSSHD_AUTH* auth) { if (auth != NULL && auth->token != NULL && auth->token != INVALID_HANDLE_VALUE) { + if (auth->profile != NULL) { + if (UnloadUserProfile(auth->token, auth->profile) != TRUE) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error %lu unloading user profile", + (unsigned long)GetLastError()); + RegCloseKey((HKEY)auth->profile); + } + /* drop it even when the unload fails */ + auth->profile = NULL; + } CloseHandle(auth->token); auth->token = NULL; } @@ -2355,7 +2393,7 @@ static int CheckPublicKeyWIN(const char* usr, if (ret == WSSHD_AUTH_SUCCESS) { WCHAR h[MAX_PATH]; - if (_GetHomeDirectory(authCtx, usr, h, MAX_PATH) == WS_SUCCESS) { + if (_GetHomeDirectoryPreAuth(authCtx, usr, h, MAX_PATH) == WS_SUCCESS) { CHAR r[MAX_PATH]; size_t rSz; diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index f057e9554..07e6f9da4 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -2388,6 +2388,8 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (ptyOut != NULL) { CloseHandle(ptyOut); } + /* back to the service account, unloading the profile needs its privileges */ + RevertToSelf(); if (processCreated) { CloseHandle(processInfo.hThread); CloseHandle(processInfo.hProcess); @@ -2396,7 +2398,6 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (cmd != NULL) { WFREE(cmd, NULL, DYNTYPE_SSHD); } - RevertToSelf(); return ret; } #else