File Transfers

The client can copy local files to the remote host through the WinRM connection itself — no SMB, no TCP port 445, no administrative share — so it works from any client OS and needs no port beyond the WinRM one. This page explains exactly how the transfer works: where files land, which temporary files are created, how integrity is guaranteed, and how the command line is rewritten.

The two entry points

Transfer-and-run — copy script files and rewrite the command to reference the remote copies (this is what upload(...) on the fluent command builder and localFileToCopyList in the legacy WinRMCommandExecutor.execute(...)[1] do):

client.command("CSCRIPT c:\\scripts\\collect.vbs")
      .upload(Path.of("c:\\scripts\\collect.vbs"))
      .execute();

Explicit destination — copy one file to a path you choose (WinRMClient.uploadFile(...)[2], or ShellFileCopy.copyLocalFileToRemoteFile(...)[3] with a legacy executor):

client.uploadFile(Path.of("collect.ps1"), "C:\\Windows\\Temp\\collect.ps1");

Both use the same transfer engine described below; they differ only in where the file lands.

Where files land

With transfer-and-run, files are copied to a per-client-machine transfer directory on the remote host:

<windir>\Temp\winrm-upload-<CLIENT-COMPUTER-NAME>
  • <windir> is discovered on the remote host with the WQL query SELECT WindowsDirectory FROM Win32_OperatingSystem — typically C:\Windows.
  • <CLIENT-COMPUTER-NAME> is the name of the machine running the client (the COMPUTERNAME environment variable, or the local host name), which usually gives each client machine its own directory. Clients that report the same computer name (cloned machines, containers) share one — which is safe, because the content-addressed file names below prevent them from ever overwriting each other's payloads; they simply also share the cache and the 30-day cleanup. Versions before 2.0.0 used <windir>\Temp\SEN_ShareFor_<CLIENT-COMPUTER-NAME>$, exposed as a hidden SMB share — no share is created anymore, and a directory left behind by an older version is neither reused nor cleaned up: it can simply be deleted.
  • The directory is created if missing (IF NOT EXIST ... MKDIR ...).

Inside that directory the remote file name is content-addressed: a 12-hex-digit fragment of the file's SHA-256 digest is inserted before the extension:

collect.vbs  →  <windir>\Temp\winrm-upload-MYHOST\collect.1a2b3c4d5e6f.vbs

Because the name identifies the content, two files with the same name but different content get different remote paths — concurrent clients (even ones whose computer names collide) can never overwrite each other's payload between verification and execution. The flip side: a script that inspects its own file name (e.g. WScript.ScriptName) sees the digest fragment.

Overlong names are truncated (on Unicode code-point boundaries) so that the complete remote path — including the temporary-file suffixes described below — stays within the traditional Windows MAX_PATH limit (260 characters) that old hosts still enforce; the digest fragment keeps truncated names unique.

With an explicit destination (uploadFile), the file lands exactly at the path you give — no content-addressing, no renaming. The destination must be an absolute Windows path, either drive-rooted (C:\...) or UNC (\\server\share\...); relative and drive-relative (C:x.ps1) paths are rejected, because they would resolve against the remote shell's current directory. The destination directory is created if missing.

How the bytes travel

The transfer rides the already-authenticated (and, over HTTP, NTLM-encrypted) WinRM command shell — every step below is an ordinary remote command:

  1. Skip check. The destination is hashed on the host (certutil -hashfile <dest> SHA256, with a SHA1 fallback for pre-2012 hosts in the same command). If it already carries the digest of the local file, the upload is skipped entirely — re-running an identical script costs only the fixed bookkeeping (the directory-discovery query, the cleanup/MKDIR leg, and this digest probe), never the upload legs. A destination present with a different digest (e.g. corrupted in place) is remembered and repaired by replacement in step 4.

  2. Upload. The file content is base64-encoded locally (76-character lines) and appended to a remote base64 sidecar file with chunked echo commands, batched into as few command legs as possible, each under cmd.exe's command-line length limit (~8 kB per leg).

  3. Decode and verify. One command leg decodes the sidecar with certutil -f -decode into the staging file, deletes the sidecar, and hashes the staging file — the digest is compared against the locally computed one before going any further.

  4. Publish and verify again. The verified staging file is moved onto the destination:

    • if the destination did not exist, MOVE — and if a concurrent transfer of the same content won the race, the staging copy is simply discarded (a destination that already carries the right digest is never rewritten, so a copy verified by another operation cannot be invalidated);
    • if the destination pre-existed with a mismatched digest, MOVE /Y force-replaces it (repair).

    The same command leg hashes the destination one last time: the operation only succeeds if the destination provably contains the local bytes. On any failure the temporary files are deleted (best effort). A destination that already carried the correct content is never touched; but when a mismatched destination is being repaired, the replacement happens before the final verification — so a failure during a repair can leave the destination already replaced (and still unverified). Either way an unverified destination is never silently trusted: the failure is reported, and the next transfer detects the mismatch and repairs it.

