PowerShell for Non-N00bs: How Much is That Character in the Window? (Or, How to Change Your Password Without Really Trying)

I'm creating a slightly different track here.  PowerShell for Non-N00bs is for me to track some of the tidbits I've found along the way.

This will be a two-fer article.  By this I mean I'm going to address two tangentially related subjects: how to find an 'odd' character that is visible, but not matching a regular expression, and, how to update stored passwords for Remote Desktop via cmdkey.exe.  Just to set expectations right, it won't be a tutorial on how to use cmdkey.exe, or a step-by-step of all the PowerShell commands leading to our final result.

===

My past article shows how I enjoy being able to log in to $remote_host, once I've established my bona fides on $local_host.  At least on Unix.  Under Windows, the same functionality exists, but it's tied to our domain credentials, which changes every CENSORED days.  This means we then get to re-log in to all the $remote_hosts and network resources we use daily, typing in the same password.

Turns out, there's a better way to do this.  cmdkey.exe allows us to manage the cached credentials, to a point.  We can list the hosts and resources (cmdkey.exe terms them "targets") for which we have credentials cached, we can add or delete that list, but we can't dump the credentials.  That makes sense - we don't want somebody finding our computer unlocked and seeing what our password is.

So, this is going to be easy.  We'll use cmdkey.exe to list the targets, maybe perform some sanity-checking to ensure we're only modifying our cached credentials for domain resources, and update it.

cmdkey.exe /list will list the cached credentials:

Currently stored credentials:

    Target: WindowsLive:(token):name=timid@msdn.com;serviceuri=www.mesh.com
Type: Generic
User: timid@msdn.com
    Local machine persistence

Target: timid-desktop
Type: Generic
User: workdomain\timid

    Target: timid-laptop
Type: Generic
User: workdomain\timid

    Target: timid-labhost
Type: Generic
User: labdomain\timid

 (Sanitized for my protection, of course.)

So, we have a few problems here.  Firstly, it's multiline output.  Secondly, I only want to change my workdomain.  Thirdly... well, that's the most interesting one, actually.  Let's go there and work back, because that's how I started this investigation.

===

 Let's just see if we can get a list of all the possible targets.  That should be easy:

(cmdkey.exe /list | select-string 'target') -replace ".*:" -replace "\s" will return only the 'Target:' lines, less all the text before and including the ":", and with all whitespace characters removed.

<name=timid@msdn.com;serviceuri=www.mesh.com>
 timid-desktop
 timid-laptop
timid-labhost 

Well, the first one got completely mashed (because the target string itself has a ":"), but we'll be skipping over it later anyhow. But, what's this with the space before the domain hosts?

"'{0}'" -f ((cmdkey.exe /list | select-string 'target') -replace ".*:" -replace "\s")[3] will print out the fourth (indexes start at 0, remember) element in the above list, with a single quote prepended and appended.

' timid-labhost '

Curiouser and curiouser.  There are spaces before and after the string, but they're not whitespace, else the "\s" metacharacter would have matched and elided them.  Wonder what they are.  Let's look at the first one.

"'{0}'" -f [byte][char](((cmdkey.exe /list | select-string 'target') -replace ".*:" -replace "\s")[3]).SubString(0,1)

[byte][char] is the PowerShell way of getting a character's ASCII value, assuming it's ASCII in the first place.  (I don't do much with Unicode yet, so I can't say for sure what will happen if you toss a double-byte character at it.)

'0'

 Zero?  That's a null.  Why is a null printing?  (This is PowerShell V1.0, because that's what we have to use.  This is probably fixed in V2, but that nirvana will have to wait.)

"'{0}'" -f ((cmdkey.exe /list | select-string 'target') -replace ".*:" -replace "\s" -replace "\0")[3]

'timid-labhost'

That's more like it.

===

The reason for this digression is, not only was this my first course of investigation, but it also plays into the "multiline data" solution.  So we have multiline data, delimited with blank lines.  Or maybe they're not blank.  Maybe there's invisible whitespace there.  With PowerShell and regular expressions, it's pretty easy to handle both.

