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
1 change: 1 addition & 0 deletions contrib/win32/openssh/win32iocompat.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\signal_sigchld.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\w32api_proxies.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\win32_usertoken_utils.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\win32_session.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\w32log.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\pwd.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\win32_dirent.c" />
Expand Down
1 change: 1 addition & 0 deletions contrib/win32/openssh/win32iocompat.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\spawn.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\w32api_proxies.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\win32_usertoken_utils.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\win32_session.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\gss-sspi.c" />
<ClCompile Include="$(OpenSSH-Src-Path)\contrib\win32\win32compat\win32_pty.c" />
<ClCompile Include="sshTelemetry.c" />
Expand Down
2 changes: 2 additions & 0 deletions contrib/win32/win32compat/misc_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ void to_lower_case(char *s);
void to_wlower_case(wchar_t *s);
HANDLE get_user_token(const char* user, int impersonation);
int load_user_profile(HANDLE user_token, char* user);
extern int attach_to_console_session;
HANDLE get_console_session_token(HANDLE authenticated_token);
int create_directory_withsddl(wchar_t *path, wchar_t *sddl, BOOL check_permissions);
int is_absolute_path(const char *);
int file_in_chroot_jail(HANDLE);
Expand Down
18 changes: 16 additions & 2 deletions contrib/win32/win32compat/spawn-ext.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ __posix_spawn_asuser(pid_t *pidp, const char *path, const posix_spawn_file_actio
int r = -1;
/* use token generated from password auth if already present */
HANDLE user_token = NULL;

int on_console_session = FALSE;

if (password_auth_token)
user_token = password_auth_token;
else if (sspi_auth_user)
Expand All @@ -25,7 +26,20 @@ __posix_spawn_asuser(pid_t *pidp, const char *path, const posix_spawn_file_actio
errno = EOTHER;
return -1;
}
if (strcmp(user, "sshd"))

/* if configured, run inside the user's existing console session */
if (attach_to_console_session) {
HANDLE console_token = get_console_session_token(user_token);

if (console_token != NULL) {
CloseHandle(user_token);
user_token = console_token;
on_console_session = TRUE;
}
}

/* a console user's profile is already loaded by their interactive logon */
if (!on_console_session && strcmp(user, "sshd"))
load_user_profile(user_token, user);

r = posix_spawn_internal(pidp, path, file_actions, attrp, argv, envp, user_token, TRUE);
Expand Down
67 changes: 67 additions & 0 deletions contrib/win32/win32compat/w32api_proxies.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ load_secur32()
return s_hm_secur32;
}

static HMODULE
load_wtsapi32()
{
static HMODULE s_hm_wtsapi32 = NULL;

if (!s_hm_wtsapi32)
s_hm_wtsapi32 = load_module(L"wtsapi32.dll");

return s_hm_wtsapi32;
}

static HMODULE
load_ntdll()
{
Expand Down Expand Up @@ -259,6 +270,62 @@ ULONG pRtlNtStatusToDosError(NTSTATUS status)
return pRtlNtStatusToDosError(status);
}

BOOL pWTSQuerySessionInformationW(HANDLE server, DWORD session_id,
WTS_INFO_CLASS info_class,
LPWSTR *buffer,
DWORD *bytes_returned)
{
HMODULE hm = NULL;
typedef BOOL(WINAPI *WTSQuerySessionInformationWType)(HANDLE, DWORD, WTS_INFO_CLASS, LPWSTR *, DWORD *);
static WTSQuerySessionInformationWType s_pWTSQuerySessionInformationW = NULL;

if (!s_pWTSQuerySessionInformationW) {
if ((hm = load_wtsapi32()) == NULL)
return FALSE;

if ((s_pWTSQuerySessionInformationW = (WTSQuerySessionInformationWType)
get_proc_address(hm, "WTSQuerySessionInformationW")) == NULL)
return FALSE;
}

return s_pWTSQuerySessionInformationW(server, session_id, info_class, buffer, bytes_returned);
}

BOOL pWTSQueryUserToken(ULONG session_id, PHANDLE token)
{
HMODULE hm = NULL;
typedef BOOL(WINAPI *WTSQueryUserTokenType)(ULONG, PHANDLE);
static WTSQueryUserTokenType s_pWTSQueryUserToken = NULL;

if (!s_pWTSQueryUserToken) {
if ((hm = load_wtsapi32()) == NULL)
return FALSE;

if ((s_pWTSQueryUserToken = (WTSQueryUserTokenType)
get_proc_address(hm, "WTSQueryUserToken")) == NULL)
return FALSE;
}

return s_pWTSQueryUserToken(session_id, token);
}

