在PowerShell中捕获以其他用户身份运行的进程的输出/错误



这是以前提出和回答过的问题的变体。

变化在于使用UserNamePassword来设置 System.Diagnostics.ProcessStartInfo 对象。在这种情况下,我们无法读取进程的输出和错误流——这是有道理的,因为进程不属于我们!

但即便如此,我们已经催生了这个过程,所以应该可以捕获输出。

我怀疑这是一个重复的,但它似乎在答案部分被误解了。

可以从使用不同用户标识启动的(总是提升的)进程捕获输出流,如以下自包含示例代码所示:

注意:

  • 如果通过 PowerShell远程处理(例如通过Invoke-Command -ComputerName,包括 JEA)执行命令,则不起作用。

    • 有关繁琐的解决方法,请参阅底部部分。

    • 但是,JEA 不需要此解决方法,如注释中所述:

      JEA 会话实际上支持RunAsCredential(除了虚拟帐户和组托管服务帐户之外),因此我们可以简单地以预期的公共用户身份运行,从而避免在会话期间更改用户上下文的需要。

    • 正如 Ash 指出的那样,无论您是否模拟其他用户,在访问远程会话中的网络资源时也可能会遇到臭名昭著的双跃点问题。

  • 代码提示输入目标用户的凭据。

  • 工作目录必须设置为允许目标用户访问的目录路径,并默认为下面的本地配置文件文件夹 - 假设它存在;根据需要进行调整。

  • 标准输出
  • 和标准输出作为多行字符串单独捕获,完整。

    • 如果要合并两个流,请通过 shell 调用目标程序并使用其重定向功能 (2>&1)。

    • 下面的示例调用执行对 shell 的调用,即通过其/c参数进行cmd.exe,将一行输出到 stdout,另一行输出到 stderr(>&2)。如果按如下方式修改Arguments = ...行,则 stderr 流将合并到 stdout 流中:

      Arguments = '/c "(echo success & echo failure >&2) 2>&1"'
      
  • 该代码适用于 Windows PowerShell 和 PowerShell (Core) 7+,并通过异步读取流来防止潜在的死锁。[1]

    • 在按需安装的跨平台PowerShell(核心)7+版本中,实现效率更高,因为它使用专用线程通过ForEach-Object-Parallel等待异步任务完成。

    • 在 Windows Windows PowerShell 版本中,必须使用定期轮询(穿插Start-Sleep调用)来查看异步任务是否已完成。

    • 如果您只需要捕获一个流,例如,仅 stdout,如果您已将 stderr 合并到其中(如上所述),则可以将实现简化为同步$stdout = $ps.StandardOutput.ReadToEnd()调用,如本答案所示。

# Prompt for the target user's credentials.
$cred = Get-Credential
# The working directory for the new process.
# IMPORTANT: This must be a directory that the target user is permitted to access.
#            Here, the target user's local profile folder is used.
#            Adjust as needed.
$workingDir = Join-Path (Split-Path -Parent $env:USERPROFILE) $cred.UserName
# Start the process.
$ps = [System.Diagnostics.Process]::Start(
[System.Diagnostics.ProcessStartInfo] @{
FileName = 'cmd'
Arguments = '/c "echo success & echo failure >&2"'
UseShellExecute = $false
WorkingDirectory = $workingDir
UserName = $cred.UserName
Password = $cred.Password
RedirectStandardOutput = $true
RedirectStandardError = $true
}
)
# Read the output streams asynchronously, to avoid a deadlock.
$tasks = $ps.StandardOutput.ReadToEndAsync(), $ps.StandardError.ReadToEndAsync()
if ($PSVersionTable.PSVersion.Major -ge 7) {
# PowerShell (Core) 7+: Wait for task completion in background threads.
$tasks | ForEach-Object -Parallel { $_.Wait() }
} else {  
# Windows PowerShell: Poll periodically to see when both tasks have completed.
while ($tasks.IsComplete -contains $false) {
Start-Sleep -MilliSeconds 100
}
}
# Wait for the process to exit.
$ps.WaitForExit()
# Sample output: exit code and captured stream contents.
[pscustomobject] @{
ExitCode = $ps.ExitCode
StdOut = $tasks[0].Result.Trim()
StdErr = $tasks[1].Result.Trim()
} | Format-List

输出:

ExitCode : 0
StdOut   : success
StdErr   : failure

如果需要以给定用户身份运行,则提升(以管理员身份):

  • 根据设计,您不能同时使用-Verb RunAs请求提升,也不能在单个操作中使用不同的用户标识 (-Credential) 运行 - 既不能使用Start-Process也不能使用基础 .NETAPI,System.Diagnostics.Process

    • 如果你请求提升,并且你自己是管理员,则提升的进程将以你身份运行 - 假设你已确认显示的 UAC 对话框的"是/否"形式。
    • 否则,UAC 将显示一个凭据对话框,要求您提供管理员的凭据 - 并且无法预设这些凭据,甚至无法预设用户名。
  • 根据设计,无法直接从已启动的提升进程捕获输出- 即使提升的进程使用你自己的标识运行也是如此。

    • 但是,如果以其他用户身份启动提升的进程,则可以捕获输出,如顶部所示。

