VYPR
Moderate severityNVD Advisory· Published Nov 29, 2018· Updated Aug 5, 2024

CVE-2018-16859

CVE-2018-16859

Description

Execution of Ansible playbooks on Windows platforms with PowerShell ScriptBlock logging and Module logging enabled can allow for 'become' passwords to appear in EventLogs in plaintext. A local user with administrator privileges on the machine can view these logs and discover the plaintext password. Ansible Engine 2.8 and older are believed to be vulnerable.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ansiblePyPI
>= 2.7.0a1, < 2.7.32.7.3
ansiblePyPI
< 2.5.122.5.12
ansiblePyPI
>= 2.6.0a1, < 2.6.92.6.9

Affected products

1

Patches

3
0d746b4198ab

split PS wrapper and payload (CVE-2018-16859) (#49145)

https://github.com/ansible/ansibleMatt DavisNov 26, 2018via ghsa
8 files changed · +57 17
  • changelogs/fragments/ps_sb_logging.yaml+2 0 added
    @@ -0,0 +1,2 @@
    +bugfixes:
    +- Windows - prevent sensitive content from appearing in scriptblock logging (CVE 2018-16859)
    
  • lib/ansible/executor/module_common.py+2 1 modified
    @@ -850,7 +850,8 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
             # FUTURE: smuggle this back as a dict instead of serializing here; the connection plugin may need to modify it
             module_json = json.dumps(exec_manifest)
     
    -        b_module_data = exec_wrapper.replace(b"$json_raw = ''", b"$json_raw = @'\r\n%s\r\n'@" % to_bytes(module_json))
    +        # delimit the payload JSON from the wrapper to keep sensitive contents out of scriptblocks (which can be logged)
    +        b_module_data = to_bytes(exec_wrapper) + b'\0\0\0\0' + to_bytes(module_json)
     
         elif module_substyle == 'jsonargs':
             module_args_json = to_bytes(json.dumps(module_args))
    
  • lib/ansible/executor/powershell/bootstrap_wrapper.ps1+7 0 added
    @@ -0,0 +1,7 @@
    +&chcp.com 65001 > $null
    +$exec_wrapper_str = $input | Out-String
    +$split_parts = $exec_wrapper_str.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries)
    +If (-not $split_parts.Length -eq 2) { throw "invalid payload" }
    +Set-Variable -Name json_raw -Value $split_parts[1]
    +$exec_wrapper = [ScriptBlock]::Create($split_parts[0])
    +&$exec_wrapper
    
  • lib/ansible/executor/powershell/__init__.py+0 0 added
  • lib/ansible/plugins/action/script.py+7 6 modified
    @@ -119,12 +119,13 @@ def run(self, tmp=None, task_vars=None):
                 exec_data = None
                 # WinRM requires a special wrapper to work with environment variables
                 if self._connection.transport == "winrm":
    -                pay = self._connection._create_raw_wrapper_payload(script_cmd,
    -                                                                   env_dict)
    -                exec_data = exec_wrapper.replace(b"$json_raw = ''",
    -                                                 b"$json_raw = @'\r\n%s\r\n'@"
    -                                                 % to_bytes(pay))
    -                script_cmd = "-"
    +                pay = self._connection._create_raw_wrapper_payload(script_cmd, env_dict)
    +                exec_data = to_bytes(exec_wrapper) + b"\0\0\0\0" + to_bytes(pay)
    +
    +                # build the necessary exec wrapper command
    +                # FUTURE: this still doesn't let script work on Windows with non-pipelined connections or
    +                # full manual exec of KEEP_REMOTE_FILES
    +                script_cmd = self._connection._shell.build_module_command(env_string='', shebang='#!powershell', cmd='')
     
                 result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir))
     
    
  • lib/ansible/plugins/connection/winrm.py+9 5 modified
    @@ -104,6 +104,7 @@
     import json
     import tempfile
     import subprocess
    +import xml.etree.ElementTree as ET
     
     HAVE_KERBEROS = False
     try:
    @@ -537,14 +538,17 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
             return (result.status_code, result.std_out, result.std_err)
     
         def is_clixml(self, value):
    -        return value.startswith(b"#< CLIXML")
    +        return value.startswith(b"#< CLIXML\r\n")
     
         # hacky way to get just stdout- not always sure of doc framing here, so use with care
         def parse_clixml_stream(self, clixml_doc, stream_name='Error'):
    -        clear_xml = clixml_doc.replace(b'#< CLIXML\r\n', b'')
    -        doc = xmltodict.parse(clear_xml)
    -        lines = [l.get('#text', '').replace('_x000D__x000A_', '') for l in doc.get('Objs', {}).get('S', {}) if l.get('@S') == stream_name]
    -        return '\r\n'.join(lines)
    +        clixml = ET.fromstring(clixml_doc.split(b"\r\n", 1)[-1])
    +        namespace_match = re.match(r'{(.*)}', clixml.tag)
    +        namespace = "{%s}" % namespace_match.group(1) if namespace_match else ""
    +
    +        strings = clixml.findall("./%sS" % namespace)
    +        lines = [e.text.replace('_x000D__x000A_', '') for e in strings if e.attrib.get('S') == stream_name]
    +        return to_bytes('\r\n'.join(lines))
     
         # FUTURE: determine buffer size at runtime via remote winrm config?
         def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
    
  • lib/ansible/plugins/shell/powershell.py+12 4 modified
    @@ -44,6 +44,7 @@
     import os
     import re
     import shlex
    +import pkgutil
     
     from ansible.errors import AnsibleError
     from ansible.module_utils._text import to_text
    @@ -78,8 +79,10 @@
         # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
         # exec runspace, capture output, cleanup, return module output
     
    -    # NB: do not adjust the following line- it is replaced when doing non-streamed module output
    -    $json_raw = ''
    +    # only init and stream in $json_raw if it wasn't set by the enclosing scope
    +    if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) {
    +        $json_raw = ''
    +    }
     }
     process {
         $input_as_string = [string]$input
    @@ -1966,18 +1969,23 @@ def checksum(self, path, *args, **kwargs):
             return self._encode_script(script)
     
         def build_module_command(self, env_string, shebang, cmd, arg_path=None):
    +        bootstrap_wrapper = pkgutil.get_data("ansible.executor.powershell", "bootstrap_wrapper.ps1")
    +
             # pipelining bypass
             if cmd == '':
    -            return '-'
    +            return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False)
     
             # non-pipelining
     
             cmd_parts = shlex.split(cmd, posix=False)
             cmd_parts = list(map(to_text, cmd_parts))
             if shebang and shebang.lower() == '#!powershell':
                 if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'):
    +                # we're running a module via the bootstrap wrapper
                     cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0])
    -            cmd_parts.insert(0, '&')
    +            wrapper_cmd = "type " + cmd_parts[0] + " | " + self._encode_script(script=bootstrap_wrapper,
    +                                                                               strict_mode=False, preserve_rc=False)
    +            return wrapper_cmd
             elif shebang and shebang.startswith('#!'):
                 cmd_parts.insert(0, shebang[2:])
             elif not shebang:
    
  • test/integration/targets/win_become/tasks/main.yml+18 1 modified
    @@ -1,7 +1,7 @@
     - set_fact:
         become_test_username: ansible_become_test
         become_test_admin_username: ansible_become_admin
    -    gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
    +    gen_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}"
     
     - name: create unprivileged user
       win_user:
    @@ -29,6 +29,10 @@
       - SeInteractiveLogonRight
       - SeBatchLogonRight
     
    +- name: fetch current target date/time for log filtering
    +  raw: '[datetime]::now | Out-String'
    +  register: test_starttime
    +
     - name: execute tests and ensure that test user is deleted regardless of success/failure
       block:
       - name: ensure current user is not the become user
    @@ -219,6 +223,7 @@
         assert:
           that:
           - whoami_out is successful
    +      - become_test_username in whoami_out.stdout
         when: os_version.stdout_lines[0] == "async"
     
       - name: test failure with string become invalid key
    @@ -320,6 +325,18 @@
           - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
           - nonascii_output.stderr == ''
     
    +  - name: get PS events containing password or module args created since test start
    +    raw: |
    +      $dt=[datetime]"{{ test_starttime.stdout|trim }}"
    +      (Get-WinEvent -LogName Microsoft-Windows-Powershell/Operational |
    +      ? { $_.TimeCreated -ge $dt -and $_.Message -match "{{ gen_pw }}|whoami" }).Count
    +    register: ps_log_count
    +
    +  - name: assert no PS events contain password or module args
    +    assert:
    +      that:
    +      - ps_log_count.stdout | int == 0
    +
     # FUTURE: test raw + script become behavior once they're running under the exec wrapper again
     # FUTURE: add standalone playbook tests to include password prompting and play become keywords
     
    