void pWTSFreeMemory(PVOID memory)
{
HMODULE hm = NULL;
typedef void(WINAPI *WTSFreeMemoryType)(PVOID);
static WTSFreeMemoryType s_pWTSFreeMemory = NULL;

if (!s_pWTSFreeMemory) {
if ((hm = load_wtsapi32()) == NULL)
return;

if ((s_pWTSFreeMemory = (WTSFreeMemoryType)get_proc_address(hm, "WTSFreeMemory")) == NULL)
return;
}

s_pWTSFreeMemory(memory);
}

NTSTATUS pLsaClose(LSA_HANDLE lsa_h)
{
HMODULE hm = NULL;
Expand Down
5 changes: 4 additions & 1 deletion contrib/win32/win32compat/w32api_proxies.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#define SECURITY_WIN32
#include <security.h>
#include <Ntsecapi.h>
#include <WtsApi32.h>

BOOL pLogonUserExExW(wchar_t *, wchar_t *, wchar_t *, DWORD, DWORD, PTOKEN_GROUPS, PHANDLE, PSID *, PVOID *, LPDWORD, PQUOTA_LIMITS);
BOOLEAN pTranslateNameW(LPCWSTR, EXTENDED_NAME_FORMAT, EXTENDED_NAME_FORMAT, LPWSTR, PULONG);
Expand All @@ -20,5 +21,7 @@ NTSTATUS pLsaAddAccountRights(LSA_HANDLE, PSID, PLSA_UNICODE_STRING, ULONG);
ULONG pRtlNtStatusToDosError(NTSTATUS);
NTSTATUS pLsaClose(LSA_HANDLE);
NTSTATUS pLsaRemoveAccountRights(LSA_HANDLE, PSID, BOOLEAN, PLSA_UNICODE_STRING, ULONG);

BOOL pWTSQuerySessionInformationW(HANDLE, DWORD, WTS_INFO_CLASS, LPWSTR *, DWORD *);
BOOL pWTSQueryUserToken(ULONG, PHANDLE);
void pWTSFreeMemory(PVOID);

12 changes: 12 additions & 0 deletions contrib/win32/win32compat/w32fd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1146,7 +1146,19 @@ 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;
DWORD token_session = 0, my_session = 0, info_len = 0;
/* lpDesktop is not const, so this cannot be a literal */
static wchar_t winsta0_default[] = L"WinSta0\\Default";
wchar_t* as_user_name = get_username_from_token(as_user);

/* a process in another session cannot inherit our window station and desktop */
if (GetTokenInformation(as_user, TokenSessionId, &token_session, sizeof(token_session), &info_len) &&
ProcessIdToSessionId(GetCurrentProcessId(), &my_session) &&
token_session != my_session) {
debug3("spawning into session %d (from session %d) on %ls", token_session, my_session, winsta0_default);
si.lpDesktop = winsta0_default;
}
Comment on lines +1155 to +1160

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. */
Expand Down
205 changes: 205 additions & 0 deletions contrib/win32/win32compat/win32_session.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Author: Mitch Gaffigan <mitch.gaffigan@comcast.net>
*
* Support for running a session inside the user's existing physical console
* session (WTS session) instead of the service session.
*
* Copyright (c) 2026 Mitch Gaffigan
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <Windows.h>

#include "w32api_proxies.h"
#include "misc_internal.h"
#include "Debug.h"

/* set from sshd_config's AttachToConsoleSession, armed only for the post-auth spawn */
int attach_to_console_session = 0;

/* union so the SID stays suitably aligned */
typedef union {
SID sid;
BYTE buf[SECURITY_MAX_SID_SIZE];
} sid_buf;

/* returns 1 on success, 0 on failure */
static int
copy_token_user_sid(HANDLE token, sid_buf *out)
{
/* union so the sid that follows the TOKEN_USER stays aligned */
union {
TOKEN_USER token_user;
BYTE buf[sizeof(TOKEN_USER) + SECURITY_MAX_SID_SIZE];
} u;
DWORD len = 0;

if (GetTokenInformation(token, TokenUser, &u, sizeof(u), &len) == FALSE) {
error_f("GetTokenInformation(TokenUser) failed with error:%d", GetLastError());
return 0;
}

if (CopySid(sizeof(out->buf), &out->sid, u.token_user.User.Sid) == FALSE) {
error_f("CopySid failed with error:%d", GetLastError());
return 0;
}

return 1;
}

static BOOL
tokens_same_user(HANDLE a, HANDLE b)
{
sid_buf a_sid, b_sid;

if (!copy_token_user_sid(a, &a_sid) || !copy_token_user_sid(b, &b_sid))
return FALSE;

return EqualSid(&a_sid.sid, &b_sid.sid);
}

/* needs no privilege, so use it to avoid WTSQueryUserToken at the logon screen */
static int
console_session_is_active(DWORD session_id)
{
WTS_CONNECTSTATE_CLASS *state = NULL;
DWORD len = 0;
int ret = 0;

if (pWTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session_id,
WTSConnectState, (LPWSTR *)&state, &len) == FALSE) {
debug3_f("WTSQuerySessionInformationW failed for session:%u error:%d",
session_id, GetLastError());
return 0;
}

if (state == NULL || len < sizeof(*state)) {
debug3_f("unexpected WTSConnectState result for session:%u", session_id);
goto done;
}

if (*state != WTSActive) {
debug_f("nobody is logged on to console session:%u", session_id);
goto done;
}

ret = 1;
done:
if (state)
pWTSFreeMemory(state);

return ret;
}

