Skip to content

Commit 8c4e2bd

Browse files
committed
fix(core): harden Invoke-AtlasAsUser token and session handling
Enable SeTcb/SeAssignPrimaryToken/SeIncreaseQuota before the WTS calls and enumerate sessions for the first active one (console fallback), so it works from the SYSTEM phase under Hyper-V enhanced-session/RDP where the user isn't in the console session. Surface Win32 error codes on failure.
1 parent 1c5bd83 commit 8c4e2bd

1 file changed

Lines changed: 97 additions & 3 deletions

File tree

  • playbook/Executables/AtlasModules/Scripts/Modules/Atlas.Core/Domain

playbook/Executables/AtlasModules/Scripts/Modules/Atlas.Core/Domain/RunAsUser.ps1

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,31 @@ namespace Atlas {
5252
enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation }
5353
enum TOKEN_INFORMATION_CLASS { TokenLinkedToken = 19 }
5454
55+
[StructLayout(LayoutKind.Sequential)]
56+
struct LUID {
57+
public uint LowPart;
58+
public int HighPart;
59+
}
60+
61+
[StructLayout(LayoutKind.Sequential)]
62+
struct TOKEN_PRIVILEGES {
63+
public int PrivilegeCount;
64+
public LUID Luid;
65+
public int Attributes;
66+
}
67+
68+
[StructLayout(LayoutKind.Sequential)]
69+
struct WTS_SESSION_INFO {
70+
public uint SessionId;
71+
public IntPtr pWinStationName;
72+
public int State; // WTS_CONNECTSTATE_CLASS; 0 = WTSActive
73+
}
74+
5575
const int TOKEN_DUPLICATE = 0x0002;
76+
const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
77+
const int TOKEN_QUERY = 0x0008;
78+
const int SE_PRIVILEGE_ENABLED = 0x0002;
79+
const int ERROR_NOT_ALL_ASSIGNED = 1300;
5680
const uint MAXIMUM_ALLOWED = 0x02000000;
5781
const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
5882
const int CREATE_NO_WINDOW = 0x08000000;
@@ -61,9 +85,74 @@ namespace Atlas {
6185
[DllImport("kernel32.dll", SetLastError = true)]
6286
static extern uint WTSGetActiveConsoleSessionId();
6387
88+
[DllImport("kernel32.dll")]
89+
static extern IntPtr GetCurrentProcess();
90+
91+
[DllImport("advapi32.dll", SetLastError = true)]
92+
static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, out IntPtr TokenHandle);
93+
94+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
95+
static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid);
96+
97+
[DllImport("advapi32.dll", SetLastError = true)]
98+
static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, IntPtr ReturnLength);
99+
100+
[DllImport("wtsapi32.dll", SetLastError = true)]
101+
static extern bool WTSEnumerateSessions(IntPtr hServer, int Reserved, int Version, out IntPtr ppSessionInfo, out int pCount);
102+
103+
[DllImport("wtsapi32.dll")]
104+
static extern void WTSFreeMemory(IntPtr pMemory);
105+
64106
[DllImport("wtsapi32.dll", SetLastError = true)]
65107
static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken);
66108
109+
// SYSTEM holds these privileges but they can arrive disabled; WTSQueryUserToken
110+
// needs SeTcb and CreateProcessAsUser needs SeAssignPrimaryToken + SeIncreaseQuota.
111+
static void EnablePrivilege(string name) {
112+
IntPtr token;
113+
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out token)) {
114+
throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcessToken failed.");
115+
}
116+
try {
117+
LUID luid;
118+
if (!LookupPrivilegeValue(null, name, out luid)) {
119+
throw new Win32Exception(Marshal.GetLastWin32Error(), "LookupPrivilegeValue(" + name + ") failed.");
120+
}
121+
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES();
122+
tp.PrivilegeCount = 1;
123+
tp.Luid = luid;
124+
tp.Attributes = SE_PRIVILEGE_ENABLED;
125+
if (!AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero) || Marshal.GetLastWin32Error() == ERROR_NOT_ALL_ASSIGNED) {
126+
throw new Win32Exception(Marshal.GetLastWin32Error(), "Enabling privilege " + name + " failed (is the caller SYSTEM?).");
127+
}
128+
}
129+
finally {
130+
CloseHandle(token);
131+
}
132+
}
133+
134+
// The interactive user can be in a non-console session (Hyper-V enhanced session,
135+
// RDP), so pick the first active user session and only fall back to the console.
136+
static uint GetInteractiveSessionId() {
137+
IntPtr pSessions;
138+
int count;
139+
if (WTSEnumerateSessions(IntPtr.Zero, 0, 1, out pSessions, out count)) {
140+
try {
141+
int size = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
142+
for (int i = 0; i < count; i++) {
143+
WTS_SESSION_INFO info = (WTS_SESSION_INFO)Marshal.PtrToStructure(new IntPtr(pSessions.ToInt64() + (long)i * size), typeof(WTS_SESSION_INFO));
144+
if (info.State == 0 && info.SessionId != 0) {
145+
return info.SessionId;
146+
}
147+
}
148+
}
149+
finally {
150+
WTSFreeMemory(pSessions);
151+
}
152+
}
153+
return WTSGetActiveConsoleSessionId();
154+
}
155+
67156
[DllImport("advapi32.dll", SetLastError = true)]
68157
static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes,
69158
SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType, out IntPtr phNewToken);
@@ -95,14 +184,19 @@ namespace Atlas {
95184
// Runs the command line as the active console user; returns the child exit code
96185
// (or 0 when not waiting). Throws Win32Exception on any failure.
97186
public static int Launch(string applicationName, string commandLine, string workingDirectory, bool wait, bool elevated) {
98-
uint sessionId = WTSGetActiveConsoleSessionId();
187+
EnablePrivilege("SeTcbPrivilege");
188+
EnablePrivilege("SeAssignPrimaryTokenPrivilege");
189+
EnablePrivilege("SeIncreaseQuotaPrivilege");
190+
191+
uint sessionId = GetInteractiveSessionId();
99192
if (sessionId == 0xFFFFFFFF) {
100-
throw new InvalidOperationException("No active console session; there is no interactive user to run as.");
193+
throw new InvalidOperationException("No active user session; there is no interactive user to run as.");
101194
}
102195
103196
IntPtr userToken = IntPtr.Zero;
104197
if (!WTSQueryUserToken(sessionId, out userToken)) {
105-
throw new Win32Exception(Marshal.GetLastWin32Error(), "WTSQueryUserToken failed (is the caller SYSTEM with a user logged on?).");
198+
int error = Marshal.GetLastWin32Error();
199+
throw new Win32Exception(error, string.Format("WTSQueryUserToken failed for session {0} (Win32 error {1}).", sessionId, error));
106200
}
107201
108202
IntPtr primaryToken = IntPtr.Zero;

0 commit comments

Comments
 (0)