2f8d3fcf4110

split PS wrapper and payload (CVE-2018-16859) (#49143)

https://github.com/ansible/ansibleMatt DavisNov 26, 2018via ghsa
9 files changed · +66 24
  • changelogs/fragments/ps_sb_logging.yaml+2 0 added
    @@ -0,0 +1,2 @@
    +bugfixes:
    +- Windows - prevent sensitive content from appearing in scriptblock logging (CVE 2018-16859)
    
  • lib/ansible/executor/module_common.py+2 1 modified
    @@ -912,7 +912,8 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
             # FUTURE: smuggle this back as a dict instead of serializing here; the connection plugin may need to modify it
             module_json = json.dumps(exec_manifest)
     
    -        b_module_data = exec_wrapper.replace(b"$json_raw = ''", b"$json_raw = @'\r\n%s\r\n'@" % to_bytes(module_json))
    +        # delimit the payload JSON from the wrapper to keep sensitive contents out of scriptblocks (which can be logged)
    +        b_module_data = to_bytes(exec_wrapper) + b'\0\0\0\0' + to_bytes(module_json)
     
         elif module_substyle == 'jsonargs':
             module_args_json = to_bytes(json.dumps(module_args))
    
  • lib/ansible/executor/powershell/bootstrap_wrapper.ps1+7 0 added
    @@ -0,0 +1,7 @@
    +&chcp.com 65001 > $null
    +$exec_wrapper_str = $input | Out-String
    +$split_parts = $exec_wrapper_str.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries)
    +If (-not $split_parts.Length -eq 2) { throw "invalid payload" }
    +Set-Variable -Name json_raw -Value $split_parts[1]
    +$exec_wrapper = [ScriptBlock]::Create($split_parts[0])
    +&$exec_wrapper
    
  • lib/ansible/executor/powershell/__init__.py+0 0 added
  • lib/ansible/plugins/action/script.py+7 6 modified
    @@ -126,12 +126,13 @@ def run(self, tmp=None, task_vars=None):
                 exec_data = None
                 # WinRM requires a special wrapper to work with environment variables
                 if self._connection.transport == "winrm":
    -                pay = self._connection._create_raw_wrapper_payload(script_cmd,
    -                                                                   env_dict)
    -                exec_data = exec_wrapper.replace(b"$json_raw = ''",
    -                                                 b"$json_raw = @'\r\n%s\r\n'@"
    -                                                 % to_bytes(pay))
    -                script_cmd = "-"
    +                pay = self._connection._create_raw_wrapper_payload(script_cmd, env_dict)
    +                exec_data = to_bytes(exec_wrapper) + b"\0\0\0\0" + to_bytes(pay)
    +
    +                # build the necessary exec wrapper command
    +                # FUTURE: this still doesn't let script work on Windows with non-pipelined connections or
    +                # full manual exec of KEEP_REMOTE_FILES
    +                script_cmd = self._connection._shell.build_module_command(env_string='', shebang='#!powershell', cmd='')
     
                 result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir))
     
    
  • lib/ansible/plugins/connection/psrp.py+1 0 modified
    @@ -278,6 +278,7 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
                 # starting a new interpreter to save on time
                 b_command = base64.b64decode(cmd.split(" ")[-1])
                 script = to_text(b_command, 'utf-16-le')
    +            in_data = to_text(in_data, errors="surrogate_or_strict", nonstring="passthru")
                 display.vvv("PSRP: EXEC %s" % script, host=self._psrp_host)
             else:
                 # in other cases we want to execute the cmd as the script
    
  • lib/ansible/plugins/connection/winrm.py+9 5 modified
    @@ -103,6 +103,7 @@
     import json
     import tempfile
     import subprocess
    +import xml.etree.ElementTree as ET
     
     HAVE_KERBEROS = False
     try:
    @@ -550,14 +551,17 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
             return (result.status_code, result.std_out, result.std_err)
     
         def is_clixml(self, value):
    -        return value.startswith(b"#< CLIXML")
    +        return value.startswith(b"#< CLIXML\r\n")
     
         # hacky way to get just stdout- not always sure of doc framing here, so use with care
         def parse_clixml_stream(self, clixml_doc, stream_name='Error'):
    -        clear_xml = clixml_doc.replace(b'#< CLIXML\r\n', b'')
    -        doc = xmltodict.parse(clear_xml)
    -        lines = [l.get('#text', '').replace('_x000D__x000A_', '') for l in doc.get('Objs', {}).get('S', {}) if l.get('@S') == stream_name]
    -        return '\r\n'.join(lines)
    +        clixml = ET.fromstring(clixml_doc.split(b"\r\n", 1)[-1])
    +        namespace_match = re.match(r'{(.*)}', clixml.tag)
    +        namespace = "{%s}" % namespace_match.group(1) if namespace_match else ""
    +
    +        strings = clixml.findall("./%sS" % namespace)
    +        lines = [e.text.replace('_x000D__x000A_', '') for e in strings if e.attrib.get('S') == stream_name]
    +        return to_bytes('\r\n'.join(lines))
     
         # FUTURE: determine buffer size at runtime via remote winrm config?
         def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
    
  • lib/ansible/plugins/shell/powershell.py+20 11 modified
    @@ -44,6 +44,7 @@
     import os
     import re
     import shlex
    +import pkgutil
     
     from ansible.errors import AnsibleError
     from ansible.module_utils._text import to_text
    @@ -78,8 +79,10 @@
         # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
         # exec runspace, capture output, cleanup, return module output
     
    -    # NB: do not adjust the following line- it is replaced when doing non-streamed module output
    -    $json_raw = ''
    +    # only init and stream in $json_raw if it wasn't set by the enclosing scope
    +    if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) {
    +        $json_raw = ''
    +    }
     }
     process {
         $input_as_string = [string]$input
    @@ -929,9 +932,11 @@ class NativeWaitHandle : WaitHandle
     $become_exec_wrapper = {
         chcp.com 65001 > $null
         $ProgressPreference = "SilentlyContinue"
    -    $exec_wrapper_str = [System.Console]::In.ReadToEnd()
    -    $exec_wrapper = [ScriptBlock]::Create($exec_wrapper_str)
    -    &$exec_wrapper
    +    $raw = [System.Console]::In.ReadToEnd()
    +    $split_parts = $raw.Split(@("`0`0`0`0"), 0)
    +    If (-not $split_parts.Length -eq 2) { throw "invalid payload" }
    +    $json_raw = $split_parts[1]
    +    &([ScriptBlock]::Create($split_parts[0]))
     }
     
     $exec_wrapper = {
    @@ -955,10 +960,9 @@ class NativeWaitHandle : WaitHandle
         # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
         # exec runspace, capture output, cleanup, return module output. Do not change this as it is set become before being passed to the
         # become process.
    -    $json_raw = ""
     
    -    If (-not $json_raw) {
    -        Write-Error "no input given" -Category InvalidArgument
    +    if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) {
    +        Write-Error "no payload supplied" -Category InvalidArgument
         }
     
         $payload = ConvertTo-HashtableFromPsCustomObject -myPsObject (ConvertFrom-Json $json_raw)
    @@ -1095,7 +1099,7 @@ class NativeWaitHandle : WaitHandle
         # wrapper which calls our read wrapper passed through stdin. Cannot use 'powershell -' as
         # the $ErrorActionPreference is always set to Stop and cannot be changed
         $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
    -    $exec_wrapper = $exec_wrapper.ToString().Replace('$json_raw = ""', "`$json_raw = '$payload_string'")
    +    $exec_wrapper = $exec_wrapper.ToString() + "`0`0`0`0" + $payload_string
         $rc = 0
     
         $exec_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($become_exec_wrapper.ToString()))
    @@ -1584,18 +1588,23 @@ def checksum(self, path, *args, **kwargs):
             return self._encode_script(script)
     
         def build_module_command(self, env_string, shebang, cmd, arg_path=None):
    +        bootstrap_wrapper = pkgutil.get_data("ansible.executor.powershell", "bootstrap_wrapper.ps1")
    +
             # pipelining bypass
             if cmd == '':
    -            return '-'
    +            return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False)
     
             # non-pipelining
     
             cmd_parts = shlex.split(cmd, posix=False)
             cmd_parts = list(map(to_text, cmd_parts))
             if shebang and shebang.lower() == '#!powershell':
                 if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'):
    +                # we're running a module via the bootstrap wrapper
                     cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0])
    -            cmd_parts.insert(0, '&')
    +            wrapper_cmd = "type " + cmd_parts[0] + " | " + self._encode_script(script=bootstrap_wrapper,
    +                                                                               strict_mode=False, preserve_rc=False)
    +            return wrapper_cmd
             elif shebang and shebang.startswith('#!'):
                 cmd_parts.insert(0, shebang[2:])
             elif not shebang:
    
  • test/integration/targets/win_become/tasks/main.yml+18 1 modified
    @@ -1,7 +1,7 @@
     - set_fact:
         become_test_username: ansible_become_test
         become_test_admin_username: ansible_become_admin
    -    gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
    +    gen_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}"
     
     - name: create unprivileged user
       win_user:
    @@ -29,6 +29,10 @@
       - SeInteractiveLogonRight
       - SeBatchLogonRight
     
    +- name: fetch current target date/time for log filtering
    +  raw: '[datetime]::now | Out-String'
    +  register: test_starttime
    +
     - name: execute tests and ensure that test user is deleted regardless of success/failure
       block:
       - name: ensure current user is not the become user
    @@ -199,6 +203,7 @@
         assert:
           that:
           - whoami_out is successful
    +      - become_test_username in whoami_out.stdout
     
       - name: test failure with string become invalid key
         vars: *become_vars
    @@ -312,6 +317,18 @@
           - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
           - nonascii_output.stderr == ''
     
    +  - name: get PS events containing password or module args created since test start
    +    raw: |
    +      $dt=[datetime]"{{ test_starttime.stdout|trim }}"
    +      (Get-WinEvent -LogName Microsoft-Windows-Powershell/Operational |
    +      ? { $_.TimeCreated -ge $dt -and $_.Message -match "{{ gen_pw }}|whoami" }).Count
    +    register: ps_log_count
    +
    +  - name: assert no PS events contain password or module args
    +    assert:
    +      that:
    +      - ps_log_count.stdout | int == 0
    +
     # FUTURE: test raw + script become behavior once they're running under the exec wrapper again
     # FUTURE: add standalone playbook tests to include password prompting and play become keywords
     
    