要获得所需的内容,需要以下方法

  • 您需要两个Start-Process呼叫:

    • 第一个以目标用户身份启动提升进程(-Credential)

    • 第二个从该进程启动以请求提升,然后在目标用户的上下文中提升,假设他们是管理员。

  • 由于您只能从提升的进程本身内部捕获输出,因此您需要通过 shell 启动目标程序并使用其重定向 (>) 功能捕获文件中的输出。

不幸的是,这是一个不平凡的解决方案,需要考虑许多微妙之处。

下面是一个自包含的示例

  • 它执行命令whoaminet session(仅在提升的会话中成功),并在指定工作目录中的文件out.txt中捕获它们的组合 stdout 和 stderr 输出。

  • 同步执行,即它等待提升的目标进程退出,然后再继续;如果这不是必需的,请删除-PassThru和封闭(...).WaitForExit(),以及从嵌套的Start-Process调用中-Wait

    • 注意:-Wait不能在外部Start-Process调用中使用的原因是bug,从 PowerShell 7.2.2 开始仍然存在。 - 请参阅 GitHub 问题 #17033。
  • 按照源代码注释中的说明:

    • 当系统提示输入目标用户的凭据时,请务必指定管理员的凭据,以确保使用该用户的标识提升成功。

    • $workingDir中,指定允许目标用户访问的工作目录,即使从提升的会话也是如此。默认情况下使用目标用户的本地配置文件 - 假设它存在。

