Use PowerShell to Copy Files over WMI

Scenario

Recently, a question came up on StackOverflow, about PowerShell, where someone wanted the ability to copy a file to a remote system via Windows Management Instrumentation (WMI). The question went on to clarify that the individual did not want to have any dependency on SMB (Server Message Block) shares, and that they specifically wanted to reuse an existing PowerShell CIM Session to c

Why Doesn’t This Work?

The issue here is that the Windows Remote Management (WinRM) service, on the remote computer, has no awareness of the WinRM client’s (aka. the “caller”) filesystem. Even if such a method existed on a WMI class, enabling the copying of files, the remote WinRM service would have to make an outbound connection back to the WinRM client in order to access its filesystem. This would introduce new questions, such as:

  • Does the caller / client’s network allow inbound connections to any services?
  • Is the remote system allowed to make outbound requests, at the network layer?
  • What credentials would be used in such an outbound request?
  • How would the credentials, for the outbound request, be provided? Are they embedded on the remote system, or are they sent to it by the caller / client?

Obviously, there are a number of complications here that the individual, who asked the StackOverflow question, didn’t want to have to deal with.

Copying Files Across PSSessions

In PowerShell 5.0, Microsoft added the ability for copying files across PowerShell Remoting sessions (PSSession), however this doesn’t apply to CIM (Common Information Model) sessions. You can do CIM operations across a PSSession, but you can’t do PSSessions across a CIM session. If you do want to use this new capability, to copy files across a PSSession, you can use the Copy-Item command with the -ToSession or -FromSession parameters.

But even so, this doesn’t solve the problem that the individual presented.

Solution

Other people have asked similar questions in the past, but received limited / broken responses, or no responses at all. However, the responses that I did see gave me some ideas about how to potentially solve the issue. For a long time, WMI has provided a Win32_Process class, which offers a static Create() method, enabling you to invoke processes remotely.

That got me to thinking … even if I can’t “copy a file” using CIM directly, and I’m not allowed to use SMB, according to the constraints on the original question, what if I could construct a PowerShell command, that wrote arbitrary bytes to a file? The next question becomes … how do I translate those bytes of data across CIM, in such a manner that they can be interpreted by a remote process invocation? The solution here is Base64. Base64 is an excellent, simple method of translating data, by representing bytes of data as a string. The Base64 encoded string can be sent using virtually any medium (even e-mail), and then translated back to a Byte array on the remote system.

What I ended up coming up with, after 10-15 minutes of tinkering and refinement, is the following PowerShell function, called Copy-FileByCim. The function accepts a few arguments:

  • The CimSession object that you pre-create
  • The LocalFile path that you want to copy to the remote system
  • The RemotePath of the file that you want to write to
function Copy-FileByCim {
    [CmdletBinding()]
    param (
        [Microsoft.Management.Infrastructure.CimSession] $CimSession
      , [string] $LocalFile
      , [string] $RemotePath
    )

    $Process = $Cim.GetClass('root\cimv2', 'Win32_Process')

    $Base64File = [System.Convert]::ToBase64String((Get-Content -Path $LocalFile -Encoding Byte -Raw))
    $Arguments = @{
        CommandLine = 'powershell.exe -Command "Set-Content -Path ''{0}'' -Value ([System.Convert]::FromBase64String(''{1}'')) -Encoding Byte; sleep 3"' -f $RemotePath, $Base64File
    }

    Write-Verbose -Message $Arguments.CommandLine
    
    $Result = Invoke-CimMethod -CimSession $CimSession -ClassName Win32_Process -MethodName Create -Arguments $Arguments
    Write-Output -InputObject $Result
}

Once you’ve imported the function into your PowerShell session, you can easily invoke the function, using the following syntax.

### Create a new CIM Session against the local system
$MyCimSession = New-CimSession -ComputerName localhost

### Copy a file from the local system to the remote system
Copy-FileByCim -CimSession $MyCimSession -LocalFile c:\windows\write.exe -RemoteFile c:\ArtofShell\write.exe

In my testing, this function was able to copy small files with relative easy. It isn’t the most elegant solution, but it demonstrates that it is possible to leverage WMI to “copy” files, so to speak, using a bit of Base64 translation.

Caveats

While this function does provide a workaround, it isn’t without its flaws. The remote WinRM service that you’re connecting to, using the CIM Session, has a configuration option called MaxEnvelopeSizeKb. As you might suspect, this setting determines the largest “envelope” (aka. payload), in kilobytes, that can be sent in a WinRM request. In other words, if you try to send a payload that’s larger than 500kb, by default, then you’ll run into a relatively user-friendly exception.

WinRM - Invoke-CimMethod Error

Invoke-CimMethod : The WinRM client sent a request to the remote WS-Management service and was notified that the request size exceeded the configured MaxEnvelopeSize quota.

Thankfully, this error tells us exactly what is wrong, and hints at us how to fix it. We can increase the MaxEnvelopeSize using PowerShell itself, however it might require you to restart the WinRM service. You can use the wsman: PSDrive to view and configure this setting for the WinRM service. Let’s take a look at how to do that.

### View the current configuration for WS-Man
Get-ChildItem -Path WSMan:\localhost

### Example Output
   WSManConfig: Microsoft.WSMan.Management\WSMan::localhost

Type            Name                           SourceOfValue   Value                               
----            ----                           -------------   -----                               
System.String   MaxEnvelopeSizekb                              500                                 
System.String   MaxTimeoutms                                   60000                               
System.String   MaxBatchItems                                  32000                               
System.String   MaxProviderRequests                            4294967295                          
Container       Client                                                                             
Container       Service                                                                            
Container       Shell                                                                              
Container       Listener                                                                           
Container       Plugin                                                                             
Container       ClientCertificate                                                                  

### Update the MaxEnvelopeSizekb to accept 1024kb (1 MB) payloads
Set-Item -Path WSMan:\localhost\MaxEnvelopeSizekb -Value 1024

### Restart the WinRM service on the remote system
### NOTE: Change localhost to your remote system's name
Get-Service -ComputerName localhost -Name winrm | Restart-Service -Force

While changing the MaxEnvelopeSize setting may not always solve your problem, another thing you could consider doing is “chunking” your data, so that the envelope is not particularly large. You could continue appending to the remote file, rather than overwriting it, each time you make a remote call.

In addition to the above problem, I also frequently encountered Return Value 8, which means “Unknown failure,” coming from the remote process invocation. Unfortunately, I don’t have a solution to this problem, or any more insight into why it happens. A screenshot is included below, for reference.

WMI Error: Unknown Failure

Conclusion

Thanks for reading, and I hope you enjoy this content. You can visit me at the following locations:

  • Hey Trevor. Great work as usual. I didn’t go read the post on StackOverflow but I have to ask..Why? Not that the solution isn’t great but why did he need to xfer a file over WMI?