/*
* WTSQueryUserToken returns the filtered token for an administrator on a UAC
* enabled system. sshd sessions are elevated today, so follow the linked token
* to keep that behavior. Returns the token to use, closing the original if it
* was replaced.
*/
static HANDLE
elevate_token(HANDLE token)
{
TOKEN_ELEVATION_TYPE elevation_type;
TOKEN_LINKED_TOKEN linked;
HANDLE primary = NULL;
DWORD len = 0;

if (GetTokenInformation(token, TokenElevationType, &elevation_type,
sizeof(elevation_type), &len) == FALSE) {
debug3_f("GetTokenInformation(TokenElevationType) failed with error:%d",
GetLastError());
return token;
}

/* standard users and UAC disabled systems have no linked token */
if (elevation_type != TokenElevationTypeLimited)
return token;

if (GetTokenInformation(token, TokenLinkedToken, &linked, sizeof(linked), &len) == FALSE) {
debug_f("GetTokenInformation(TokenLinkedToken) failed with error:%d, "
"continuing with the filtered token", GetLastError());
return token;
}

/* the linked token is an impersonation token, we need a primary one */
if (DuplicateTokenEx(linked.LinkedToken, TOKEN_ALL_ACCESS, NULL,
SecurityImpersonation, TokenPrimary, &primary) == FALSE) {
debug_f("DuplicateTokenEx failed with error:%d, "
"continuing with the filtered token", GetLastError());
CloseHandle(linked.LinkedToken);
return token;
}

debug3_f("using the linked elevated token");
CloseHandle(linked.LinkedToken);
CloseHandle(token);

return primary;
}

/*
* Returns a primary token for the physical console session, or NULL when there
* is no such session or it belongs to a user other than authenticated_token.
* The caller owns the returned handle.
*/
HANDLE
get_console_session_token(HANDLE authenticated_token)
{
Comment on lines +165 to +167
HANDLE token = NULL;
DWORD console_session_id;

console_session_id = WTSGetActiveConsoleSessionId();
if (console_session_id == 0xFFFFFFFF || console_session_id == 0) {
debug_f("no physical console session is attached");
return NULL;
}

if (!console_session_is_active(console_session_id))
return NULL;

if (pWTSQueryUserToken(console_session_id, &token) == FALSE) {
DWORD err = GetLastError();

if (err == ERROR_PRIVILEGE_NOT_HELD)
error_f("WTSQueryUserToken needs SeTcbPrivilege, ensure the sshd "
"service has TCB privileges");
else
debug_f("WTSQueryUserToken failed for session:%u error:%d",
console_session_id, err);

return NULL;
}

if (!tokens_same_user(token, authenticated_token)) {
debug_f("console session:%u belongs to a different user, not attaching",
console_session_id);
CloseHandle(token);
return NULL;
}

token = elevate_token(token);

verbose("attaching session to console session %u", console_session_id);

return token;
}
Loading