An empty local file skips steps 2–4: the destination is created with TYPE NUL and verified the same way.

Temporary files

Two short-lived artifacts exist next to the destination during a transfer:

<destination>.<unique>.part        the staging file (decoded content, verified before publish)
<destination>.<unique>.part.b64    the base64 sidecar consumed by certutil -decode

<unique> combines a process-wide counter with 64 random bits, so concurrent transfers — same JVM or not — never collide. Both files are removed on success and best-effort deleted on failure.

Housekeeping

Before each transfer-and-run, entries of the transfer directory not modified for 30 days are purged (best effort, via forfiles). Content-addressing means every revision of a changing script gets a new remote name, so without this lifecycle the directory would grow without bound; the purge also reclaims staging files orphaned by an interrupted transfer. The only cost: a script unused for 30 days is re-uploaded once.

Command-line substitution

With transfer-and-run, after the files are copied, every occurrence of each local path in the command string is replaced — literally and case-insensitively — by the corresponding remote path, and the result is executed through CMD.EXE /C (...):

given:    CSCRIPT c:\scripts\collect.vbs /debug
uploads:  c:\scripts\collect.vbs
executes: CMD.EXE /C (CSCRIPT C:\Windows\Temp\winrm-upload-MYHOST\collect.1a2b3c4d5e6f.vbs /debug)

Notes:

  • The match is a literal, case-insensitive substring match on the path exactly as you passed it to upload(...)/localFileToCopyList — use the same spelling in the command (C:\Scripts\X.vbs and c:\scripts\x.vbs both match, but C:\SCRIPTS\..\SCRIPTS\X.VBS does not).
  • A listed file that the command never references is still uploaded; the command is unchanged.
  • Because the transfer legs run first, they are what creates the remote shell — so a workingDirectory(...) on the same request does not apply (the shell already exists, with its default directory, when the actual command runs).

Constraints and safety checks

  • File names must be safely embeddable in a quoted cmd.exe argument and creatable on Windows. Rejected: names containing % or ! (cmd.exe expands them even between quotes), ", control characters, the Windows-forbidden characters < > : " / \ | ? *, names ending with a dot or a space, and reserved device names (CON, NUL, COM1…, with or without extension).
  • Size: the mechanism is designed for small script files. Base64 over SOAP costs one WinRM operation per ~8 kB leg — fine for scripts, wrong for bulk data.
  • Server operation quotas: old hosts cap concurrent WinRM operations per user very low (15 on Windows 2008 R2). Transfer steps are batched to minimize operations, and a command rejected by the quota before it could run is retried with escalating delays (5/10/15/20 s).
  • Integrity: every path through the transfer ends with a digest verification of the actual destination; the digest is a transfer-integrity check (the channel itself is authenticated and, over HTTP, encrypted).
  • Host requirements: the account must be able to run remote commands and to write to the destination directory, and the host must provide certutil (transfer) and forfiles (housekeeping). See Preparing the Windows Host[4].

See also

file transfer file copy upload certutil base64 digest content-addressed temporary files winrm wql wmi windows ntlm kerberos java remote management
Links:
  • [1] apidocs/org/metricshub/winrm/command/WinRMCommandExecutor.html
  • [2] apidocs/org/metricshub/winrm/WinRMClient.html
  • [3] apidocs/org/metricshub/winrm/ShellFileCopy.html
  • [4] preparing-the-host.html
  • [5] commands.html
Searching...
No results.