1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2018, Jordan Borean <jborean93@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10DOCUMENTATION = r'''
11---
12module: psexec
13short_description: Runs commands on a remote Windows host based on the PsExec
14  model
15description:
16- Runs a remote command from a Linux host to a Windows host without WinRM being
17  set up.
18- Can be run on the Ansible controller to bootstrap Windows hosts to get them
19  ready for WinRM.
20options:
21  hostname:
22    description:
23    - The remote Windows host to connect to, can be either an IP address or a
24      hostname.
25    type: str
26    required: yes
27  connection_username:
28    description:
29    - The username to use when connecting to the remote Windows host.
30    - This user must be a member of the C(Administrators) group of the Windows
31      host.
32    - Required if the Kerberos requirements are not installed or the username
33      is a local account to the Windows host.
34    - Can be omitted to use the default Kerberos principal ticket in the
35      local credential cache if the Kerberos library is installed.
36    - If I(process_username) is not specified, then the remote process will run
37      under a Network Logon under this account.
38    type: str
39  connection_password:
40    description:
41    - The password for I(connection_user).
42    - Required if the Kerberos requirements are not installed or the username
43      is a local account to the Windows host.
44    - Can be omitted to use a Kerberos principal ticket for the principal set
45      by I(connection_user) if the Kerberos library is installed and the
46      ticket has already been retrieved with the C(kinit) command before.
47    type: str
48  port:
49    description:
50    - The port that the remote SMB service is listening on.
51    type: int
52    default: 445
53  encrypt:
54    description:
55    - Will use SMB encryption to encrypt the SMB messages sent to and from the
56      host.
57    - This requires the SMB 3 protocol which is only supported from Windows
58      Server 2012 or Windows 8, older versions like Windows 7 or Windows Server
59      2008 (R2) must set this to C(no) and use no encryption.
60    - When setting to C(no), the packets are in plaintext and can be seen by
61      anyone sniffing the network, any process options are included in this.
62    type: bool
63    default: yes
64  connection_timeout:
65    description:
66    - The timeout in seconds to wait when receiving the initial SMB negotiate
67      response from the server.
68    type: int
69    default: 60
70  executable:
71    description:
72    - The executable to run on the Windows host.
73    type: str
74    required: yes
75  arguments:
76    description:
77    - Any arguments as a single string to use when running the executable.
78    type: str
79  working_directory:
80    description:
81    - Changes the working directory set when starting the process.
82    type: str
83    default: C:\Windows\System32
84  asynchronous:
85    description:
86    - Will run the command as a detached process and the module returns
87      immediately after starting the process while the process continues to
88      run in the background.
89    - The I(stdout) and I(stderr) return values will be null when this is set
90      to C(yes).
91    - The I(stdin) option does not work with this type of process.
92    - The I(rc) return value is not set when this is C(yes)
93    type: bool
94    default: no
95  load_profile:
96    description:
97    - Runs the remote command with the user's profile loaded.
98    type: bool
99    default: yes
100  process_username:
101    description:
102    - The user to run the process as.
103    - This can be set to run the process under an Interactive logon of the
104      specified account which bypasses limitations of a Network logon used when
105      this isn't specified.
106    - If omitted then the process is run under the same account as
107      I(connection_username) with a Network logon.
108    - Set to C(System) to run as the builtin SYSTEM account, no password is
109      required with this account.
110    - If I(encrypt) is C(no), the username and password are sent as a simple
111      XOR scrambled byte string that is not encrypted. No special tools are
112      required to get the username and password just knowledge of the protocol.
113    type: str
114  process_password:
115    description:
116    - The password for I(process_username).
117    - Required if I(process_username) is defined and not C(System).
118    type: str
119  integrity_level:
120    description:
121    - The integrity level of the process when I(process_username) is defined
122      and is not equal to C(System).
123    - When C(default), the default integrity level based on the system setup.
124    - When C(elevated), the command will be run with Administrative rights.
125    - When C(limited), the command will be forced to run with
126      non-Administrative rights.
127    type: str
128    choices:
129    - limited
130    - default
131    - elevated
132    default: default
133  interactive:
134    description:
135    - Will run the process as an interactive process that shows a process
136      Window of the Windows session specified by I(interactive_session).
137    - The I(stdout) and I(stderr) return values will be null when this is set
138      to C(yes).
139    - The I(stdin) option does not work with this type of process.
140    type: bool
141    default: no
142  interactive_session:
143    description:
144    - The Windows session ID to use when displaying the interactive process on
145      the remote Windows host.
146    - This is only valid when I(interactive) is C(yes).
147    - The default is C(0) which is the console session of the Windows host.
148    type: int
149    default: 0
150  priority:
151    description:
152    - Set the command's priority on the Windows host.
153    - See U(https://msdn.microsoft.com/en-us/library/windows/desktop/ms683211.aspx)
154      for more details.
155    type: str
156    choices:
157    - above_normal
158    - below_normal
159    - high
160    - idle
161    - normal
162    - realtime
163    default: normal
164  show_ui_on_logon_screen:
165    description:
166    - Shows the process UI on the Winlogon secure desktop when
167      I(process_username) is C(System).
168    type: bool
169    default: no
170  process_timeout:
171    description:
172    - The timeout in seconds that is placed upon the running process.
173    - A value of C(0) means no timeout.
174    type: int
175    default: 0
176  stdin:
177    description:
178    - Data to send on the stdin pipe once the process has started.
179    - This option has no effect when I(interactive) or I(asynchronous) is
180      C(yes).
181    type: str
182requirements:
183- pypsexec
184- smbprotocol[kerberos] for optional Kerberos authentication
185notes:
186- This module requires the Windows host to have SMB configured and enabled,
187  and port 445 opened on the firewall.
188- This module will wait until the process is finished unless I(asynchronous)
189  is C(yes), ensure the process is run as a non-interactive command to avoid
190  infinite hangs waiting for input.
191- The I(connection_username) must be a member of the local Administrator group
192  of the Windows host. For non-domain joined hosts, the
193  C(LocalAccountTokenFilterPolicy) should be set to C(1) to ensure this works,
194  see U(https://support.microsoft.com/en-us/help/951016/description-of-user-account-control-and-remote-restrictions-in-windows).
195- For more information on this module and the various host requirements, see
196  U(https://github.com/jborean93/pypsexec).
197seealso:
198- module: ansible.builtin.raw
199- module: ansible.windows.win_command
200- module: community.windows.win_psexec
201- module: ansible.windows.win_shell
202author:
203- Jordan Borean (@jborean93)
204'''
205
206EXAMPLES = r'''
207- name: Run a cmd.exe command
208  community.windows.psexec:
209    hostname: server
210    connection_username: username
211    connection_password: password
212    executable: cmd.exe
213    arguments: /c echo Hello World
214
215- name: Run a PowerShell command
216  community.windows.psexec:
217    hostname: server.domain.local
218    connection_username: username@DOMAIN.LOCAL
219    connection_password: password
220    executable: powershell.exe
221    arguments: Write-Host Hello World
222
223- name: Send data through stdin
224  community.windows.psexec:
225    hostname: 192.168.1.2
226    connection_username: username
227    connection_password: password
228    executable: powershell.exe
229    arguments: '-'
230    stdin: |
231      Write-Host Hello World
232      Write-Error Error Message
233      exit 0
234
235- name: Run the process as a different user
236  community.windows.psexec:
237    hostname: server
238    connection_user: username
239    connection_password: password
240    executable: whoami.exe
241    arguments: /all
242    process_username: anotheruser
243    process_password: anotherpassword
244
245- name: Run the process asynchronously
246  community.windows.psexec:
247    hostname: server
248    connection_username: username
249    connection_password: password
250    executable: cmd.exe
251    arguments: /c rmdir C:\temp
252    asynchronous: yes
253
254- name: Use Kerberos authentication for the connection (requires smbprotocol[kerberos])
255  community.windows.psexec:
256    hostname: host.domain.local
257    connection_username: user@DOMAIN.LOCAL
258    executable: C:\some\path\to\executable.exe
259    arguments: /s
260
261- name: Disable encryption to work with WIndows 7/Server 2008 (R2)
262  community.windows.psexec:
263    hostanme: windows-pc
264    connection_username: Administrator
265    connection_password: Password01
266    encrypt: no
267    integrity_level: elevated
268    process_username: Administrator
269    process_password: Password01
270    executable: powershell.exe
271    arguments: (New-Object -ComObject Microsoft.Update.Session).CreateUpdateInstaller().IsBusy
272
273- name: Download and run ConfigureRemotingForAnsible.ps1 to setup WinRM
274  community.windows.psexec:
275    hostname: '{{ hostvars[inventory_hostname]["ansible_host"] | default(inventory_hostname) }}'
276    connection_username: '{{ ansible_user }}'
277    connection_password: '{{ ansible_password }}'
278    encrypt: yes
279    executable: powershell.exe
280    arguments: '-'
281    stdin: |
282      $ErrorActionPreference = "Stop"
283      $sec_protocols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault
284      $sec_protocols = $sec_protocols -bor [Net.SecurityProtocolType]::Tls12
285      [Net.ServicePointManager]::SecurityProtocol = $sec_protocols
286      $url = "https://github.com/ansible/ansible/raw/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
287      Invoke-Expression ((New-Object Net.WebClient).DownloadString($url))
288      exit
289  delegate_to: localhost
290'''
291
292RETURN = r'''
293msg:
294  description: Any exception details when trying to run the process
295  returned: module failed
296  type: str
297  sample: 'Received exception from remote PAExec service: Failed to start "invalid.exe". The system cannot find the file specified. [Err=0x2, 2]'
298stdout:
299  description: The stdout from the remote process
300  returned: success and interactive or asynchronous is 'no'
301  type: str
302  sample: Hello World
303stderr:
304  description: The stderr from the remote process
305  returned: success and interactive or asynchronous is 'no'
306  type: str
307  sample: Error [10] running process
308pid:
309  description: The process ID of the asynchronous process that was created
310  returned: success and asynchronous is 'yes'
311  type: int
312  sample: 719
313rc:
314  description: The return code of the remote process
315  returned: success and asynchronous is 'no'
316  type: int
317  sample: 0
318'''
319
320import traceback
321
322from ansible.module_utils.basic import AnsibleModule, missing_required_lib
323from ansible.module_utils._text import to_bytes, to_text
324
325PYPSEXEC_IMP_ERR = None
326try:
327    from pypsexec import client
328    from pypsexec.exceptions import PypsexecException, PAExecException, \
329        PDUException, SCMRException
330    from pypsexec.paexec import ProcessPriority
331    from smbprotocol.exceptions import SMBException, SMBAuthenticationError, \
332        SMBResponseException
333    import socket
334    HAS_PYPSEXEC = True
335except ImportError:
336    PYPSEXEC_IMP_ERR = traceback.format_exc()
337    HAS_PYPSEXEC = False
338
339KERBEROS_IMP_ERR = None
340try:
341    import gssapi
342    # GSSAPI extension required for Kerberos Auth in SMB
343    from gssapi.raw import inquire_sec_context_by_oid
344    HAS_KERBEROS = True
345except ImportError:
346    KERBEROS_IMP_ERR = traceback.format_exc()
347    HAS_KERBEROS = False
348
349
350def remove_artifacts(module, client):
351    try:
352        client.remove_service()
353    except (SMBException, PypsexecException) as exc:
354        module.warn("Failed to cleanup PAExec service and executable: %s"
355                    % to_text(exc))
356
357
358def main():
359    module_args = dict(
360        hostname=dict(type='str', required=True),
361        connection_username=dict(type='str'),
362        connection_password=dict(type='str', no_log=True),
363        port=dict(type='int', required=False, default=445),
364        encrypt=dict(type='bool', default=True),
365        connection_timeout=dict(type='int', default=60),
366        executable=dict(type='str', required=True),
367        arguments=dict(type='str'),
368        working_directory=dict(type='str', default=r'C:\Windows\System32'),
369        asynchronous=dict(type='bool', default=False),
370        load_profile=dict(type='bool', default=True),
371        process_username=dict(type='str'),
372        process_password=dict(type='str', no_log=True),
373        integrity_level=dict(type='str', default='default',
374                             choices=['default', 'elevated', 'limited']),
375        interactive=dict(type='bool', default=False),
376        interactive_session=dict(type='int', default=0),
377        priority=dict(type='str', default='normal',
378                      choices=['above_normal', 'below_normal', 'high',
379                               'idle', 'normal', 'realtime']),
380        show_ui_on_logon_screen=dict(type='bool', default=False),
381        process_timeout=dict(type='int', default=0),
382        stdin=dict(type='str')
383    )
384    result = dict(
385        changed=False,
386    )
387    module = AnsibleModule(
388        argument_spec=module_args,
389        supports_check_mode=False,
390    )
391
392    process_username = module.params['process_username']
393    process_password = module.params['process_password']
394    use_system = False
395    if process_username is not None and process_username.lower() == "system":
396        use_system = True
397        process_username = None
398        process_password = None
399
400    if process_username is not None and process_password is None:
401        module.fail_json(msg='parameters are required together when not '
402                             'running as System: process_username, '
403                             'process_password')
404    if not HAS_PYPSEXEC:
405        module.fail_json(msg=missing_required_lib("pypsexec"),
406                         exception=PYPSEXEC_IMP_ERR)
407
408    hostname = module.params['hostname']
409    connection_username = module.params['connection_username']
410    connection_password = module.params['connection_password']
411    port = module.params['port']
412    encrypt = module.params['encrypt']
413    connection_timeout = module.params['connection_timeout']
414    executable = module.params['executable']
415    arguments = module.params['arguments']
416    working_directory = module.params['working_directory']
417    asynchronous = module.params['asynchronous']
418    load_profile = module.params['load_profile']
419    elevated = module.params['integrity_level'] == "elevated"
420    limited = module.params['integrity_level'] == "limited"
421    interactive = module.params['interactive']
422    interactive_session = module.params['interactive_session']
423
424    priority = {
425        "above_normal": ProcessPriority.ABOVE_NORMAL_PRIORITY_CLASS,
426        "below_normal": ProcessPriority.BELOW_NORMAL_PRIORITY_CLASS,
427        "high": ProcessPriority.HIGH_PRIORITY_CLASS,
428        "idle": ProcessPriority.IDLE_PRIORITY_CLASS,
429        "normal": ProcessPriority.NORMAL_PRIORITY_CLASS,
430        "realtime": ProcessPriority.REALTIME_PRIORITY_CLASS
431    }[module.params['priority']]
432    show_ui_on_logon_screen = module.params['show_ui_on_logon_screen']
433
434    process_timeout = module.params['process_timeout']
435    stdin = module.params['stdin']
436
437    if (connection_username is None or connection_password is None) and \
438            not HAS_KERBEROS:
439        module.fail_json(msg=missing_required_lib("gssapi"),
440                         execption=KERBEROS_IMP_ERR)
441
442    win_client = client.Client(server=hostname, username=connection_username,
443                               password=connection_password, port=port,
444                               encrypt=encrypt)
445
446    try:
447        win_client.connect(timeout=connection_timeout)
448    except SMBAuthenticationError as exc:
449        module.fail_json(msg='Failed to authenticate over SMB: %s'
450                             % to_text(exc))
451    except SMBResponseException as exc:
452        module.fail_json(msg='Received unexpected SMB response when opening '
453                             'the connection: %s' % to_text(exc))
454    except PDUException as exc:
455        module.fail_json(msg='Received an exception with RPC PDU message: %s'
456                             % to_text(exc))
457    except SCMRException as exc:
458        module.fail_json(msg='Received an exception when dealing with SCMR on '
459                             'the Windows host: %s' % to_text(exc))
460    except (SMBException, PypsexecException) as exc:
461        module.fail_json(msg=to_text(exc))
462    except socket.error as exc:
463        module.fail_json(msg=to_text(exc))
464
465    # create PAExec service and run the process
466    result['changed'] = True
467    b_stdin = to_bytes(stdin, encoding='utf-8') if stdin else None
468    run_args = dict(
469        executable=executable, arguments=arguments, asynchronous=asynchronous,
470        load_profile=load_profile, interactive=interactive,
471        interactive_session=interactive_session,
472        run_elevated=elevated, run_limited=limited,
473        username=process_username, password=process_password,
474        use_system_account=use_system, working_dir=working_directory,
475        priority=priority, show_ui_on_win_logon=show_ui_on_logon_screen,
476        timeout_seconds=process_timeout, stdin=b_stdin
477    )
478    try:
479        win_client.create_service()
480    except (SMBException, PypsexecException) as exc:
481        module.fail_json(msg='Failed to create PAExec service: %s'
482                         % to_text(exc))
483
484    try:
485        proc_result = win_client.run_executable(**run_args)
486    except (SMBException, PypsexecException) as exc:
487        module.fail_json(msg='Received error when running remote process: %s'
488                         % to_text(exc))
489    finally:
490        remove_artifacts(module, win_client)
491
492    if asynchronous:
493        result['pid'] = proc_result[2]
494    elif interactive:
495        result['rc'] = proc_result[2]
496    else:
497        result['stdout'] = proc_result[0]
498        result['stderr'] = proc_result[1]
499        result['rc'] = proc_result[2]
500
501    # close the SMB connection
502    try:
503        win_client.disconnect()
504    except (SMBException, PypsexecException) as exc:
505        module.warn("Failed to close the SMB connection: %s" % to_text(exc))
506
507    module.exit_json(**result)
508
509
510if __name__ == '__main__':
511    main()
512