4d748d34f939

split PS wrapper and payload (CVE-2018-16859)

https://github.com/ansible/ansibleMatt DavisNov 17, 2018via ghsa
8 files changed · +57 17
  • changelogs/fragments/ps_sb_logging.yaml+2 0 added
    @@ -0,0 +1,2 @@
    +bugfixes:
    +- Windows - prevent sensitive content from appearing in scriptblock logging (CVE 2018-16859)
    
  • lib/ansible/executor/module_common.py+2 1 modified
    @@ -850,7 +850,8 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
             # FUTURE: smuggle this back as a dict instead of serializing here; the connection plugin may need to modify it
             module_json = json.dumps(exec_manifest)
     
    -        b_module_data = exec_wrapper.replace(b"$json_raw = ''", b"$json_raw = @'\r\n%s\r\n'@" % to_bytes(module_json))
    +        # delimit the payload JSON from the wrapper to keep sensitive contents out of scriptblocks (which can be logged)
    +        b_module_data = to_bytes(exec_wrapper) + b'\0\0\0\0' + to_bytes(module_json)
     
         elif module_substyle == 'jsonargs':
             module_args_json = to_bytes(json.dumps(module_args))
    
  • lib/ansible/executor/powershell/bootstrap_wrapper.ps1+7 0 added
    @@ -0,0 +1,7 @@
    +&chcp.com 65001 > $null
    +$exec_wrapper_str = $input | Out-String
    +$split_parts = $exec_wrapper_str.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries)
    +If (-not $split_parts.Length -eq 2) { throw "invalid payload" }
    +Set-Variable -Name json_raw -Value $split_parts[1]
    +$exec_wrapper = [ScriptBlock]::Create($split_parts[0])
    +&$exec_wrapper
    
  • lib/ansible/executor/powershell/__init__.py+0 0 added
  • lib/ansible/plugins/action/script.py+7 6 modified
    @@ -126,12 +126,13 @@ def run(self, tmp=None, task_vars=None):
                 exec_data = None
                 # WinRM requires a special wrapper to work with environment variables
                 if self._connection.transport == "winrm":
    -                pay = self._connection._create_raw_wrapper_payload(script_cmd,
    -                                                                   env_dict)
    -                exec_data = exec_wrapper.replace(b"$json_raw = ''",
    -                                                 b"$json_raw = @'\r\n%s\r\n'@"
    -                                                 % to_bytes(pay))
    -                script_cmd = "-"
    +                pay = self._connection._create_raw_wrapper_payload(script_cmd, env_dict)
    +                exec_data = to_bytes(exec_wrapper) + b"\0\0\0\0" + to_bytes(pay)
    +
    +                # build the necessary exec wrapper command
    +                # FUTURE: this still doesn't let script work on Windows with non-pipelined connections or
    +                # full manual exec of KEEP_REMOTE_FILES
    +                script_cmd = self._connection._shell.build_module_command(env_string='', shebang='#!powershell', cmd='')
     
                 result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir))
     
    
  • lib/ansible/plugins/connection/winrm.py+9 5 modified
    @@ -103,6 +103,7 @@
     import json
     import tempfile
     import subprocess
    +import xml.etree.ElementTree as ET
     
     HAVE_KERBEROS = False
     try:
    @@ -550,14 +551,17 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
             return (result.status_code, result.std_out, result.std_err)
     
         def is_clixml(self, value):
    -        return value.startswith(b"#< CLIXML")
    +        return value.startswith(b"#< CLIXML\r\n")
     
         # hacky way to get just stdout- not always sure of doc framing here, so use with care
         def parse_clixml_stream(self, clixml_doc, stream_name='Error'):
    -        clear_xml = clixml_doc.replace(b'#< CLIXML\r\n', b'')
    -        doc = xmltodict.parse(clear_xml)
    -        lines = [l.get('#text', '').replace('_x000D__x000A_', '') for l in doc.get('Objs', {}).get('S', {}) if l.get('@S') == stream_name]
    -        return '\r\n'.join(lines)
    +        clixml = ET.fromstring(clixml_doc.split(b"\r\n", 1)[-1])
    +        namespace_match = re.match(r'{(.*)}', clixml.tag)
    +        namespace = "{%s}" % namespace_match.group(1) if namespace_match else ""
    +
    +        strings = clixml.findall("./%sS" % namespace)
    +        lines = [e.text.replace('_x000D__x000A_', '') for e in strings if e.attrib.get('S') == stream_name]
    +        return to_bytes('\r\n'.join(lines))
     
         # FUTURE: determine buffer size at runtime via remote winrm config?
         def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
    
  • lib/ansible/plugins/shell/powershell.py+12 4 modified
    @@ -44,6 +44,7 @@
     import os
     import re
     import shlex
    +import pkgutil
     
     from ansible.errors import AnsibleError
     from ansible.module_utils._text import to_native, to_text
    @@ -78,8 +79,10 @@
         # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
         # exec runspace, capture output, cleanup, return module output
     
    -    # NB: do not adjust the following line- it is replaced when doing non-streamed module output
    -    $json_raw = ''
    +    # only init and stream in $json_raw if it wasn't set by the enclosing scope
    +    if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) {
    +        $json_raw = ''
    +    }
     }
     process {
         $input_as_string = [string]$input
    @@ -2022,18 +2025,23 @@ def checksum(self, path, *args, **kwargs):
             return self._encode_script(script)
     
         def build_module_command(self, env_string, shebang, cmd, arg_path=None):
    +        bootstrap_wrapper = pkgutil.get_data("ansible.executor.powershell", "bootstrap_wrapper.ps1")
    +
             # pipelining bypass
             if cmd == '':
    -            return '-'
    +            return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False)
     
             # non-pipelining
     
             cmd_parts = shlex.split(to_native(cmd), posix=False)
             cmd_parts = list(map(to_text, cmd_parts))
             if shebang and shebang.lower() == '#!powershell':
                 if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'):
    +                # we're running a module via the bootstrap wrapper
                     cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0])
    -            cmd_parts.insert(0, '&')
    +            wrapper_cmd = "type " + cmd_parts[0] + " | " + self._encode_script(script=bootstrap_wrapper,
    +                                                                               strict_mode=False, preserve_rc=False)
    +            return wrapper_cmd
             elif shebang and shebang.startswith('#!'):
                 cmd_parts.insert(0, shebang[2:])
             elif not shebang:
    
  • test/integration/targets/win_become/tasks/main.yml+18 1 modified
    @@ -1,7 +1,7 @@
     - set_fact:
         become_test_username: ansible_become_test
         become_test_admin_username: ansible_become_admin
    -    gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
    +    gen_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}"
     
     - name: create unprivileged user
       win_user:
    @@ -29,6 +29,10 @@
       - SeInteractiveLogonRight
       - SeBatchLogonRight
     
    +- name: fetch current target date/time for log filtering
    +  raw: '[datetime]::now | Out-String'
    +  register: test_starttime
    +
     - name: execute tests and ensure that test user is deleted regardless of success/failure
       block:
       - name: ensure current user is not the become user
    @@ -219,6 +223,7 @@
         assert:
           that:
           - whoami_out is successful
    +      - become_test_username in whoami_out.stdout
         when: os_version.stdout_lines[0] == "async"
     
       - name: test failure with string become invalid key
    @@ -320,6 +325,18 @@
           - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
           - nonascii_output.stderr == ''
     
    +  - name: get PS events containing password or module args created since test start
    +    raw: |
    +      $dt=[datetime]"{{ test_starttime.stdout|trim }}"
    +      (Get-WinEvent -LogName Microsoft-Windows-Powershell/Operational |
    +      ? { $_.TimeCreated -ge $dt -and $_.Message -match "{{ gen_pw }}|whoami" }).Count
    +    register: ps_log_count
    +
    +  - name: assert no PS events contain password or module args
    +    assert:
    +      that:
    +      - ps_log_count.stdout | int == 0
    +
     # FUTURE: test raw + script become behavior once they're running under the exec wrapper again
     # FUTURE: add standalone playbook tests to include password prompting and play become keywords
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

18

News mentions

0

No linked articles in our index yet.