mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 03:11:24 -07:00
win_become: another option to support become flags for runas (#34551)
* win_become: another option to support become flags for runas * removed uneeded entries * fixed up whitespace issue * Copy edit
This commit is contained in:
parent
1c22d82c5e
commit
d0e6889f93
5 changed files with 318 additions and 69 deletions
|
@ -604,9 +604,14 @@ namespace Ansible
|
|||
private static extern int ResumeThread(
|
||||
SafeHandle hThread);
|
||||
|
||||
public static CommandResult RunAsUser(string username, string password, string lpCommandLine, string lpCurrentDirectory, string stdinInput)
|
||||
public static CommandResult RunAsUser(string username, string password, string lpCommandLine,
|
||||
string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType)
|
||||
{
|
||||
SecurityIdentifier account = GetBecomeSid(username);
|
||||
SecurityIdentifier account = null;
|
||||
if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
|
||||
{
|
||||
account = GetBecomeSid(username);
|
||||
}
|
||||
|
||||
STARTUPINFOEX si = new STARTUPINFOEX();
|
||||
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
|
||||
|
@ -649,14 +654,14 @@ namespace Ansible
|
|||
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
||||
|
||||
// Get the user tokens to try running processes with
|
||||
List<IntPtr> tokens = GetUserTokens(account, username, password);
|
||||
List<IntPtr> tokens = GetUserTokens(account, username, password, logonType);
|
||||
|
||||
bool launch_success = false;
|
||||
foreach (IntPtr token in tokens)
|
||||
{
|
||||
if (CreateProcessWithTokenW(
|
||||
token,
|
||||
LogonFlags.LOGON_WITH_PROFILE,
|
||||
logonFlags,
|
||||
null,
|
||||
new StringBuilder(lpCommandLine),
|
||||
startup_flags,
|
||||
|
@ -729,7 +734,7 @@ namespace Ansible
|
|||
}
|
||||
}
|
||||
|
||||
private static List<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password)
|
||||
private static List<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType)
|
||||
{
|
||||
List<IntPtr> tokens = new List<IntPtr>();
|
||||
List<String> service_sids = new List<String>()
|
||||
|
@ -739,16 +744,20 @@ namespace Ansible
|
|||
"S-1-5-20" // NT AUTHORITY\NetworkService
|
||||
};
|
||||
|
||||
GrantAccessToWindowStationAndDesktop(account);
|
||||
string account_sid = account.ToString();
|
||||
IntPtr hSystemToken = IntPtr.Zero;
|
||||
string account_sid = "";
|
||||
if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
|
||||
{
|
||||
GrantAccessToWindowStationAndDesktop(account);
|
||||
// Try to get SYSTEM token handle so we can impersonate to get full admin token
|
||||
hSystemToken = GetSystemUserHandle();
|
||||
account_sid = account.ToString();
|
||||
}
|
||||
bool impersonated = false;
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr hSystemTokenDup = IntPtr.Zero;
|
||||
|
||||
// Try to get SYSTEM token handle so we can impersonate to get full admin token
|
||||
IntPtr hSystemToken = GetSystemUserHandle();
|
||||
if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid))
|
||||
{
|
||||
// We need the SYSTEM token if we want to become one of those accounts, fail here
|
||||
|
@ -780,12 +789,11 @@ namespace Ansible
|
|||
// might get a limited token in UAC-enabled cases, but better than nothing...
|
||||
}
|
||||
|
||||
LogonType logonType;
|
||||
string domain = null;
|
||||
|
||||
if (service_sids.Contains(account_sid))
|
||||
{
|
||||
// We're using a well-known service account, do a service logon instead of interactive
|
||||
// We're using a well-known service account, do a service logon instead of the actual flag set
|
||||
logonType = LogonType.LOGON32_LOGON_SERVICE;
|
||||
domain = "NT AUTHORITY";
|
||||
password = null;
|
||||
|
@ -805,7 +813,6 @@ namespace Ansible
|
|||
else
|
||||
{
|
||||
// We are trying to become a local or domain account
|
||||
logonType = LogonType.LOGON32_LOGON_INTERACTIVE;
|
||||
if (username.Contains(@"\"))
|
||||
{
|
||||
var user_split = username.Split(Convert.ToChar(@"\"));
|
||||
|
@ -876,7 +883,6 @@ namespace Ansible
|
|||
TokenAccessLevels.AssignPrimary |
|
||||
TokenAccessLevels.Impersonate;
|
||||
|
||||
// TODO: Find out why I can't see processes from Network Service and Local Service
|
||||
if (OpenProcessToken(hProcess, desired_access, out hToken))
|
||||
{
|
||||
string sid = GetTokenUserSID(hToken);
|
||||
|
@ -1144,16 +1150,84 @@ Function Dump-Error ($excep) {
|
|||
$eo.exception = $excep | Out-String
|
||||
$host.SetShouldExit(1)
|
||||
|
||||
$eo | ConvertTo-Json -Depth 10
|
||||
$eo | ConvertTo-Json -Depth 10 -Compress
|
||||
}
|
||||
|
||||
Function Parse-EnumValue($enum, $flag_type, $value, $prefix) {
|
||||
$raw_enum_value = "$prefix$($value.ToUpper())"
|
||||
try {
|
||||
$enum_value = [Enum]::Parse($enum, $raw_enum_value)
|
||||
} catch [System.ArgumentException] {
|
||||
$valid_options = [Enum]::GetNames($enum) | ForEach-Object { $_.Substring($prefix.Length).ToLower() }
|
||||
throw "become_flags $flag_type value '$value' is not valid, valid values are: $($valid_options -join ", ")"
|
||||
}
|
||||
return $enum_value
|
||||
}
|
||||
|
||||
Function Parse-BecomeFlags($flags) {
|
||||
$logon_type = [Ansible.LogonType]::LOGON32_LOGON_INTERACTIVE
|
||||
$logon_flags = [Ansible.LogonFlags]::LOGON_WITH_PROFILE
|
||||
|
||||
if ($flags -eq $null -or $flags -eq "") {
|
||||
$flag_split = @()
|
||||
} elseif ($flags -is [string]) {
|
||||
$flag_split = $flags.Split(" ")
|
||||
} else {
|
||||
throw "become_flags must be a string, was $($flags.GetType())"
|
||||
}
|
||||
|
||||
foreach ($flag in $flag_split) {
|
||||
$split = $flag.Split("=")
|
||||
if ($split.Count -ne 2) {
|
||||
throw "become_flags entry '$flag' is in an invalid format, must be a key=value pair"
|
||||
}
|
||||
$flag_key = $split[0]
|
||||
$flag_value = $split[1]
|
||||
if ($flag_key -eq "logon_type") {
|
||||
$enum_details = @{
|
||||
enum = [Ansible.LogonType]
|
||||
flag_type = $flag_key
|
||||
value = $flag_value
|
||||
prefix = "LOGON32_LOGON_"
|
||||
}
|
||||
$logon_type = Parse-EnumValue @enum_details
|
||||
} elseif ($flag_key -eq "logon_flags") {
|
||||
$logon_flag_values = $flag_value.Split(",")
|
||||
$logon_flags = 0 -as [Ansible.LogonFlags]
|
||||
foreach ($logon_flag_value in $logon_flag_values) {
|
||||
if ($logon_flag_value -eq "") {
|
||||
continue
|
||||
}
|
||||
$enum_details = @{
|
||||
enum = [Ansible.LogonFlags]
|
||||
flag_type = $flag_key
|
||||
value = $logon_flag_value
|
||||
prefix = "LOGON_"
|
||||
}
|
||||
$logon_flag = Parse-EnumValue @enum_details
|
||||
$logon_flags = $logon_flags -bor $logon_flag
|
||||
}
|
||||
} else {
|
||||
throw "become_flags key '$flag_key' is not a valid runas flag, must be 'logon_type' or 'logon_flags'"
|
||||
}
|
||||
}
|
||||
|
||||
return $logon_type, [Ansible.LogonFlags]$logon_flags
|
||||
}
|
||||
|
||||
Function Run($payload) {
|
||||
# NB: action popping handled inside subprocess wrapper
|
||||
|
||||
Add-Type -TypeDefinition $helper_def -Debug:$false
|
||||
|
||||
$username = $payload.become_user
|
||||
$password = $payload.become_password
|
||||
|
||||
Add-Type -TypeDefinition $helper_def -Debug:$false
|
||||
try {
|
||||
$logon_type, $logon_flags = Parse-BecomeFlags -flags $payload.become_flags
|
||||
} catch {
|
||||
Dump-Error -excep $_
|
||||
return $null
|
||||
}
|
||||
|
||||
# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via filesystem
|
||||
$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".ps1")
|
||||
|
@ -1161,24 +1235,29 @@ Function Run($payload) {
|
|||
$rc = 0
|
||||
|
||||
Try {
|
||||
# allow (potentially unprivileged) target user access to the tempfile (NB: this likely won't work if traverse checking is enabled)
|
||||
$acl = Get-Acl $temp
|
||||
# do not modify the ACL if the logon_type is LOGON32_LOGON_NEW_CREDENTIALS
|
||||
# as this results in the local execution running under the same user's token,
|
||||
# otherwise we need to allow (potentially unprivileges) the become user access
|
||||
# to the tempfile (NB: this likely won't work if traverse checking is enaabled).
|
||||
if ($logon_type -ne [Ansible.LogonType]::LOGON32_LOGON_NEW_CREDENTIALS) {
|
||||
$acl = Get-Acl -Path $temp
|
||||
|
||||
Try {
|
||||
$acl.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule($username, "FullControl", "Allow")))
|
||||
Try {
|
||||
$acl.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule($username, "FullControl", "Allow")))
|
||||
} Catch [System.Security.Principal.IdentityNotMappedException] {
|
||||
throw "become_user '$username' is not recognized on this host"
|
||||
} Catch {
|
||||
throw "failed to set ACL on temp become execution script: $($_.Exception.Message)"
|
||||
}
|
||||
Set-Acl -Path $temp -AclObject $acl | Out-Null
|
||||
}
|
||||
Catch [System.Security.Principal.IdentityNotMappedException] {
|
||||
throw "become_user '$username' is not recognized on this host"
|
||||
}
|
||||
|
||||
Set-Acl $temp $acl | Out-Null
|
||||
|
||||
$payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
|
||||
|
||||
$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -File $temp")
|
||||
$lp_current_directory = "$env:SystemRoot"
|
||||
|
||||
$result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $payload_string)
|
||||
$result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $payload_string, $logon_flags, $logon_type)
|
||||
$stdout = $result.StandardOut
|
||||
$stderr = $result.StandardError
|
||||
$rc = $result.ExitCode
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue