2

Let's say I have the variable $password = Get-Passwd ACME\bob where I get the password of a given user. The password contains all sorts of special characters including $, that is used by PowerShell.

I need to dynamically use the password in the following command:

cmdkey.exe /add:$hostname /user:"$user" /pass:"`"$password`""

I need to escape the " character on both sides so the command is parsed as pass:"123qwe" and not pass:123qwe. The issue is that it breaks when the password includes a $ character. How can I pass the password to this command without breaking it?

JChris
  • 1,638
  • 5
  • 19
  • 37
  • If you're using a modern version of Powershell, you could use the [CredentialManager](https://www.powershellgallery.com/packages/CredentialManager/2.0) module instead of `cmdkey` and let powershell deal with the escaping: `New-StoredCredential -Target $hostname -UserName $user -Password $password` – henrycarteruk Aug 29 '18 at 10:47
  • Note: The outer `"` in `/pass:"\`"$password\`""` are unnecessary - `/pass:\`"$password\`"` will do - but the syntax is otherwise correct, and would work with any `$password` values except if they had embedded `"` chars. – mklement0 Jan 18 '20 at 13:19

2 Answers2

2

Running Executables in PowerShell

For most executables, it's not necessary to manually quote the parameters in PowerShell. Just write the command using any variables, and the PowerShell parser will automatically quote the arguments for the program's command line. This usually "just works."

cmdkey.exe /add:hostname /user:username /pass:$pass

You can inspect the actual command line that PowerShell passes to an executable by using a small program I wrote called showargs.exe, available at the following page:

https://www.itprotoday.com/management-mobility/running-executables-powershell

Example:

showargs cmdkey.exe /add:hostname /user:username /pass:$pass

The showargs.exe program simply echoes its command line to standard output so you can see the literal command line that PowerShell actually passes to the executable.

An embedded $ character shouldn't be a problem. If that character is in a PowerShell string variable, PowerShell will pass it along as a part of the command line string. Example:

$pass = 'my$pass'
cmdkey.exe /add:hostname /user:username /password:$pass

No matter how you pass the command line to the program, it's important to note that the interpretation of that command line is up to that individual program. (For example, if an executable's parser doesn't support certain characters, no amount of quoting or parsing will allow you to work around the limitation.)

Cmdkey.exe Uses a Non-Standard Parser

In my prefunctory testing of of cmdkey.exe, it seems it does not have a way of "escaping" the " character on its command line. Since this seems to be the case, you will not be able to use cmdkey.exe to store a credential that contains an embedded " character.

Embedding a Space in a Cmdkey.exe Command Line Argument

Because cmdkey.exe uses a non-standard command-line parser, you can't use a variable on its command line that contains embedded spaces. For example:

PS C:\> $pass = "my pass"
PS C:\> showargs cmdkey.exe /add:hostname /user:username /password:$pass
cmdkey.exe /add:hostname /user:username "/password:my pass"

The "/password:my pass" evidently confuses the cmdkey.exe parser, so we have to work around this behavior by bypassing PowerShell's default parsing behavior. The simplest way to do this is by escaping the quotes around the argument containing the space:

PS C:\> showargs.exe cmdkey.exe /add:hostname /user:username /password:`"$pass`"
cmdkey.exe /add:hostname /user:username /password:"my pass"

In any case, you can use showargs.exe to diagnose the trouble and work out a solution appropriate to the executable you need to run.

