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
7 changes: 6 additions & 1 deletion contrib/win32/openssh/OpenSSHTestHelper.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ $PubKeyUser = "sshtest_pubkeyuser"
$PasswdUser = "sshtest_passwduser"
$AdminUser = "sshtest_adminuser"
$NonAdminUser = "sshtest_nonadminuser"
$SshdUser = "sshd_user"
$OpenSSHTestAccountsPassword = "Bulldog_123456"
$OpenSSHTestAccounts = $Script:SSOUser, $Script:PubKeyUser, $Script:PasswdUser, $Script:AdminUser, $Script:NonAdminUser
$OpenSSHTestAccounts = $Script:SSOUser, $Script:PubKeyUser, $Script:PasswdUser, $Script:AdminUser, $Script:NonAdminUser, $Script:SshdUser
$SSHDTestSvcName = "sshdTestSvc"

$Script:TestDataPath = "$env:SystemDrive\OpenSSHTests"
Expand Down Expand Up @@ -69,6 +70,7 @@ function Set-OpenSSHTestEnvironment
$Global:OpenSSHTestInfo.Add("PasswdUser", $PasswdUser) # test user to be used for password auth
$Global:OpenSSHTestInfo.Add("AdminUser", $AdminUser) # test user to be used for admin logging tests
$Global:OpenSSHTestInfo.Add("NonAdminUser", $NonAdminUser) # test user to be used for non-admin logging tests
$Global:OpenSSHTestInfo.Add("SshdUser", $SshdUser) # non-admin local user used by UserEnvironment tests
$Global:OpenSSHTestInfo.Add("TestAccountPW", $OpenSSHTestAccountsPassword) # common password for all test accounts
$Global:OpenSSHTestInfo.Add("DebugMode", $DebugMode.IsPresent) # run openssh E2E in debug mode
$Global:OpenSSHTestInfo.Add("DelayTime", 3) # delay between stoppig sshd service and trying to access log files
Expand Down Expand Up @@ -225,6 +227,9 @@ WARNING: Following changes will be made to OpenSSH configuration
$NonAdminUserProfile = Get-LocalUserProfile -User $NonAdminUser
$Global:OpenSSHTestInfo.Add("NonAdminUserProfile", $NonAdminUserProfile)

$SshdUserProfile = Get-LocalUserProfile -User $SshdUser
$Global:OpenSSHTestInfo.Add("SshdUserProfile", $SshdUserProfile)

#make $AdminUser admin
net localgroup Administrators $AdminUser /add

Expand Down
75 changes: 35 additions & 40 deletions contrib/win32/win32compat/w32fd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1055,43 +1055,37 @@ int fork()
}
char * build_commandline_string(const char* cmd, char *const argv[], BOOLEAN prepend_module_path);

