Search code examples
powershellsshescapingopensshpowershell-5.1

Escaping rules for strings containing scripts through SSH


I'm trying to send a PowerShell script as a string through SSH in PowerShell 5.1. Its objective is to modify the content of a configuration file in a remote computer. I managed to fix the problems relating to escaping the special character sequences thanks to this answer in my previous question. However, I assumed that ssh $Username@$ComputerIP $stringScript would execute similarly to cmd.exe /c $stringScript but from my testing it doesn't, they are processed differently at a level I don't understand, as the code with ssh gives an error.

Here's my intended code:

$forceOnlyKeysSSH = '`nMatch all`n`tPasswordAuthentication no'
$rmtPSAuthOnlyKeys = "powershell Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config " +
    "-Value \`"$forceOnlyKeysSSH\`""
ssh -o ConnectTimeout=10 $Username@$ComputerIP $rmtPSAuthOnlyKeys

Ideally it would modify the sshd_config file with

Match all
    PasswordAuthentication no

But I'm getting this error, that to me suggest is dividing the arguments around the spaces.

Add-Content : A positional parameter cannot be found that accepts argument 'all
        PasswordAuthentication'.
At line:1 char:1
+ Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config -Val ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Add-Content], ParameterBindingException
    + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.AddContentCommand

Am I wrongly escaping the string? Why does this error not show up when calling cmd.exe /c $rmtPSAuthOnlyKeys?


Solution

    • If you're calling ssh.exe from Windows PowerShell or PowerShell 7 v7.2.x or below, you unfortunately need to compensate for a bug that affects arguments with embedded " characters.

      • It so happens that the bug is canceled out when cmd.exe /c is called directly, due to cmd.exe's own parsing quirks.

      • All other external programs are affected, however, including ssh.exe.

        • Therefore, with a default SSH server configuration on Windows, even though the command passed to the ssh.exe client is ultimately passed to cmd /c, the bug must be dealt with.

        • See the workaround below.

    • This bug is fixed in PowerShell (Core) 7 versions 7.3 and above, so no additional effort is required when calling from there.

    • See this answer for background information.


    Workaround:

    Specifically, in Windows PowerShell and PowerShell 7.2- you must manually \-escape " chars. (double quotes) that are embedded in arguments.

    Here's a cross-edition solution that applies the manual escaping only when needed:

    $forceOnlyKeysSSH = '`nMatch all`n`tPasswordAuthentication no'
    $rmtPSAuthOnlyKeys = "powershell -NoProfile Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config " +
        "-Value \`"$forceOnlyKeysSSH\`""
    if ($PSVersionTable.PSVersion -lt '7.3') { # Workaround needed
      # Manually add another round of \-escaping to the embedded, escaped \" chars.
      $rmtPSAuthOnlyKeys = $rmtPSAuthOnlyKeys.Replace('\"', '\\\"')
    }
    ssh -o ConnectTimeout=10 $Username@$ComputerIP $rmtPSAuthOnlyKeys
    

    Note:

    • As written, the value stored in $forceOnlyKeysSSH is subject to whitespace normalization, so that, e.g., foo bar would be passed as foo bar; that is, runs of two or more spaces are collapsed into a single space each.

    • If that is a concern, more work is needed, as detailed in the answer to your previous question - note that the need for the additional manual \-escaping equally applies situationally; to spell out the - more cumbersome - solution without whitespace normalization:

      $forceOnlyKeysSSH = '`nMatch all`n`tPasswordAuthentication no'
      # Note the (escaped) "..." enclosure around the (implied) -Command argument
      # and the (escaped) "^""..."^"" enclosure around $forceOnlyKeysSSH
      $rmtPSAuthOnlyKeys = "powershell -NoProfile `"Add-Content -Force -Verbose -Path c:\ProgramData\ssh\sshd_config " +
          "-Value `"^`"`"$forceOnlyKeysSSH`"^`"`"`""
      if ($PSVersionTable.PSVersion -lt '7.3') { # Workaround needed
        # Manually add another round of \-escaping to the embedded " chars.
        $rmtPSAuthOnlyKeys = $rmtPSAuthOnlyKeys.Replace('"', '\"')
      }
      ssh -o ConnectTimeout=10 $Username@$ComputerIP $rmtPSAuthOnlyKeys