Bill_Stewart
  • 22,916
  • 4
  • 51
  • 62
  • When PowerShell applies quoting behind the scenes, it invariably does so around the _entire_ argument, which the target program may not recognize; indeed, a `$password` value such as `a b` breaks the command. Also, the automatic quoting isn't smart enough to escape embedded `"` chars. – mklement0 Aug 29 '18 at 03:03
  • If the case of `$password` being `a b`, PowerShell will send as `"/password:a b"`, which _should_ work if the executable uses the standard library. As far as embedded `"` characters, there's no real standard for that (the executable would have to support escaping of some kind), so yes you'd have to work around that. This is one of the caveats of putting a string containing special characters on a command line; it can be a challenge to get it parsed correctly. Depending on the capabilities of the executable's parser, some characters just cannot be passed. – Bill_Stewart Aug 29 '18 at 03:13
  • Agreed, it _should_ work, but it doesn't with `cmdkey.exe`, and, if memory serves, neither with `msiexec` - there may be others. Indeed, passing embedded `"` appears to be a fundamental challenge in PowerShell itself (irrespective of the target program) - see my answer. – mklement0 Aug 29 '18 at 03:30
  • Not actually PowerShell itself, but any command line that uses a standard parser. That is, unless the program's author created a way to escape a `"` character. – Bill_Stewart Aug 29 '18 at 12:29
  • You can verify that your approach (relying on PowerShell to enclose the _entire_ argument in double quotes on demand) doesn't work using the following command: `$password = 'a b'; cmdkey.exe /add:foo /user:bar /pass:$password`. You'll get a syntax error message. – mklement0 Aug 29 '18 at 13:21
  • Evidently `cmdkey.exe` has some interesting parsing rules, then. As I mentioned, different executables might parse differently. You can check what gets passed using [`showargs.exe`](https://www.itprotoday.com/management-mobility/running-executables-powershell). What the executable actually _does_ with the command line is up to that executable. – Bill_Stewart Aug 29 '18 at 14:12
  • (Aside: Since `cmdkey.exe` rejects an argument like `"/password:a b"`, this suggests that it's using a non-standard parser and doesn't support embedding spaces in arguments either.) – Bill_Stewart Aug 29 '18 at 16:01
  • Agreed re non-standard parser, but it does support spaces in arguments, using the very technique employed by the OP; simple example: `$password = 'a b'; cmdkey.exe /add:foo /user:bar /pass:"\`"$password\`""`. So, to summarize, I suggest adding a note that your technique works _except_ if `$password` contains spaces, because PowerShell then double-quotes the entire argument, which `cmdkey.exe` can't handle. Conversely, the OP's own attempt works in a _wider_ range of scenarios: also supports `$password` values with spaces, as long as they don't also contain double quotes. – mklement0 Aug 29 '18 at 17:36
  • Yes, the "blindly ... " statement was too broad, so I removed it. PowerShell _is_ doing things incorrectly, but only in limited circumstances now detailed in the footnote to my answer. Yes, ultimately there is no system-wide standard on Windows. But there are standard libraries, such as the C runtime and the .NET CLR, which _do_ recognize `\"` as an escaped `"`, as do all Unix-heritage utilities. In short: `\"` is the safest option. For the full story, see https://stackoverflow.com/a/31413730/45375 – mklement0 Aug 29 '18 at 21:07
  • Thanks for the lengthy answer. In my case just using `pass:$password` worked. The passwords do not contain `"` nor spaces, only `$` that I thought was tricky, but ended up being OK. – JChris Aug 29 '18 at 23:40
  • @JChris: If `pass:$password` works for you, then so does the code in your question, which is actually more robust in practice (while `pass:$password` _should_ work in all cases, it doesn't, as discussed). Bill: To summarize PowerShell's misbehavior: (a) it doesn't escape embedded `"` when passing arguments through; (b) it doesn't recognize manually escaped embedded `"` (`\"`) when deciding whether to wrap the argument as a whole in double quotes. (b) is discussed in my answer; a quick demonstration of (a): `showargs /pass:'a"b'` – mklement0 Aug 30 '18 at 01:58
  • @Bill_Stewart: It's not necessarily true that `cmdkey.exe` has a problem with embedded `"` chars., given that the following invocation works from `cmd.exe`: `cmdkey.exe /add:foo /user:bar /pass:"a \" b"` - at least it doesn't cause a _syntax_ error, the way the seemingly equivalent PowerShell command `cmdkey.exe /add:foo /user:bar /pass:"a \\`" b"` does. – mklement0 Aug 30 '18 at 02:10
  • @JChris: Bill's answer shows a simplification of your approach: `/pass:"\`"$password\`""` -> `/pass:\`"$password\`"` - that is, with a variable reference you do not need the outer `"..."` - it is this syntax that should be the most robust across different external programs (though, as stated, PowerShell has fundamental limitations with respect to embedded `"` chars,) – mklement0 Aug 30 '18 at 03:01
  • @Bill_Stewart: To summarize: It is _Windows PowerShell_ that prevents passing something like `/pass:'"a \" b"'` (with the intent to be seen as `/pass:"a \" b"` by the target program) as intended (PowerShell _Core_, even on Windows, does not have this problem); otherwise, +1 for a now solid answer, and thanks for the discussion, which deepened my own understanding. – mklement0 Aug 30 '18 at 13:43
1

tl;dr

  • Your command should work - except if $password contains " chars.

    • Embedded $ chars., by contrast, should not be a problem.
  • Your /pass:"`"$password`"" technique - i.e., explicit, embedded double-quoting - also handles values with embedded spaces correctly, unlike the /pass:$password technique (also) suggested in Bill Stewart's helpful answer.

    • You can simplify the command by omitting the outer "..." quoting, as also suggested in Bill's answer:
      /pass:`"$password`"
    • Caveat: If PowerShell's argument-passing worked correctly, these techniques shouldn't work and if it ever gets fixed, such techniques will stop working - see this answer for background.
  • As for supporting " chars. in values: even \-escaping them doesn't always work, namely when spaces are also involved - see details below.


The issue is that it breaks when the password includes a $ character.

Your command passes any $ characters embedded in the value of $password through to the target program as-is.

Therefore, if there is a problem, the implication is that your target program - cmdkey.exe - interprets $ characters, but note that the docs make no mention of that.
If it indeed does, however, you would have to escape $ characters as required by the target program in order to pass them literally.

Caveat: There is a fundamental limitation, however:

Commands typically break if the argument value contains spaces AND embedded " chars, whether the latter are properly escaped for the target program or not.

Normally, you'd escape value-internal " as \" so as not to break the enclosing double-quoting of the value:

# !! Only works if:
# !!  * $password contains NO "
# !!  * $password contains " but NOT ALSO SPACES
# !!  * $password contains PAIRS of " and what is between each
# !!    pair does not contain spaces
cmdkey.exe /add:$hostname /user:"$user" /pass:`"$($password -replace '"', '\"')`"

Note:

  • Escaping embedded " as \" is a not a standard per se, but it is recognized by most external programs; the only notable exceptions are batch files - see this answer for details.
  • Arguably, PowerShell should handle this escaping automatically - see this GitHub issue for details.

If Windows PowerShell thinks it cannot pass the resulting token as-is as a single argument to the target program, it blindly applies double-quoting around the entire token[1], which, in combination with the escaped ", can result in invalid syntax:

E.g., if $password literally contains a " b and is manually escaped as a \" b by the command above, PowerShell ends up passing the following behind the scenes:

 ...  "/pass:"a \" b""

That is, the resulting literal token that PowerShell saw - /pass:"a \" b" - was blindly enclosed in double quotes, as a whole, even though most target programs would parse /pass:"a \" b" correctly as-is.
As a result, the explicitly provided double-quoting is invalidated (as it would then require another level of escaping) - and short of using --%, the stop-parsing symbol, which then limits you to literals (or %...%-style environment-variable references), there is no way around that.

If the target program recognizes an argument if it is double-quoted as a whole (e.g.,
"/pass:a b" instead of /pass:"a b"), you can omit the explicit double-quoting around the argument value.
Do note, however, that some target programs - including cmdkey.exe and notably msiexec - do not recognize the argument if it is double-quoted as a whole.

... /pass:$($password -replace '"', '\"')

With $password literally containing a " b, PowerShell then passes behind the scenes:

... "/pass:a \" b"

This is syntactically valid - for target programs that recognize \" as an escaped ", which is the norm - but, as stated, the fact that the entire argument is enclosed in `"...", and not just the value may not be supported by the target program.


[1] Windows PowerShell ignores any \-escaping in the resulting token and considers all embedded " to have syntactic function: From that perspective, if the token is not composed of any mix of directly concatenated unquoted and double-quoted strings, enclosing double-quoting is blindly applied - which may break the command.
This behavior is not only obscure, it prevents robust, predictable parameter passing.
PowerShell Core now recognizes \" as as escaped; however, quotes that aren't pre-escaped as \" still result in broken quoting; e.g., 'a "b c" d' is passed as "a "b c" d", which target programs parse as 2 arguments, a b and c d (after quote removal).

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • To add to your "tl;dr" - it would seem that `cmdkey.exe` doesn't support embedded spaces in arguments on its command line, either. – Bill_Stewart Aug 29 '18 at 16:03
  • @Bill_Stewart: Yes, it does, using the very technique employed by the OP; simple example: `$password = 'a b'; cmdkey.exe /add:foo /user:bar /pass:"\`"$password\`""`. – mklement0 Aug 29 '18 at 17:39
  • Thanks for testing that. My original assessment seems accurate: It would seem `cmdkey.exe` does not use the standard (typical) command-line parser and does things its own (non-standard) way. – Bill_Stewart Aug 29 '18 at 18:33