Skip to content

Commit

Permalink
win_become: another option to support become flags for runas (ansible…
Browse files Browse the repository at this point in the history
…#34551)

* win_become: another option to support become flags for runas

* removed uneeded entries

* fixed up whitespace issue

* Copy edit
  • Loading branch information
jborean93 authored and nitzmahone committed Jan 19, 2018
1 parent 1c22d82 commit d0e6889
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 70 deletions.
77 changes: 75 additions & 2 deletions docs/docsite/rst/become.rst
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,79 @@ or with this Ansible task:
to set the account's password under ``ansible_become_pass`` if the
become_user has a password.

Become Flags
------------
Ansible 2.5 adds the ``become_flags`` parameter to the ``runas`` become method. This parameter can be set using the ``become_flags`` task directive or set in Ansible's configuration using ``ansible_become_flags``. The two valid values that are initially supported for this parameter are ``logon_type`` and ``logon_flags``.


.. Note:: These flags should only be set when becoming a normal user account, not a local service account like LocalSystem.

The key ``logon_type`` sets the type of logon operation to perform. The value
can be set to one of the following:

* ``interactive``: The default logon type. The process will be run under a
context that is the same as when running a process locally. This bypasses all
WinRM restrictions and is the recommended method to use.

* ``batch``: Runs the process under a batch context that is similar to a
scheduled task with a password set. This should bypass most WinRM
restrictions and is useful if the ``become_user`` is not allowed to log on
interactively.

* ``new_credentials``: Runs under the same credentials as the calling user, but
outbound connections are run under the context of the ``become_user`` and
``become_password``, similar to ``runas.exe /netonly``. The ``logon_flags``
flag should also be set to ``netcredentials_only``. Use this flag if
the process needs to access a network resource (like an SMB share) using a
different set of credentials.

* ``network``: Runs the process under a network context without any cached
credentials. This results in the same type of logon session as running a
normal WinRM process without credential delegation, and operates under the same
restrictions.

* ``network_cleartext``: Like the ``network`` logon type, but instead caches
the credentials so it can access network resources. This is the same type of
logon session as running a normal WinRM process with credential delegation.

For more information, see
`dwLogonType <https://msdn.microsoft.com/en-au/library/windows/desktop/aa378184.aspx>`_.

The ``logon_flags`` key specifies how Windows will log the user on when creating
the new process. The value can be set to one of the following:

* ``with_profile``: The default logon flag set. The process will load the
user's profile in the ``HKEY_USERS`` registry key to ``HKEY_CURRENT_USER``.

* ``netcredentials_only``: The process will use the same token as the caller
but will use the ``become_user`` and ``become_password`` when accessing a remote
resource. This is useful in inter-domain scenarios where there is no trust
relationship, and should be used with the ``new_credentials`` ``logon_type``.

For more information, see `dwLogonFlags <https://msdn.microsoft.com/en-us/library/windows/desktop/ms682434.aspx>`_.

Here are some examples of how to use ``become_flags`` with Windows tasks:

.. code-block:: yaml
- name: copy a file from a fileshare with custom credentials
win_copy:
src: \\server\share\data\file.txt
dest: C:\temp\file.txt
remote_src: yex
vars:
ansible_become: yes
ansible_become_method: runas
ansible_become_user: DOMAIN\user
ansible_become_pass: Password01
ansible_become_flags: logon_type=new_credentials logon_flags=netcredentials_only
- name: run a command under a batch logon
win_command: whoami
become: yes
become_flags: logon_type=batch
Limitations
-----------

Expand All @@ -457,8 +530,8 @@ Be aware of the following limitations with ``become`` on Windows:
* Running a task with ``async`` and ``become`` on Windows Server 2008, 2008 R2
and Windows 7 does not work.

* The become user logs on with an interactive session, so it must have the
ability to do so on the Windows host. If it does not inherit the
* By default, the become user logs on with an interactive session, so it must
have the right to do so on the Windows host. If it does not inherit the
``SeAllowLogOnLocally`` privilege or inherits the ``SeDenyLogOnLocally``
privilege, the become process will fail.

Expand Down
8 changes: 5 additions & 3 deletions lib/ansible/executor/module_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ def _is_binary(b_module_data):


def _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression, async_timeout, become,
become_method, become_user, become_password, environment):
become_method, become_user, become_password, become_flags, environment):
"""
Given the source of the module, convert it to a Jinja2 template to insert
module code and return whether it's a new or old style module.
Expand Down Expand Up @@ -787,6 +787,7 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
exec_manifest["actions"].insert(0, 'become')
exec_manifest["become_user"] = become_user
exec_manifest["become_password"] = become_password
exec_manifest['become_flags'] = become_flags
exec_manifest["become"] = to_text(base64.b64encode(to_bytes(become_wrapper)))

lines = b_module_data.split(b'\n')
Expand Down Expand Up @@ -842,6 +843,7 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
exec_manifest["actions"].insert(0, 'become')
exec_manifest["become_user"] = "SYSTEM"
exec_manifest["become_password"] = None
exec_manifest['become_flags'] = None
exec_manifest["become"] = to_text(base64.b64encode(to_bytes(become_wrapper)))

# FUTURE: smuggle this back as a dict instead of serializing here; the connection plugin may need to modify it
Expand Down Expand Up @@ -872,7 +874,7 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas


def modify_module(module_name, module_path, module_args, task_vars=None, templar=None, module_compression='ZIP_STORED', async_timeout=0, become=False,
become_method=None, become_user=None, become_password=None, environment=None):
become_method=None, become_user=None, become_password=None, become_flags=None, environment=None):
"""
Used to insert chunks of code into modules before transfer rather than
doing regular python imports. This allows for more efficient transfer in
Expand Down Expand Up @@ -903,7 +905,7 @@ def modify_module(module_name, module_path, module_args, task_vars=None, templar

(b_module_data, module_style, shebang) = _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression,
async_timeout=async_timeout, become=become, become_method=become_method,
become_user=become_user, become_password=become_password,
become_user=become_user, become_password=become_password, become_flags=become_flags,
environment=environment)