wchar_t*
get_username_from_token(HANDLE as_user)
static BOOL
is_sshd_service_token(HANDLE token)
{
wchar_t* user_name = NULL;
SID_NAME_USE usage;
BOOL is_sshd = FALSE;
DWORD count = 0;
GetTokenInformation(as_user, TokenUser, NULL, 0, &count);
if (count) {
void* buffer = malloc(count);
if (buffer) {
if (GetTokenInformation(as_user, TokenUser, buffer, count, &count)) {
TOKEN_USER* owner = (TOKEN_USER*)buffer;
DWORD name_length = 0;
DWORD domain_length = 0;
LookupAccountSidW(NULL, owner->User.Sid, NULL, &name_length, NULL, &domain_length, &usage); /* Figure out the length of the name. */
if (name_length) {
wchar_t* domain_name = malloc(domain_length * sizeof(wchar_t));
if (domain_name) {
user_name = malloc(name_length * sizeof(wchar_t));
if (user_name) {
memset(user_name, 0, name_length * sizeof(wchar_t));
memset(domain_name, 0, domain_length * sizeof(wchar_t));
BOOL success = LookupAccountSidW(NULL, owner->User.Sid, user_name, &name_length, domain_name, &domain_length, &usage);
if (!success) /* Silently return an empty string if unsuccessful. */
{
free(user_name);
user_name = NULL;
}
}
free(domain_name);
}
}
if (GetTokenInformation(token, TokenUser, NULL, 0, &count) ||
GetLastError() != ERROR_INSUFFICIENT_BUFFER || !count)
return FALSE;
void* buffer = malloc(count);
if (!buffer)
return FALSE;
if (GetTokenInformation(token, TokenUser, buffer, count, &count)) {
TOKEN_USER* user = (TOKEN_USER*)buffer;
SID_NAME_USE usage;
DWORD name_len = 0, domain_len = 0;
LookupAccountSidW(NULL, user->User.Sid, NULL, &name_len, NULL, &domain_len, &usage);
if (name_len && domain_len) {
wchar_t* name = malloc(name_len * sizeof(wchar_t));
wchar_t* domain = malloc(domain_len * sizeof(wchar_t));
if (name && domain &&
LookupAccountSidW(NULL, user->User.Sid, name, &name_len, domain, &domain_len, &usage)) {
is_sshd = (_wcsicmp(name, L"sshd") == 0 && _wcsicmp(domain, L"NT SERVICE") == 0);
}
free(buffer);
if (name)
free(name);
if (domain)
free(domain);
}
}
return user_name;
free(buffer);
return is_sshd;
}

/*
Expand All @@ -1106,10 +1100,11 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out,
{
PROCESS_INFORMATION pi;
STARTUPINFOW si;
BOOL b;
BOOL b = FALSE;
char *cmdline;
wchar_t * cmdline_utf16 = NULL;
int ret = -1;

if ((cmdline = build_commandline_string(cmd, argv, prepend_module_path)) == NULL) {
errno = ENOMEM;
goto cleanup;
Expand All @@ -1125,7 +1120,7 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out,
si.hStdOutput = out;
si.hStdError = err;
si.dwFlags = STARTF_USESTDHANDLES;

if (strstr(cmd, "sshd-session.exe") || strstr(cmd, "sshd-auth.exe")) {
flags |= DETACHED_PROCESS;
}
Expand All @@ -1146,12 +1141,12 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out,
if (as_user) {
debug3("spawning %ls as user", t);
LPVOID lpEnvironment = NULL;
wchar_t* as_user_name = get_username_from_token(as_user);
if (as_user_name) {
if (wcsncmp(L"sshd", as_user_name, wcslen(L"sshd")) != 0) { /* Ignore any names that begin with the service name `sshd`. */
b = CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE); /* Load a user environment block inheriting the current context, thereby passing session state. */
}
free(as_user_name);
if (!is_sshd_service_token(as_user)) {
/* Load the user's environment block (HKCU vars, USERPROFILE, etc.),
* inheriting the current context so session state set in
* sshd-session is preserved. Skipped for the NT SERVICE\sshd
* virtual account (sshd worker chain re-spawning itself). */
CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE);
Comment thread
tgauth marked this conversation as resolved.
}
if (lpEnvironment) { /* Pass the user environment block to the new process. */
b = CreateProcessAsUserW(as_user, NULL, t, NULL, NULL, TRUE, flags | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi);
Expand Down
131 changes: 131 additions & 0 deletions regress/pesterTests/UserEnvironment.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
If ($PSVersiontable.PSVersion.Major -le 2) {$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path}
Import-Module $PSScriptRoot\CommonUtils.psm1 -Force
Import-Module OpenSSHUtils -Force

$tC = 1
$tI = 0
$suite = "userenvironment"

Describe "E2E scenarios for user environment block" -Tags "CI" {
BeforeAll {
if ($OpenSSHTestInfo -eq $null) {
throw "`$OpenSSHTestInfo is null. Please run Set-OpenSSHTestEnvironment to set test environments."
}

$server = $OpenSSHTestInfo["Target"]
$port = $OpenSSHTestInfo["Port"]

$testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite
if (-not (Test-Path $testDir)) {
$null = New-Item $testDir -ItemType directory -Force -ErrorAction SilentlyContinue
}

$script:envTestUser = $OpenSSHTestInfo["SshdUser"]
$script:envTestProfile = $OpenSSHTestInfo["SshdUserProfile"]

$keypassphrase = "testpassword"
$script:envTestKey = Join-Path $testDir "sshd_user_envtest_ed25519"
Remove-Item -Path "$($script:envTestKey)*" -Force -ErrorAction SilentlyContinue
ssh-keygen.exe -q -t ed25519 -f $script:envTestKey -N $keypassphrase
$envTestSshDir = Join-Path $script:envTestProfile .ssh
if (-not (Test-Path $envTestSshDir -PathType Container)) {
New-Item $envTestSshDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
$script:envTestAuthKeys = Join-Path $envTestSshDir authorized_keys
Copy-Item "$($script:envTestKey).pub" $script:envTestAuthKeys -Force
Repair-AuthorizedKeyPermission -FilePath $script:envTestAuthKeys -confirm:$false
Add-PasswordSetting -Pass $keypassphrase
}

AfterAll {
Remove-PasswordSetting
if ($script:envTestKey) {
Remove-Item -Path "$($script:envTestKey)*" -Force -ErrorAction SilentlyContinue
}
if ($script:envTestAuthKeys -and (Test-Path $script:envTestAuthKeys)) {
Remove-Item $script:envTestAuthKeys -Force -ErrorAction SilentlyContinue
}
}

AfterEach { $tI++ }
Comment thread
tgauth marked this conversation as resolved.

Context "$tC - User environment variables" {
BeforeAll { $tI = 1 }
AfterAll { $tC++ }

It "$tC.$tI - USERNAME matches the connecting user" {
$o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERNAME%
Comment thread
tgauth marked this conversation as resolved.
"$o".Trim() | Should Be $script:envTestUser
}

It "$tC.$tI - USERPROFILE points to the connecting user's profile" {
$o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERPROFILE%
$remote = "$o".Trim()
$remote | Should Not BeNullOrEmpty
$remote | Should Not Match 'system32\\config\\systemprofile'
# Profile leaf is normally the user name, but Windows can suffix
# it (e.g. sshd_user.DESKTOP-XYZ.001) when the profile dir was
# recreated, so just check it starts with the user name.
($remote -split '\\')[-1] | Should Match ("^" + [regex]::Escape($script:envTestUser))
}

It "$tC.$tI - HOMEDRIVE and HOMEPATH resolve to the user's profile" {
$hd = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %HOMEDRIVE%)".Trim()
$hp = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %HOMEPATH%)".Trim()
$up = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERPROFILE%)".Trim()
$hp | Should Not Match 'system32\\config\\systemprofile'
$hp | Should Not Match '^\\Windows'
$hp | Should Match ("^\\Users\\" + [regex]::Escape($script:envTestUser))
$hd | Should Be $env:SystemDrive
($hd + $hp) | Should Be $up
}

It "$tC.$tI - APPDATA is populated for user" {
$ad = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %APPDATA%)".Trim()
$ad | Should Not Match 'system32\\config\\systemprofile'
$ad | Should Match 'AppData\\Roaming$'
$ad | Should Match ([regex]::Escape($script:envTestUser))
}

