Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 120 additions & 1 deletion .github/workflows/windows-sftp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ name: Windows wolfsshd SFTP Test
# 1. Basic test: wolfsshd + SFTP client (pwd, ls, put/get small file)
# 2. 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:
Expand Down Expand Up @@ -119,6 +121,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
Expand All @@ -138,6 +142,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: |
Expand Down Expand Up @@ -175,6 +180,33 @@ jobs:
if (-not (Test-Path $profKey)) { New-Item -Path $profKey -Force | Out-Null }
Set-ItemProperty -Path $profKey -Name "ProfileImagePath" -Value $homeDir -Force

# 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'
$o = net user testuser $pw /add 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "net user failed: $o"
exit 1
}
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw"

$sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value
$profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid"
if (Test-Path $profKey) {
Write-Host "ERROR: testuser already has a ProfileList entry"
exit 1
}
if (Test-Path "C:\Users\testuser") {
Write-Host "ERROR: C:\Users\testuser already exists"
exit 1
}
Write-Host "testuser created with no profile, SID $sid"
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_SID=$sid"

- name: Create wolfSSHd config file
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
Expand Down Expand Up @@ -265,6 +297,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
Expand Down Expand Up @@ -302,8 +335,75 @@ 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: |
$clientExe = Get-ChildItem -Path "${{ github.workspace }}" -Recurse -Filter "client.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1
if (-not $clientExe) {
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 $clientExe.FullName `
-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"

- 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: |
Expand All @@ -326,6 +426,25 @@ jobs:
}
Write-Host "Basic SFTP 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
Expand Down
80 changes: 59 additions & 21 deletions apps/wolfsshd/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2022,13 +2023,15 @@ 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;

/* convert user name to Windows wchar type */
mbstowcs_s(&wr, usrW, MAX_USERNAME, usr, MAX_USERNAME-1);
Expand All @@ -2042,33 +2045,58 @@ static int _GetHomeDirectory(WOLFSSHD_AUTH* auth, const char* usr, WCHAR* out, i
else {
PROFILEINFO pInfo = { 0 };

/* failed with get known folder path, try with loading the user profile */
pInfo.dwFlags = PI_NOUI;
pInfo.lpUserName = usrW;
if (LoadUserProfileW(wolfSSHD_GetAuthToken(auth), &pInfo) != TRUE) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Error %lu loading user %s",
(unsigned long)GetLastError(), usr);
ret = WS_FATAL_ERROR;
/* failed with get known folder path, try with loading the user profile.
* A second load would replace the handle needed to unload the first. */
if (loadProfile && auth->profile == NULL) {
pInfo.dwSize = sizeof(pInfo);
pInfo.dwFlags = PI_NOUI;
pInfo.lpUserName = usrW;
if (LoadUserProfileW(wolfSSHD_GetAuthToken(auth), &pInfo) != TRUE) {
Comment thread
yosuke-wolfssl marked this conversation as resolved.
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Error %lu loading user %s",
(unsigned long)GetLastError(), usr);
ret = WS_FATAL_ERROR;
}
else {
/* keep loaded for the session */
auth->profile = pInfo.hProfile;
}
}

/* 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;
if (ret == WS_SUCCESS) {
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;
}
}

/* @TODO is unload of user needed here?
UnloadUserProfileW(wolfSSHD_GetAuthToken(conn->auth), pInfo.hProfile);
*/
}

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);
Expand All @@ -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) {
Comment thread
yosuke-wolfssl marked this conversation as resolved.
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;
}
Expand Down Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion apps/wolfsshd/wolfsshd.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
Loading