# Prompt for the target user's credentials.
# IMPORTANT: Must be an *administrator*
$cred = Get-Credential
# The working directory for both the intermediate non-elevated
# and the ultimate elevated process.
# IMPORTANT: This must be a directory that the target user is permitted to access,
#            even when non-elevated.
#            Here, the target user's local profile folder is used.
#            Adjust as needed.
$workingDir = Join-Path (Split-Path $env:USERPROFILE) $cred.UserName
(Start-Process -PassThru -WorkingDirectory $workingDir -Credential $cred -WindowStyle Hidden powershell.exe @'
-noprofile -command Start-Process -Wait -Verb RunAs powershell "
-noexit -command `"Set-Location -LiteralPath `"$($PWD.ProviderPath)`"; & { whoami; net session } 2>&1 > out.txt`"
"
'@).WaitForExit()

PowerShell 远程处理 (WinRM) 的上下文中以其他用户身份启动进程

您自己发现了这个答案,它解释了CreateProcessWithLogon()Windows API 函数 - .NET(以及 PowerShell)在以其他用户身份启动进程时使用的 - 在批处理登录方案中不起作用,如WinRM等服务使用。相反,需要调用CreateProcessAsUser(),可以传递事先使用LogonUser()显式创建的批处理登录用户令牌。

以下自包含示例基于此基于 C# 的答案构建,但存在重要的先决条件和限制

  • 必须向调用用户帐户授予SE_ASSIGNPRIMARYTOKEN_NAME(又名"SeAssignPrimaryTokenPrivilege"又名"替换进程级别令牌"权限)。

    • 以交互方式,您可以使用secpol.msc修改用户权限(Local Policy > User Rights Assignment);修改后,需要注销/重新启动。
    • 如果呼叫者缺少此权限,您将收到一条错误消息,指出A required privilege is not held by the client.
  • 目标用户帐户必须是Administrators组的成员。

    • 如果目标用户不在该组中,您将收到一条错误消息,指出The handle is invalid.
  • 不会尝试在内存中捕获目标进程的输出;相反,进程调用cmd.exe并使用其重定向运算符(>)将输出发送到文件

# Abort on all errors.
$ErrorActionPreference = 'Stop'
Write-Verbose -Verbose 'Compiling C# helper code...'
Add-Type @'
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
public class ProcessHelper
{
static ProcessHelper()
{
UserToken = IntPtr.Zero;
}
private static IntPtr UserToken { get; set; }
// Launch and return right away, with the process ID.
// CAVEAT: While you CAN get a process object with Get-Process -Id <pidReturned>, and
//         waiting for the process to exit with .WaitForExit() does work,
//         you WON'T BE ABLE TO QUERY THE EXIT CODE: 
//         In PowerShell, .ExitCode returns $null, suggesting that an exception occurs,
//         which PowerShell swallows. 
//         https://learn.microsoft.com/en-US/dotnet/api/System.Diagnostics.Process.ExitCode lists only two
//         exception-triggering conditions, *neither of which apply here*: the process not having exited yet, the process
//         object referring to a process on a remote computer.
//         Presumably, the issue is related to missing the PROCESS_QUERY_LIMITED_INFORMATION access right on the process
//         handle due to the process belonging to a different user. 
public int StartProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
UInt32 processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hProcess);
Native.CloseHandle(processInfo.hThread);
return (int) processId;
}
// Launch, wait for termination, return the process exit code.
public int RunProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
UInt32 processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hThread);
// Wait for termination.
if (Native.WAIT_OBJECT_0 != Native.WaitForSingleObject(processInfo.hProcess, Native.INFINITE)) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}

// Get the exit code
UInt32 dwExitCode;
if (! Native.GetExitCodeProcess(processInfo.hProcess, out dwExitCode)) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Native.CloseHandle(processInfo.hProcess);
return (int) dwExitCode;
}
// Log in as the target user and save the logon token in an instance variable.
private static void LogInOtherUser(ProcessStartInfo processStartInfo)
{
if (UserToken == IntPtr.Zero)
{
IntPtr tempUserToken = IntPtr.Zero;
string password = SecureStringToString(processStartInfo.Password);
bool loginResult = Native.LogonUser(processStartInfo.UserName, processStartInfo.Domain, password,
Native.LOGON32_LOGON_BATCH, Native.LOGON32_PROVIDER_DEFAULT,
ref tempUserToken);
if (loginResult)
{
UserToken = tempUserToken;
}
else
{
Native.CloseHandle(tempUserToken);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
private static String SecureStringToString(SecureString value)
{
IntPtr stringPointer = Marshal.SecureStringToBSTR(value);
try
{
return Marshal.PtrToStringBSTR(stringPointer);
}
finally
{
Marshal.FreeBSTR(stringPointer);
}
}
public static void ReleaseUserToken()
{
Native.CloseHandle(UserToken);
}
}
internal class Native
{
internal const Int32 LOGON32_LOGON_BATCH = 4;
internal const Int32 LOGON32_PROVIDER_DEFAULT = 0;
internal const UInt32 INFINITE = 4294967295;
internal const UInt32 WAIT_OBJECT_0 = 0x00000000;
internal const UInt32 WAIT_TIMEOUT = 0x00000102;
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public UInt32 dwProcessId;
public UInt32 dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
internal struct STARTUPINFO
{
public int cb;
[MarshalAs(UnmanagedType.LPStr)]
public string lpReserved;
[MarshalAs(UnmanagedType.LPStr)]
public string lpDesktop;
[MarshalAs(UnmanagedType.LPStr)]
public string lpTitle;
public UInt32 dwX;
public UInt32 dwY;
public UInt32 dwXSize;
public UInt32 dwYSize;
public UInt32 dwXCountChars;
public UInt32 dwYCountChars;
public UInt32 dwFillAttribute;
public UInt32 dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public UInt32 nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("advapi32.dll", SetLastError = true)]
internal extern static bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("advapi32.dll", SetLastError = true)]
internal extern static bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, 
string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes, bool bInheritHandle, uint dwCreationFlags, IntPtr lpEnvironment,
string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, 
out PROCESS_INFORMATION lpProcessInformation);      
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);    

[DllImport("kernel32.dll", SetLastError = true)]
internal extern static bool GetExitCodeProcess(IntPtr hProcess, out UInt32 lpExitCode);
}
'@
# Determine the path for the file in which process output will be captured.
$tmpFileOutput = 'C:UsersPublictmp.txt'
if (Test-Path -LiteralPath $tmpFileOutput) { Remove-Item -Force $tmpFileOutput }
$cred = Get-Credential -Message "Please specify the credentials for the user to run as:"
Write-Verbose -Verbose "Running process as user `"$($cred.UserName)`"..."
# !! If this fails with "The handle is invalid", there are two possible reasons:
# !!  - The credentials are invalid.
# !!  - The target user isn't in the Administrators groups.
$exitCode = [ProcessHelper]::new().RunProcess(
[System.Diagnostics.ProcessStartInfo] @{
# !! CAVEAT: *Full* path required.
FileName = 'C:WINDOWSsystem32cmd.exe'
# !! CAVEAT: While whoami.exe correctly reflects the target user, the per-use *environment variables*
# !!         %USERNAME% and %USERPROFILE% still reflect the *caller's* values.
Arguments = '/c "(echo Hi from & whoami & echo at %TIME%) > {0} 2>&1"' -f $tmpFileOutput
UserName = $cred.UserName
Password = $cred.Password
}
)
Write-Verbose -Verbose "Process exited with exit code $exitCode."
Write-Verbose -Verbose "Output from test file created by the process:"
Get-Content $tmpFileOutput
Remove-Item -Force -ErrorAction Ignore $tmpFileOutput # Clean up.

[1] 进程重定向的标准流的输出被缓冲,当缓冲区填满时,进程被阻止写入更多数据,在这种情况下,它必须等待流的读取器使用缓冲区。因此,如果您尝试同步读取到一个流的末尾,如果另一个流的缓冲区在此期间填满,您可能会卡住,从而阻止进程完成对第一个流的写入。

相关内容

  • 没有找到相关文章

最新更新