if module_style == 'binary':
Expand Down
1 change: 1 addition & 0 deletions lib/ansible/plugins/action/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def _configure_module(self, module_name, module_args, task_vars=None):
become_method=self._play_context.become_method,
become_user=self._play_context.become_user,
become_password=self._play_context.become_pass,
become_flags=self._play_context.become_flags,
environment=final_environment)

return (module_style, module_shebang, module_data, module_path)
Expand Down
135 changes: 107 additions & 28 deletions lib/ansible/plugins/shell/powershell.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,9 +604,14 @@ class NativeWaitHandle : WaitHandle
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;
Expand Down Expand Up @@ -649,14 +654,14 @@ class NativeWaitHandle : WaitHandle
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,
Expand Down Expand Up @@ -729,7 +734,7 @@ class NativeWaitHandle : WaitHandle
}
}
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>()
Expand All @@ -739,16 +744,20 @@ class NativeWaitHandle : WaitHandle
"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
Expand Down Expand Up @@ -780,12 +789,11 @@ class NativeWaitHandle : WaitHandle
// 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;
Expand All @@ -805,7 +813,6 @@ class NativeWaitHandle : WaitHandle
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(@"\"));
Expand Down Expand Up @@ -876,7 +883,6 @@ class NativeWaitHandle : WaitHandle
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);
Expand Down Expand Up @@ -1144,41 +1150,114 @@ class NativeWaitHandle : WaitHandle
$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")
$exec_wrapper.ToString() | Set-Content -Path $temp
$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
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"
# 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")))
} 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
}
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
Expand Down
Loading

0 comments on commit d0e6889

Please sign in to comment.