It "$tC.$tI - LOCALAPPDATA is populated for user" {
$la = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %LOCALAPPDATA%)".Trim()
$la | Should Not Match 'system32\\config\\systemprofile'
$la | Should Match 'AppData\\Local$'
$la | Should Match ([regex]::Escape($script:envTestUser))
}

It "$tC.$tI - USERDOMAIN equals the local computer name" {
$o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERDOMAIN%
$remote = "$o".Trim()
$remote | Should Not Be 'WORKGROUP'
$remote | Should Be $env:COMPUTERNAME
}
}

Context "$tC - PATH variable" {
BeforeAll {
$tI = 1
$script:pathMarker = "C:\sshtestmarker_$([guid]::NewGuid().ToString('N'))"
ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" `
reg add HKCU\Environment /v Path /t REG_EXPAND_SZ /d $($script:pathMarker) /f | Out-Null
}
AfterAll {
ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" `
reg delete HKCU\Environment /v Path /f | Out-Null
$tC++
}

It "$tC.$tI - contains both system and user entries" {
$o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %PATH%
$remote = "$o".Trim()
$segments = $remote -split ';'
$sysMatches = @($segments | Where-Object { $_ -match '[Ss]ystem32' })
$markerMatches = @($segments | Where-Object { $_ -eq $script:pathMarker })
$sysMatches.Count | Should BeGreaterThan 0
$markerMatches.Count | Should BeGreaterThan 0
$sysIndex = [array]::IndexOf($segments, $sysMatches[0])
$markerIndex = [array]::IndexOf($segments, $script:pathMarker)
$sysIndex | Should BeLessThan $markerIndex
}
}
}