PSRP_utils.ps1
# PowerShell Remoting Protocol utils # Fragments $const_fragment_start = 0x01 $const_fragment_end = 0x02 $const_fragment_start_end = 0x03 $const_fragment_middle = 0x00 $PSRM_message_fragment=@{ $const_fragment_start = "Start" $const_fragment_end = "End" $const_fragment_start_end = "Single" $const_fragment_middle = "Middle" } # Destinations $const_destination_client = 0x01 $const_destination_server = 0x02 $PSRM_message_destination=@{ $const_destination_client = "Client" $const_destination_server = "Server" } # Message types $const_SESSION_CAPABILITY = 0x00010002 $const_INIT_RUNSPACEPOOL = 0x00010004 $const_PUBLIC_KEY = 0x00010005 $const_ENCRYPTED_SESSION_KEY = 0x00010006 $const_PUBLIC_KEY_REQUEST = 0x00010007 $const_CONNECT_RUNSPACEPOOL = 0x00010008 $const_SET_MAX_RUNSPACES = 0x00021002 $const_SET_MIN_RUNSPACES = 0x00021003 $const_RUNSPACE_AVAILABILITY = 0x00021004 $const_RUNSPACEPOOL_STATE = 0x00021005 $const_CREATE_PIPELINE = 0x00021006 $const_GET_AVAILABLE_RUNSPACES = 0x00021007 $const_USER_EVENT = 0x00021008 $const_APPLICATION_PRIVATE_DATA = 0x00021009 $const_GET_COMMAND_METADATA = 0x0002100A $const_RUNSPACEPOOL_INIT_DATA = 0x0002100B $const_RESET_RUNSPACE_STATE = 0x0002100C $const_RUNSPACEPOOL_HOST_CALL = 0x00021100 $const_RUNSPACEPOOL_HOST_RESPONSE=0x00021101 $const_PIPELINE_INPUT = 0x00041002 $const_END_OF_PIPELINE_INPUT = 0x00041003 $const_PIPELINE_OUTPUT = 0x00041004 $const_ERROR_RECORD = 0x00041005 $const_PIPELINE_STATE = 0x00041006 $const_DEBUG_RECORD = 0x00041007 $const_VERBOSE_RECORD = 0x00041008 $const_WARNING_RECORD = 0x00041009 $const_PROGRESS_RECORD = 0x00041010 $const_INFORMATION_RECORD = 0x00041011 $const_PIPELINE_HOST_CALL = 0x00041100 $const_PIPELINE_HOST_RESPONSE = 0x00041101 $PSRM_message_types=@{ $const_SESSION_CAPABILITY = "Session capability" $const_INIT_RUNSPACEPOOL = "Init runspacepool" $const_PUBLIC_KEY = "Public key" $const_ENCRYPTED_SESSION_KEY = "Encrypted session key" $const_PUBLIC_KEY_REQUEST = "Public key request" $const_SET_MAX_RUNSPACES = "Set max runspaces" $const_SET_MIN_RUNSPACES = "Set min runspaces" $const_RUNSPACE_AVAILABILITY = "Runspace availability" $const_APPLICATION_PRIVATE_DATA = "Application private data" $const_GET_COMMAND_METADATA = "Get command metadata" $const_RUNSPACEPOOL_STATE = "Runspool state" $const_CREATE_PIPELINE = "Create pipeline" $const_GET_AVAILABLE_RUNSPACES = "Get available runspaces" $const_USER_EVENT = "User event" $const_RUNSPACEPOOL_HOST_CALL = "Runspacepool host call" $const_RUNSPACEPOOL_HOST_RESPONSE = "Runspacepool host response" $const_PIPELINE_STATE = "Pipeline state" $const_PIPELINE_INPUT = "Pipeline input" $const_END_OF_PIPELINE_INPUT = "End of pipeline input" $const_PIPELINE_OUTPUT = "Pipeline output" $const_PIPELINE_HOST_CALL = "Pipeline host call" $const_PIPELINE_HOST_RESPONSE = "Pipeline host response" $const_ERROR_RECORD = "Error record" $const_DEBUG_RECORD = "Debug record" $const_VERBOSE_RECORD = "Verbose record" $const_WARNING_RECORD = "Warning record" $const_PROGRESS_RECORD = "Progress record" $const_INFORMATION_RECORD = "Informaition record" $const_CONNECT_RUNSPACEPOOL = "Connect runspacepool" $const_RUNSPACEPOOL_INIT_DATA = "Runspacepool init data" $const_RESET_RUNSPACE_STATE = "Reset runspace state" } # Runspace status $const_beforeopen = 0x00 $const_opening = 0x01 $const_opened = 0x02 $const_closed = 0x03 $const_closing = 0x04 $const_broken = 0x05 $const_negotiationsent = 0x06 $const_negotiationsucceeded = 0x07 $const_connecting = 0x08 $const_disconnected = 0x09 # Invocation state $const_Notstarted = 0x00 $const_Running = 0x01 $const_Stopping = 0x02 $const_Stopped = 0x03 $const_Completed = 0x04 $const_Failed = 0x05 $const_Disconnected = 0x06 # Waiting messages $waiting_messages=@( "Your PC is almost ready..." "We're getting everything ready for you..." "Almost there..." "Back in a moment..." "This might take several minutes..." "It's taking a bit longer than expected, but we'll get there as fast as we can..." "Don't turn off your PC..." "This might take a while..." "This might take a while, I'll tell you when we're ready.." ) # Function returning a random waiting message function Get-WaitingMessage { [int]$msg=Get-Random -Minimum 0 -Maximum ($waiting_messages.Count-1) return $waiting_messages[$msg] } # Parse the PowerShell Remoting Protocol Message function Parse-PSRPMessage { [cmdletbinding()] Param( [Parameter(ParameterSetName='Base64',Mandatory=$True)] [String]$Base64Value, [Parameter(ParameterSetName='Byte',Mandatory=$True)] [Byte[]]$ByteArray, [Parameter(Mandatory=$False)] [int]$Skip=0 ) Process { # If Base64, decode to byte[] if(![String]::IsNullOrEmpty($Base64Value)) { Write-Verbose "Decoding message to bytes ($Base64Value)" [byte[]]$ByteArray = [System.Convert]::FromBase64String($Base64Value) } $messageLength = $ByteArray.Count # Check the length if($messageLength -le 4) { Throw "Message too short" } $position = $skip $messages=@() # There might be more than one message.. while($position -lt $messageLength) { # Message attributes $attributes = [ordered]@{} $ps_object_id=[byte[]]$ByteArray[($position)..($position+7)] $ps_fragment_id=[byte[]]$ByteArray[($position+8)..($position+15)] $ps_fragment=[int]$ByteArray[($position+16)] $ps_blobLength=[int][System.BitConverter]::ToUInt32([byte[]]$ByteArray[($position+20)..($position+17)],0) $ps_destination = [int]$ByteArray[($position+21)] $ps_messagetype=[int][System.BitConverter]::ToUInt32([byte[]]$ByteArray[($position+25)..($position+28)],0) $ps_rpid=[byte[]]$ByteArray[($position+29)..($position+44)] $ps_pid=[byte[]]$ByteArray[($position+45)..($position+60)] $attributes["Object Id"] = [System.BitConverter]::ToString($ps_object_id) $attributes["Fragment Id"] = [System.BitConverter]::ToString($ps_fragment_id) $attributes["Fragment"] = $PSRM_message_fragment[$ps_fragment] $attributes["Data length"] = $ps_blobLength $attributes["Destination"] = $PSRM_message_destination[$ps_destination] $attributes["Message type"] = $PSRM_message_types[$ps_messagetype] $attributes["RPID"] = ([guid]$ps_rpid).ToString() $attributes["PID"] = ([guid]$ps_pid).ToString() # Header length is 64 bytes so the actual data is after that $xmlBytes = $ByteArray[($position+64)..($position+$ps_blobLength+20)] $position += $xmlBytes.Count + 64 # The data is UTF8 text [xml]$xml=[System.Text.Encoding]::UTF8.GetString($xmlBytes) $attributes["Data"]=$xml.OuterXml $message = New-Object PSObject -Property $attributes Write-Verbose "Found message:" Write-Verbose $message $messages += $message } return $messages } } # Creates the PowerShell Remoting Protocol Message function Create-PSRPMessage { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$Data, [Parameter(Mandatory=$False)] [guid]$MSG_RPID = (New-Guid), [Parameter(Mandatory=$False)] [guid]$MSG_PID = (New-Guid), [Parameter(Mandatory=$False)] [ValidateSet('Server','Client')] [String]$Destination = "Server", [Parameter(Mandatory=$False)] [ValidateSet("Session_capability","Init_runspacepool","Public_key","Encrypted_session_key","Public_key_request","Set_max_runspaces","Set_min_runspaces","Runspace_availability","Application_private_data","Get_command_metadata","Runspool_state","Create_pipeline","Get_available_runspaces","User_event","Runspacepool_host_call","Runspacepool_host_response","Pipeline_state","Pipeline_input","End_of_pipeline_input","Pipeline_output","Pipeline_host_call","Pipeline_host_response","Error_record","Debug_record","Verbose_record","Warning_record","Progress_record","Informaition_record","Connect_runspacepool","Runspacepool_init_data","Reset_runspace_state")] [String]$Type = "Create_pipeline", [Parameter(Mandatory=$False)] [Int]$ObjectId=3 ) Process { Write-Verbose "Creating PowerShell Remote Protocol message: $Destination, $Type" $ByteArray = [System.Text.Encoding]::UTF8.getBytes($Data) $messageLength = $ByteArray.Count+43 # Add the message header size # Init the message $message=@() $ps_object_id=[byte[]]@(0,0,0,0,0,0,0,$ObjectId) $ps_fragment_id=[byte[]]@(0,0,0,0,0,0,0,0) $ps_fragment=[byte]$const_fragment_start_end $ps_blobLength=[System.BitConverter]::GetBytes([uint32]$messageLength) if($Destination -eq "Server") { $ps_destination = [byte] $const_destination_server } else { $ps_destination = [byte] $const_destination_client } $ps_messagetype=$PSRM_message_types.Keys |? { $PSRM_message_types[$_] -eq $Type.Replace("_"," ") } $ps_messagetype=[System.BitConverter]::GetBytes([uint32]$ps_messagetype) $ps_rpid=[byte[]]$MSG_RPID.ToByteArray() $ps_pid=[byte[]]$MSG_PID.ToByteArray() # Construct the message $message += $ps_object_id # 01-08 $message += $ps_fragment_id # 09-16 $message += $ps_fragment # 17 $message += $ps_blobLength[3..0] # 18-21 $message += $ps_destination # 22-25 (continues on the next line) $message += @(0x00, 0x00, 0x00) # $message += $ps_messagetype # 26-29 $message += $ps_rpid # 30-45 $message += $ps_pid # 46-61 $message += $const_bom # 62-64 $message += $ByteArray $b64Message = [System.Convert]::ToBase64String([byte[]]$message) Write-Verbose "Message created: $b64Message" return $b64Message } } # Creates a PSRP Envelope function Create-PSRPEnvelope { [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [String]$SessionId=(New-Guid).ToString(), [Parameter(Mandatory=$True)] [String]$Body, [Parameter(Mandatory=$False)] [String[]]$Option, [Parameter(Mandatory=$True)] [ValidateSet('Create','Receive','Delete','Command')] [String]$Action, [Parameter(Mandatory=$False)] [String]$Shell_Id ) Process { Write-Verbose "Creating PowerShell Remote Protocol envelope: $action, $body" switch ( $Action ) { "Command" { $action_url = 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command'} "Create" { $action_url = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create'} "Receive" { $action_url = 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive'} "Delete" { $action_url = 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete'} } $MessageId = (New-Guid).ToString().ToUpper() $OperationId = (New-Guid).ToString().ToUpper() $SequenceId="1" #$To = "https://ps.outlook.com:443/powershell?PSVersion=5.1.17134.590" $To = "https://outlook.office365.com:443/PowerShell-LiveID?BasicAuthToOAuthConversion=true&PSVersion=5.1.17763.1490" $Envelope=@" <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd"> <s:Header> <a:To>$To</a:To> <a:ReplyTo> <a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address> </a:ReplyTo> <a:Action s:mustUnderstand="true">$action_url</a:Action> <w:MaxEnvelopeSize s:mustUnderstand="true">512000</w:MaxEnvelopeSize> <a:MessageID>uuid:$MessageId</a:MessageID> <w:Locale xml:lang="en-US" s:mustUnderstand="false" /> <p:DataLocale xml:lang="en-US" s:mustUnderstand="false" /> <p:SessionId s:mustUnderstand="false">uuid:$SessionId</p:SessionId> <p:OperationID s:mustUnderstand="false">uuid:$OperationId</p:OperationID> <p:SequenceId s:mustUnderstand="false">$SequenceId</p:SequenceId> <w:ResourceURI xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">http://schemas.microsoft.com/powershell/Microsoft.Exchange</w:ResourceURI> $( if(![String]::IsNullOrEmpty($Shell_Id)) { @" <w:SelectorSet xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"> <w:Selector Name="ShellId">$Shell_Id</w:Selector> </w:SelectorSet> "@ } ) $( if(![String]::IsNullOrEmpty($Option)) { @" <w:OptionSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" s:mustUnderstand="true"> <w:Option Name="$($Option[0])">$($Option[1])</w:Option> </w:OptionSet> "@ } )<w:OperationTimeout>PT180.000S</w:OperationTimeout> </s:Header> <s:Body> $Body </s:Body> </s:Envelope> "@ # This can be used to compress the data (which we don't want to) # <rsp:CompressionType s:mustUnderstand="true" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell">xpress</rsp:CompressionType> Write-Verbose "ENVELOPE: $Envelope" return $Envelope } } # Creates a PSRP Envelope function Call-PSRP { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$Envelope, [Parameter(Mandatory=$True)] [System.Management.Automation.PSCredential]$Credentials, [Parameter(Mandatory=$False)] [Bool]$Oauth=$false ) Process { Write-Verbose "Calling the Remote PowerShell: $Envelope" $headers = @{ "Authorization" = Create-AuthorizationHeader -Credentials $Credentials "Content-Type" = "application/soap+xml;charset=UTF-8" "User-Agent" = "Microsoft WinRM Client" } $url="https://outlook.office365.com:443/PowerShell-LiveID?" # EXO Remote PS uses basic authentication header to provide the Oauth token.. if($Oauth) { $url+="BasicAuthToOauthConversion=true;" } $url += "PSVersion=5.1.17134.590" $response = Invoke-WebRequest -UseBasicParsing -Method Post -Uri $url -Headers $headers -Body $Envelope -TimeoutSec 190 Write-Verbose "RESPONSE: $response.Content" return $response.Content } } # Reads the response(s) function Receive-PSRP { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [System.Management.Automation.PSCredential]$Credentials, [Parameter()] [Bool]$Oauth=$false, [Parameter(Mandatory=$True)] [String]$SessionId, [Parameter(Mandatory=$True)] [String]$Shell_Id, [Parameter(Mandatory=$False)] [String]$CommandId ) Process { Write-Verbose "Retrieving PowerShell Remote Protocol response" $AuthHeader = Create-AuthorizationHeader -Credentials $Credentials $CommandIdString ="" if(![String]::IsNullOrEmpty($CommandId)) { $CommandIdString = " CommandId=`"$CommandId`"" } $Body = @" <rsp:Receive xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" SequenceId="0"> <rsp:DesiredStream$CommandIdString>stdout</rsp:DesiredStream> </rsp:Receive> "@ $SessionId = (New-Guid).ToString().ToUpper() $Envelope = Create-PSRPEnvelope -SessionId $SessionId -Body $Body -Action Receive -Shell_Id $Shell_Id -Option @("WSMAN_CMDSHELL_OPTION_KEEPALIVE","TRUE") $response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth Write-Verbose "RESPONSE: $response" return $response } } # Reads the response(s) and returns an array of objects function Receive-PSRPObjects { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [System.Management.Automation.PSCredential]$Credentials, [Parameter()] [Bool]$Oauth=$false, [Parameter(Mandatory=$True)] [String]$Envelope, [Parameter(Mandatory=$True)] [String]$SessionId, [Parameter(Mandatory=$True)] [String]$Shell_Id, [Parameter(Mandatory=$False)] [String]$CommandId ) Process { $return_array = @() try { # Make the command call $response = Call-PSRP -Envelope $Envelope -Credentials $Credentials -Oauth $Oauth $get_output = $true # Get the output while($get_output) { try { [xml]$response = Receive-PSRP -Credentials $Credentials -SessionId $SessionId -Shell_Id $Shell_Id -CommandId $commandId -Oauth $Oauth # Loop through streams foreach($message in $response.Envelope.Body.ReceiveResponse.Stream) { $parsed_message = Parse-PSRPMessage -Base64Value $message.'#text' [xml]$xmlData = $parsed_message.Data if($parsed_message.'Message type' -eq "Pipeline output") { # Loop thru the attributes $attributes = [ordered]@{} foreach($node in $xmlData.Obj.Props.ChildNodes) { $name = $node.N $value = $node.InnerText if($name -eq "ObjectClass") { # Special attribute.. $value=$node.LST.s[1] } $attributes[$name]=$value } $return_array+=(New-Object psobject -Property $attributes) } elseif($parsed_message.'Message type' -eq "Pipeline state") { $errorRecord = (Select-Xml -Xml $xmlData -XPath "//*[@N='ErrorRecord']").Node.'#text' if(![string]::IsNullOrEmpty($errorRecord)) { # Something went wrong, probably not an admin user Write-Error "Got an error! May be not an admin user?" Write-Verbose "ERROR: $errorRecord" } } elseif($parsed_message.'Message type' -eq "Warning record") { $warningRecord = (Select-Xml -Xml $xmlData -XPath "//*[@N='InformationalRecord_Message']").Node.'#text' if(![string]::IsNullOrEmpty($warningRecord)) { Write-Warning $warningRecord } } } # Loop thru the CommandStates foreach($state in $response.Envelope.Body.ReceiveResponse.CommandState) { # Okay, we're done! $exitCode = $state.ExitCode if(![string]::IsNullOrEmpty($exitCode)) { Write-Progress -Activity "Retrieving objects" -Completed $get_output = $false } } } catch { # Something wen't wrong so exit the loop break } } } catch { # Do nothing } return $return_array } } |