psexec: new module to run commands on a remote Windows host without WinRM (#36723)
* psexec: new module to run commands on a remote Windows host without WinRM * fix up sanity issue, create test firewall rule for SMB traffic * Fixed up yaml linting issues, trying to fix on the fly firewall rule * Added SMB exception to catch when cleaning up PAExec exe * Don't load profile for Azure hosts when becoming another user * Fixed up example to use correct option * Reworded notes section of module docs * Simplified module options around process integrity levels and the system account
This commit is contained in:
parent
e254121729
commit
629efb6eaa
4 changed files with 776 additions and 0 deletions
494
lib/ansible/modules/commands/psexec.py
Normal file
494
lib/ansible/modules/commands/psexec.py
Normal file
|
@ -0,0 +1,494 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2018, Jordan Borean <jborean93@gmail.com>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {
|
||||||
|
'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: psexec
|
||||||
|
short_description: Runs commands on a remote Windows host based on the PsExec
|
||||||
|
model
|
||||||
|
version_added: "2.6"
|
||||||
|
description:
|
||||||
|
- Runs a remote command from a Linux host to a Windows host without WinRM being
|
||||||
|
set up.
|
||||||
|
- Can be run on the Ansible controller to bootstrap Windows hosts to get them
|
||||||
|
ready for WinRM.
|
||||||
|
options:
|
||||||
|
hostname:
|
||||||
|
description:
|
||||||
|
- The remote Windows host to connect to, can be either an IP address or a
|
||||||
|
hostname.
|
||||||
|
required: yes
|
||||||
|
connection_username:
|
||||||
|
description:
|
||||||
|
- The username to use when connecting to the remote Windows host.
|
||||||
|
- This user must be a member of the C(Administrators) group of the Windows
|
||||||
|
host.
|
||||||
|
- Required if the Kerberos requirements are not installed or the username
|
||||||
|
is a local account to the Windows host.
|
||||||
|
- Can be omitted to use the default Kerberos principal ticket in the
|
||||||
|
local credential cache if the Kerberos library is installed.
|
||||||
|
- If I(process_username) is not specified, then the remote process will run
|
||||||
|
under a Network Logon under this account.
|
||||||
|
connection_password:
|
||||||
|
description:
|
||||||
|
- The password for I(connection_user).
|
||||||
|
- Required if the Kerberos requirements are not installed or the username
|
||||||
|
is a local account to the Windows host.
|
||||||
|
- Can be omitted to use a Kerberos principal ticket for the principal set
|
||||||
|
by I(connection_user) if the Kerberos library is installed and the
|
||||||
|
ticket has already been retrieved with the C(kinit) command before.
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- The port that the remote SMB service is listening on.
|
||||||
|
default: 445
|
||||||
|
encrypt:
|
||||||
|
description:
|
||||||
|
- Will use SMB encryption to encrypt the SMB messages sent to and from the
|
||||||
|
host.
|
||||||
|
- This requires the SMB 3 protocol which is only supported from Windows
|
||||||
|
Server 2012 or Windows 8, older versions like Windows 7 or Windows Server
|
||||||
|
2008 (R2) must set this to C(no) and use no encryption.
|
||||||
|
- When setting to C(no), the packets are in plaintext and can be seen by
|
||||||
|
anyone sniffing the network, any process options are included in this.
|
||||||
|
type: bool
|
||||||
|
default: 'yes'
|
||||||
|
connection_timeout:
|
||||||
|
description:
|
||||||
|
- The timeout in seconds to wait when receiving the initial SMB negotiate
|
||||||
|
response from the server.
|
||||||
|
default: 60
|
||||||
|
executable:
|
||||||
|
description:
|
||||||
|
- The executable to run on the Windows host.
|
||||||
|
required: yes
|
||||||
|
arguments:
|
||||||
|
description:
|
||||||
|
- Any arguments as a single string to use when running the executable.
|
||||||
|
working_directory:
|
||||||
|
description:
|
||||||
|
- Changes the working directory set when starting the process.
|
||||||
|
default: C:\Windows\System32
|
||||||
|
asynchronous:
|
||||||
|
description:
|
||||||
|
- Will run the command as a detached process and the module returns
|
||||||
|
immediately after starting the processs while the process continues to
|
||||||
|
run in the background.
|
||||||
|
- The I(stdout) and I(stderr) return values will be null when this is set
|
||||||
|
to C(yes).
|
||||||
|
- The I(stdin) option does not work with this type of process.
|
||||||
|
- The I(rc) return value is not set when this is C(yes)
|
||||||
|
type: bool
|
||||||
|
default: 'no'
|
||||||
|
load_profile:
|
||||||
|
description:
|
||||||
|
- Runs the remote command with the user's profile loaded.
|
||||||
|
type: bool
|
||||||
|
default: 'yes'
|
||||||
|
process_username:
|
||||||
|
description:
|
||||||
|
- The user to run the process as.
|
||||||
|
- This can be set to run the process under an Interactive logon of the
|
||||||
|
specified account which bypasses limitations of a Network logon used when
|
||||||
|
this isn't specified.
|
||||||
|
- If omitted then the process is run under the same account as
|
||||||
|
I(connection_username) with a Network logon.
|
||||||
|
- Set to C(System) to run as the builtin SYSTEM account, no password is
|
||||||
|
required with this account.
|
||||||
|
- If I(encrypt) is C(no), the username and password are sent as a simple
|
||||||
|
XOR scrambled byte string that is not encrypted. No special tools are
|
||||||
|
required to get the username and password just knowledge of the protocol.
|
||||||
|
process_password:
|
||||||
|
description:
|
||||||
|
- The password for I(process_username).
|
||||||
|
- Required if I(process_username) is defined and not C(System).
|
||||||
|
integrity_level:
|
||||||
|
description:
|
||||||
|
- The integrity level of the process when I(process_username) is defined
|
||||||
|
and is not equal to C(System).
|
||||||
|
- When C(default), the default integrity level based on the system setup.
|
||||||
|
- When C(elevated), the command will be run with Administrative rights.
|
||||||
|
- When C(limited), the command will be forced to run with
|
||||||
|
non-Administrative rights.
|
||||||
|
choices:
|
||||||
|
- limited
|
||||||
|
- default
|
||||||
|
- elevated
|
||||||
|
default: default
|
||||||
|
interactive:
|
||||||
|
description:
|
||||||
|
- Will run the process as an interactive process that shows a process
|
||||||
|
Window of the Windows session specified by I(interactive_session).
|
||||||
|
- The I(stdout) and I(stderr) return values will be null when this is set
|
||||||
|
to C(yes).
|
||||||
|
- The I(stdin) option does not work with this type of process.
|
||||||
|
type: bool
|
||||||
|
default: 'no'
|
||||||
|
interactive_session:
|
||||||
|
description:
|
||||||
|
- The Windows session ID to use when displaying the interactive process on
|
||||||
|
the remote Windows host.
|
||||||
|
- This is only valid when I(interactive) is C(yes).
|
||||||
|
- The default is C(0) which is the console session of the Windows host.
|
||||||
|
default: 0
|
||||||
|
priority:
|
||||||
|
description:
|
||||||
|
- Set the command's priority on the Windows host.
|
||||||
|
- See U(https://msdn.microsoft.com/en-us/library/windows/desktop/ms683211.aspx)
|
||||||
|
for more details.
|
||||||
|
choices:
|
||||||
|
- above_normal
|
||||||
|
- below_normal
|
||||||
|
- high
|
||||||
|
- idle
|
||||||
|
- normal
|
||||||
|
- realtime
|
||||||
|
default: normal
|
||||||
|
show_ui_on_logon_screen:
|
||||||
|
description:
|
||||||
|
- Shows the process UI on the Winlogon secure desktop when
|
||||||
|
I(process_username) is C(System).
|
||||||
|
type: bool
|
||||||
|
default: 'no'
|
||||||
|
process_timeout:
|
||||||
|
description:
|
||||||
|
- The timeout in seconds that is placed upon the running process.
|
||||||
|
- A value of C(0) means no timeout.
|
||||||
|
default: 0
|
||||||
|
stdin:
|
||||||
|
description:
|
||||||
|
- Data to send on the stdin pipe once the process has started.
|
||||||
|
- This option has no effect when I(interactive) or I(asynchronous) is
|
||||||
|
C(yes).
|
||||||
|
requirements:
|
||||||
|
- pypsexec
|
||||||
|
- smbprotocol[kerberos] for optional Kerberos authentication
|
||||||
|
notes:
|
||||||
|
- This module requires the Windows host to have SMB configured and enabled,
|
||||||
|
and port 445 opened on the firewall.
|
||||||
|
- This module will wait until the process is finished unless I(asynchronous)
|
||||||
|
is C(yes), ensure the process is run as a non-interactive command to avoid
|
||||||
|
infinite hangs waiting for input.
|
||||||
|
- The I(connection_username) must be a member of the local Administrator group
|
||||||
|
of the Windows host. For non-domain joined hosts, the
|
||||||
|
C(LocalAccountTokenFilterPolicy) should be set to C(1) to ensure this works,
|
||||||
|
see U(https://support.microsoft.com/en-us/help/951016/description-of-user-account-control-and-remote-restrictions-in-windows).
|
||||||
|
- For more information on this module and the various host requirements, see
|
||||||
|
U(https://github.com/jborean93/pypsexec).
|
||||||
|
author:
|
||||||
|
- Jordan Borean (@jborean93)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: run a cmd.exe command
|
||||||
|
psexec:
|
||||||
|
hostname: server
|
||||||
|
connection_username: username
|
||||||
|
connection_password: password
|
||||||
|
executable: cmd.exe
|
||||||
|
arguments: /c echo Hello World
|
||||||
|
|
||||||
|
- name: run a PowerShell command
|
||||||
|
psexec:
|
||||||
|
hostname: server.domain.local
|
||||||
|
connection_username: username@DOMAIN.LOCAL
|
||||||
|
connection_password: password
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: Write-Host Hello World
|
||||||
|
|
||||||
|
- name: send data through stdin
|
||||||
|
psexec:
|
||||||
|
hostname: 192.168.1.2
|
||||||
|
connection_username: username
|
||||||
|
connection_password: password
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: '-'
|
||||||
|
stdin: |
|
||||||
|
Write-Host Hello World
|
||||||
|
Write-Error Error Message
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
- name: Run the process as a different user
|
||||||
|
psexec:
|
||||||
|
hostname: server
|
||||||
|
connection_user: username
|
||||||
|
connection_password: password
|
||||||
|
executable: whoami.exe
|
||||||
|
arguments: /all
|
||||||
|
process_username: anotheruser
|
||||||
|
process_password: anotherpassword
|
||||||
|
|
||||||
|
- name: Run the process asynchronously
|
||||||
|
psexec:
|
||||||
|
hostname: server
|
||||||
|
connection_username: username
|
||||||
|
connection_password: password
|
||||||
|
executable: cmd.exe
|
||||||
|
arguments: /c rmdir C:\temp
|
||||||
|
asynchronous: yes
|
||||||
|
|
||||||
|
- name: Use Kerberos authentication for the connection (requires smbprotocol[kerberos])
|
||||||
|
psexec:
|
||||||
|
hostname: host.domain.local
|
||||||
|
connection_username: user@DOMAIN.LOCAL
|
||||||
|
executable: C:\some\path\to\executable.exe
|
||||||
|
arguments: /s
|
||||||
|
|
||||||
|
- name: Disable encryption to work with WIndows 7/Server 2008 (R2)
|
||||||
|
psexec:
|
||||||
|
hostanme: windows-pc
|
||||||
|
connection_username: Administrator
|
||||||
|
connection_password: Password01
|
||||||
|
encrypt: no
|
||||||
|
integrity_level: elevated
|
||||||
|
process_username: Administrator
|
||||||
|
process_password: Password01
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: (New-Object -ComObject Microsoft.Update.Session).CreateUpdateInstaller().IsBusy
|
||||||
|
|
||||||
|
- name: Download and run ConfigureRemotingForAnsible.ps1 to setup WinRM
|
||||||
|
psexec:
|
||||||
|
hostname: windows-pc
|
||||||
|
connection_username: Administrator
|
||||||
|
connection_password: Password01
|
||||||
|
encrypt: yes
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: '-'
|
||||||
|
stdin: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$sec_protocols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault
|
||||||
|
$sec_protocols = $sec_protocols -bor [Net.SecurityProtocolType]::Tls12
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = $sec_protocols
|
||||||
|
$url = "https://github.com/ansible/ansible/raw/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
|
||||||
|
Invoke-Expression ((New-Object Net.WebClient).DownloadString($url))
|
||||||
|
exit
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
msg:
|
||||||
|
description: Any exception details when trying to run the process
|
||||||
|
returned: module failed
|
||||||
|
type: str
|
||||||
|
sample: 'Received exception from remote PAExec service: Failed to start "invalid.exe". The system cannot find the file specified. [Err=0x2, 2]'
|
||||||
|
stdout:
|
||||||
|
description: The stdout from the remote process
|
||||||
|
returned: success and interactive or asynchronous is 'no'
|
||||||
|
type: str
|
||||||
|
sample: Hello World
|
||||||
|
stderr:
|
||||||
|
description: The stderr from the remote process
|
||||||
|
returned: success and interactive or asynchronous is 'no'
|
||||||
|
type: str
|
||||||
|
sample: Error [10] running process
|
||||||
|
pid:
|
||||||
|
description: The process ID of the asynchronous process that was created
|
||||||
|
returned: success and asynchronous is 'yes'
|
||||||
|
type: int
|
||||||
|
sample: 719
|
||||||
|
rc:
|
||||||
|
description: The return code of the remote process
|
||||||
|
returned: success and asynchronous is 'no'
|
||||||
|
type: int
|
||||||
|
sample: 0
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils._text import to_bytes, to_text
|
||||||
|
|
||||||
|
PYPSEXEC_IMP_ERR = None
|
||||||
|
try:
|
||||||
|
from pypsexec import client
|
||||||
|
from pypsexec.exceptions import PypsexecException, PAExecException, \
|
||||||
|
PDUException, SCMRException
|
||||||
|
from pypsexec.paexec import ProcessPriority
|
||||||
|
from smbprotocol.exceptions import SMBException, SMBAuthenticationError, \
|
||||||
|
SMBResponseException
|
||||||
|
HAS_PYPSEXEC = True
|
||||||
|
except ImportError as exc:
|
||||||
|
PYPSEXEC_IMP_ERR = exc
|
||||||
|
HAS_PYPSEXEC = False
|
||||||
|
|
||||||
|
KERBEROS_IMP_ERR = None
|
||||||
|
try:
|
||||||
|
import gssapi
|
||||||
|
# GSSAPI extension required for Kerberos Auth in SMB
|
||||||
|
from gssapi.raw import inquire_sec_context_by_oid
|
||||||
|
HAS_KERBEROS = True
|
||||||
|
except ImportError as exc:
|
||||||
|
KERBEROS_IMP_ERR = exc
|
||||||
|
HAS_KERBEROS = False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_artifacts(module, client):
|
||||||
|
try:
|
||||||
|
client.remove_service()
|
||||||
|
except (SMBException, PypsexecException) as exc:
|
||||||
|
module.warn("Failed to cleanup PAExec service and executable: %s"
|
||||||
|
% to_text(exc))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module_args = dict(
|
||||||
|
hostname=dict(type='str', required=True),
|
||||||
|
connection_username=dict(type='str'),
|
||||||
|
connection_password=dict(type='str', no_log=True),
|
||||||
|
port=dict(type='int', required=False, default=445),
|
||||||
|
encrypt=dict(type='bool', default=True),
|
||||||
|
connection_timeout=dict(type='int', default=60),
|
||||||
|
executable=dict(type='str', required=True),
|
||||||
|
arguments=dict(type='str'),
|
||||||
|
working_directory=dict(type='str', default=r'C:\Windows\System32'),
|
||||||
|
asynchronous=dict(type='bool', default=False),
|
||||||
|
load_profile=dict(type='bool', default=True),
|
||||||
|
process_username=dict(type='str'),
|
||||||
|
process_password=dict(type='str', no_log=True),
|
||||||
|
integrity_level=dict(type='str', default='default',
|
||||||
|
choices=['default', 'elevated', 'limited']),
|
||||||
|
interactive=dict(type='bool', default=False),
|
||||||
|
interactive_session=dict(type='int', default=0),
|
||||||
|
priority=dict(type='str', default='normal',
|
||||||
|
choices=['above_normal', 'below_normal', 'high',
|
||||||
|
'idle', 'normal', 'realtime']),
|
||||||
|
show_ui_on_logon_screen=dict(type='bool', default=False),
|
||||||
|
process_timeout=dict(type='int', default=0),
|
||||||
|
stdin=dict(type='str')
|
||||||
|
)
|
||||||
|
result = dict(
|
||||||
|
changed=False,
|
||||||
|
)
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=module_args,
|
||||||
|
supports_check_mode=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
process_username = module.params['process_username']
|
||||||
|
process_password = module.params['process_password']
|
||||||
|
use_system = False
|
||||||
|
if process_username is not None and process_username.lower() == "system":
|
||||||
|
use_system = True
|
||||||
|
process_username = None
|
||||||
|
process_password = None
|
||||||
|
|
||||||
|
if process_username is not None and process_password is None:
|
||||||
|
module.fail_json(msg='parameters are required together when not '
|
||||||
|
'running as System: process_username, '
|
||||||
|
'process_password')
|
||||||
|
if not HAS_PYPSEXEC:
|
||||||
|
module.fail_json(msg='The pypsexec python module is required',
|
||||||
|
exception=PYPSEXEC_IMP_ERR)
|
||||||
|
|
||||||
|
hostname = module.params['hostname']
|
||||||
|
connection_username = module.params['connection_username']
|
||||||
|
connection_password = module.params['connection_password']
|
||||||
|
port = module.params['port']
|
||||||
|
encrypt = module.params['encrypt']
|
||||||
|
connection_timeout = module.params['connection_timeout']
|
||||||
|
executable = module.params['executable']
|
||||||
|
arguments = module.params['arguments']
|
||||||
|
working_directory = module.params['working_directory']
|
||||||
|
asynchronous = module.params['asynchronous']
|
||||||
|
load_profile = module.params['load_profile']
|
||||||
|
elevated = module.params['integrity_level'] == "elevated"
|
||||||
|
limited = module.params['integrity_level'] == "limited"
|
||||||
|
interactive = module.params['interactive']
|
||||||
|
interactive_session = module.params['interactive_session']
|
||||||
|
|
||||||
|
priority = {
|
||||||
|
"above_normal": ProcessPriority.ABOVE_NORMAL_PRIORITY_CLASS,
|
||||||
|
"below_normal": ProcessPriority.BELOW_NORMAL_PRIORITY_CLASS,
|
||||||
|
"high": ProcessPriority.HIGH_PRIORITY_CLASS,
|
||||||
|
"idle": ProcessPriority.IDLE_PRIORITY_CLASS,
|
||||||
|
"normal": ProcessPriority.NORMAL_PRIORITY_CLASS,
|
||||||
|
"realtime": ProcessPriority.REALTIME_PRIORITY_CLASS
|
||||||
|
}[module.params['priority']]
|
||||||
|
show_ui_on_logon_screen = module.params['show_ui_on_logon_screen']
|
||||||
|
|
||||||
|
process_timeout = module.params['process_timeout']
|
||||||
|
stdin = module.params['stdin']
|
||||||
|
|
||||||
|
if connection_username is None or connection_password is None and \
|
||||||
|
not HAS_KERBEROS:
|
||||||
|
module.fail_json(msg='The gssapi python module with the GGF extension '
|
||||||
|
'is required for Kerberos authentication',
|
||||||
|
exception=KERBEROS_IMP_ERR)
|
||||||
|
|
||||||
|
win_client = client.Client(server=hostname, username=connection_username,
|
||||||
|
password=connection_password, port=port,
|
||||||
|
encrypt=encrypt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
win_client.connect(timeout=connection_timeout)
|
||||||
|
except SMBAuthenticationError as exc:
|
||||||
|
module.fail_json(msg='Failed to authenticate over SMB: %s'
|
||||||
|
% to_text(exc))
|
||||||
|
except SMBResponseException as exc:
|
||||||
|
module.fail_json(msg='Received unexpected SMB response when opening '
|
||||||
|
'the connection: %s' % to_text(exc))
|
||||||
|
except PDUException as exc:
|
||||||
|
module.fail_json(msg='Received an exception with RPC PDU message: %s'
|
||||||
|
% to_text(exc))
|
||||||
|
except SCMRException as exc:
|
||||||
|
module.fail_json(msg='Received an exception when dealing with SCMR on '
|
||||||
|
'the Windows host: %s' % to_text(exc))
|
||||||
|
except (SMBException, PypsexecException) as exc:
|
||||||
|
module.fail_json(msg=to_text(exc))
|
||||||
|
|
||||||
|
# create PAExec service and run the process
|
||||||
|
result['changed'] = True
|
||||||
|
b_stdin = to_bytes(stdin, encoding='utf-8') if stdin else None
|
||||||
|
run_args = dict(
|
||||||
|
executable=executable, arguments=arguments, asynchronous=asynchronous,
|
||||||
|
load_profile=load_profile, interactive_session=interactive_session,
|
||||||
|
run_elevated=elevated, run_limited=limited,
|
||||||
|
username=process_username, password=process_password,
|
||||||
|
use_system_account=use_system, working_dir=working_directory,
|
||||||
|
priority=priority, show_ui_on_win_logon=show_ui_on_logon_screen,
|
||||||
|
timeout_seconds=process_timeout, stdin=b_stdin
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
win_client.create_service()
|
||||||
|
except (SMBException, PypsexecException) as exc:
|
||||||
|
module.fail_json(msg='Failed to create PAExec service: %s'
|
||||||
|
% to_text(exc))
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc_result = win_client.run_executable(**run_args)
|
||||||
|
except (SMBException, PypsexecException) as exc:
|
||||||
|
module.fail_json(msg='Received error when running remote process: %s'
|
||||||
|
% to_text(exc))
|
||||||
|
finally:
|
||||||
|
remove_artifacts(module, win_client)
|
||||||
|
|
||||||
|
if asynchronous:
|
||||||
|
result['pid'] = proc_result[2]
|
||||||
|
elif interactive:
|
||||||
|
result['rc'] = proc_result[2]
|
||||||
|
else:
|
||||||
|
result['stdout'] = proc_result[0]
|
||||||
|
result['stderr'] = proc_result[1]
|
||||||
|
result['rc'] = proc_result[2]
|
||||||
|
|
||||||
|
# close the SMB connection
|
||||||
|
try:
|
||||||
|
win_client.disconnect()
|
||||||
|
except (SMBException, PypsexecException) as exc:
|
||||||
|
module.warn("Failed to close the SMB connection: %s" % to_text(exc))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
2
test/integration/targets/psexec/aliases
Normal file
2
test/integration/targets/psexec/aliases
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
windows/ci/group1
|
||||||
|
|
49
test/integration/targets/psexec/tasks/main.yml
Normal file
49
test/integration/targets/psexec/tasks/main.yml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
- name: check whether the host supports encryption
|
||||||
|
win_shell: |
|
||||||
|
if ([System.Environment]::OSVersion.Version -lt [Version]"6.2") {
|
||||||
|
"false"
|
||||||
|
} else {
|
||||||
|
"true"
|
||||||
|
}
|
||||||
|
register: encryption_supported_raw
|
||||||
|
|
||||||
|
- name: install pypsexec Python library for tests
|
||||||
|
pip:
|
||||||
|
name: pypsexec
|
||||||
|
state: latest
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: define psexec variables
|
||||||
|
set_fact:
|
||||||
|
psexec_hostname: '{{ansible_host}}'
|
||||||
|
psexec_username: '{{ansible_user}}'
|
||||||
|
psexec_password: '{{ansible_password}}'
|
||||||
|
psexec_encrypt: '{{encryption_supported_raw.stdout_lines[0]|bool}}'
|
||||||
|
|
||||||
|
- name: create test rule to allow SMB traffic inbound
|
||||||
|
win_firewall_rule:
|
||||||
|
name: File and Printer Sharing (SMB-In) Test
|
||||||
|
direction: in
|
||||||
|
action: allow
|
||||||
|
localport: 445
|
||||||
|
enabled: yes
|
||||||
|
protocol: tcp
|
||||||
|
program: System
|
||||||
|
profiles:
|
||||||
|
- domain
|
||||||
|
- private
|
||||||
|
- public
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: run tests
|
||||||
|
block:
|
||||||
|
- include_tasks: tests.yml
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: remove test rule that allows SMB traffic inbound
|
||||||
|
win_firewall_rule:
|
||||||
|
name: File and Printer Sharing (SMB-In) Test
|
||||||
|
direction: in
|
||||||
|
action: allow
|
||||||
|
state: absent
|
231
test/integration/targets/psexec/tasks/tests.yml
Normal file
231
test/integration/targets/psexec/tasks/tests.yml
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
---
|
||||||
|
- name: fail when process_password is not set with process_username
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: hostname.exe
|
||||||
|
process_username: '{{psexec_username}}'
|
||||||
|
delegate_to: localhost
|
||||||
|
register: fail_no_process_pass
|
||||||
|
failed_when: 'fail_no_process_pass.msg != "parameters are required together when not running as System: process_username, process_password"'
|
||||||
|
|
||||||
|
- name: get current host
|
||||||
|
win_command: hostname.exe
|
||||||
|
register: actual_hostname
|
||||||
|
|
||||||
|
- name: run basic psexec command
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: hostname.exe
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_hostname_actual
|
||||||
|
|
||||||
|
- name: assert basic psexec command matches expected output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_hostname_actual is changed
|
||||||
|
- psexec_hostname_actual.rc == 0
|
||||||
|
- psexec_hostname_actual.stderr == ''
|
||||||
|
- psexec_hostname_actual.stdout == actual_hostname.stdout
|
||||||
|
|
||||||
|
- name: get output for executable with arguments
|
||||||
|
win_command: hostname.exe /?
|
||||||
|
register: actual_hostname_help
|
||||||
|
failed_when: actual_hostname_help.rc != 1
|
||||||
|
|
||||||
|
- name: run psexec command with arguments
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: hostname.exe
|
||||||
|
arguments: /?
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_hostname_help
|
||||||
|
failed_when: psexec_hostname_help.rc != 1
|
||||||
|
|
||||||
|
- name: assert basic pesexec command with arguments matches expected output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_hostname_help is changed
|
||||||
|
- psexec_hostname_help.rc == 1
|
||||||
|
- psexec_hostname_help.stderr == actual_hostname_help.stderr
|
||||||
|
- psexec_hostname_help.stdout == actual_hostname_help.stdout
|
||||||
|
|
||||||
|
- name: run psexec command and send data through stdin
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: '-'
|
||||||
|
stdin: |
|
||||||
|
Write-Host hello world
|
||||||
|
Write-Host this is another message
|
||||||
|
exit 0
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_stdin
|
||||||
|
|
||||||
|
- name: assert psexec ommand and send data through stdin
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_stdin is changed
|
||||||
|
- psexec_stdin.rc == 0
|
||||||
|
- psexec_stdin.stderr == ''
|
||||||
|
- psexec_stdin.stdout == 'hello world\nthis is another message\n'
|
||||||
|
|
||||||
|
- name: run psexec command with specific process username
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
load_profile: no # on Azure, the profile does not exist yet so we don't load it for this task
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: '-'
|
||||||
|
stdin: |
|
||||||
|
((Get-CimInstance Win32_Process -filter "processid = $pid") | Get-CimAssociatedInstance -Association Win32_SessionProcess).LogonType
|
||||||
|
exit 0
|
||||||
|
process_username: '{{psexec_username}}'
|
||||||
|
process_password: '{{psexec_password}}'
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_username
|
||||||
|
|
||||||
|
- name: assert psexec command with specific process username
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_username is changed
|
||||||
|
- psexec_process_username.rc == 0
|
||||||
|
- psexec_process_username.stderr == ''
|
||||||
|
- psexec_process_username.stdout_lines[0] != '3' # 3 is Network Logon Type, we assert we are not a network logon with process credentials
|
||||||
|
|
||||||
|
- name: run psexec command with both stderr and stdout
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: cmd.exe
|
||||||
|
arguments: /c echo first && echo second 1>&2 && echo third
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_stderr
|
||||||
|
|
||||||
|
- name: assert psexec command with both stderr and stdout
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_stderr is changed
|
||||||
|
- psexec_process_stderr.rc == 0
|
||||||
|
- psexec_process_stderr.stderr == 'second \r\n'
|
||||||
|
- psexec_process_stderr.stdout == 'first \r\nthird\r\n'
|
||||||
|
|
||||||
|
- name: run process asynchronously
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: Start-Sleep -Seconds 30
|
||||||
|
asynchronous: yes
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_async
|
||||||
|
|
||||||
|
- name: check if process is still running
|
||||||
|
win_shell: (Get-Process -ID {{psexec_process_async.pid}}).ProcessName
|
||||||
|
register: psexec_process_async_actual
|
||||||
|
|
||||||
|
- name: assert run process asynchronously
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_async is changed
|
||||||
|
- psexec_process_async.rc is not defined
|
||||||
|
- psexec_process_async.pid is defined
|
||||||
|
- psexec_process_async.stdout is not defined
|
||||||
|
- psexec_process_async.stderr is not defined
|
||||||
|
- psexec_process_async_actual.stdout_lines[0] == 'powershell'
|
||||||
|
|
||||||
|
- name: run process interactively
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: Write-Host hi
|
||||||
|
interactive: yes
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_interactive
|
||||||
|
|
||||||
|
- name: assert run process interactively
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_interactive is changed
|
||||||
|
- psexec_process_interactive.rc == 0
|
||||||
|
- psexec_process_interactive.stdout is not defined
|
||||||
|
- psexec_process_interactive.stderr is not defined
|
||||||
|
|
||||||
|
- name: run process with timeout
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: Start-Sleep -Seconds 30
|
||||||
|
process_timeout: 5
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_timeout
|
||||||
|
failed_when: psexec_process_timeout.rc == 0
|
||||||
|
|
||||||
|
- name: assert psexec process with timeout
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_timeout.rc != 0
|
||||||
|
- psexec_process_timeout.stdout == ''
|
||||||
|
- psexec_process_timeout.stderr == ''
|
||||||
|
|
||||||
|
- name: run process as system
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: whoami.exe
|
||||||
|
process_username: System
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_system
|
||||||
|
|
||||||
|
- name: assert run process as system
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_system is changed
|
||||||
|
- psexec_process_system.rc == 0
|
||||||
|
- psexec_process_system.stderr == ''
|
||||||
|
- psexec_process_system.stdout == 'nt authority\system\r\n'
|
||||||
|
|
||||||
|
- name: run process with different chdir
|
||||||
|
psexec:
|
||||||
|
hostname: '{{psexec_hostname}}'
|
||||||
|
connection_username: '{{psexec_username}}'
|
||||||
|
connection_password: '{{psexec_password}}'
|
||||||
|
encrypt: '{{psexec_encrypt}}'
|
||||||
|
executable: powershell.exe
|
||||||
|
arguments: (pwd).Path
|
||||||
|
working_directory: C:\Windows
|
||||||
|
delegate_to: localhost
|
||||||
|
register: psexec_process_working_dir
|
||||||
|
|
||||||
|
- name: assert run process with different chdir
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- psexec_process_working_dir is changed
|
||||||
|
- psexec_process_working_dir.rc == 0
|
||||||
|
- psexec_process_working_dir.stderr == ''
|
||||||
|
- psexec_process_working_dir.stdout == 'C:\Windows\r\n'
|
Loading…
Reference in a new issue