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:
Jordan Borean 2018-01-20 07:58:10 +10:00 committed by Matt Davis
parent 1c22d82c5e
commit d0e6889f93
5 changed files with 318 additions and 69 deletions

View file

@ -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