$buffer = ""; cmdkey /list | % { if ($_ -match "^\s*$") { "$buffer"; $buffer = ""} else {$buffer += $_} }; $buffer

The "^\s*$" will match an empty line, or one that has only whitespace in it.  The "\s" metacharacter matches any whitespace, and "\s*" specifies zero or more.  "^" anchors the string to start of line, and "$" anchors the other end to the end of line.   All this says is "if we come across a blank or whitespace-only line, print our buffer and re-initialize it.  Otherwise, append the line to the buffer."

    Target: WindowsLive:(token):name=timid@msdn.com;serviceuri=www.mesh.com Type: Generic User: timid@msdn.com    Local machine persistence
    Target: timid-desktop Type: Generic User: workdomain\timid    Target: timid-laptop Type: Generic User: workdomain\timid Target: timid-labhost Type: Generic User: labdomain\timid   

How odd (not really) the buffer flush logic failed.  There must be another character  in some of those 'blank' lines.

$buffer = ""; cmdkey /list | % { if ($_ -match "^[\0\s]*$") { "$buffer"; $buffer = ""} else {$buffer += $_} }; $buffer

In addition to the earlier rigamarole about \s being whitespace, \0 is the PowerShell (.NET regex actually) way to say "a null" from our fun with -replace, earlier.  [\0\s] is a character set and means "any character matching either a null or whitespace."  The [ ] define a set, and the members are enumerated therein.

    Target: WindowsLive:(token):name=timid@msdn.com;serviceuri=www.mesh.com Type: Generic User: timid@msdn.com    Local machine persistence
    Target: timid-desktop Type: Generic User: workdomain\timid
    Target: timid-laptop Type: Generic User: workdomain\timid
Target: timid-labhost Type: Generic User: labdomain\timid   

 Okay, that's close enough for ... well, let's just say it's close enough. 

===

Let's bring it all together:

& { $buffer = ""; cmdkey /list | % { if ($_ -match "^[\0\s]*$") { "$buffer"; $buffer = ""} else {$buffer += $_} }; $buffer } | Select-String "${env:userdomain}\\${env:username}"

 & { } is PowerShell's way of saying "Do all this first".  It's similar to a ( ) clause, but () is part of a statement, and & { } groups statements.  If we were to try (1; 2), it'd fail because it's expecting a single statement, and a ";" delimits two separate statements.  Oddly enough & { } -is- a statement, so ( & { 1; 2} ) is perfectly valid.  That's good, because we need to do some chopping.

(& { $buffer = ""; cmdkey /list | % { if ($_ -match "^[\0\s]*$") { "$buffer"; $buffer = ""} else {$buffer += $_} }; $buffer } | Select-String "${env:userdomain}\\${env:username}") -replace "^[^:]*:\s" -replace "^\0" -replace "[\0\s].*" 

This looks like a mouthful, but it's just reusing the same stuff. 

We're selecting all the lines that match our work domain.

We're replacing from the beginning of the line everything that isn't a ":" up to the first ":" and any whitespace beyond.  That's the "^[^:]*:\s".

We're replacing the leading null, if any.  That's the "^\0".

We're replacing any whitespace or nulls, and any characters thereafter.   That's the "[\0\s].*".  What remains is the workdomain hostname.

===

All that remains is to set the password:

if (!$password) { $password = Read-Host; } (& { $buffer = ""; cmdkey /list | % { if ($_ -match "^[\0\s]*$") { "$buffer"; $buffer = ""} else {$buffer += $_} }; $buffer } | Select-String "${env:userdomain}\\${env:username}") -replace "^[^:]*:\s" -replace "^\0" -replace "[\0\s].*" | % { cmdkey /generic:$_ /user:"${env:userdomain}\${env:username}" /pass:$password }

To review, we learned a few things:

  • How to inspect a non-printing character that is fouling things up.
  • How to use cmdkey.exe to update cached credentials for Remote Desktop. 
  • [byte][char] is a long way to say ord().
  • When the PowerShell team said, "Saving the world, one one-liner at a time," they didn't limit themselves to 80 characters.