2008 Winter Scripting Games

Solution to Advanced Windows PowerShell Event 1: Pairing Off

Event 1 Solution


Windows PowerShell solution to Event 5 in the 2008 Winter Scripting Games.

Solutions are also available for VBScript and Perl.

*

You Call That a Strong Password?


If you want to know how the Scripting Games are put together (at least in the Advanced Division) it works something like this: one of the Scripting Guys comes up with an idea for an event, and he writes up an event description. Sometime after that, he writes up the event instructions. And then, sometime after that, he actually sits down to see if it’s even possible to write a script that can solve the problem.

Believe it or not, that approach – chaotic and disorganized as it might sound – usually works. On rare occasions, however, it does result in some last-minute scrambling; that’s invariably because the Scripting Guys realize they have no idea how to solve the problem. At other times it results in an event that isn’t quite as difficult as the Scripting Guys originally envisioned.

We’d have to say that Event 5 falls in the latter category: it’s not quite as difficult as we originally thought it might be. You didn’t have to be a virtuoso scripter to solve this problem: you just had to be able to follow directions and survive whole bunch of typing. Do that, and you were almost guaranteed to get credit for Event 5.

So what’s the end result of following directions and doing a whole bunch of typing? In the case of the Scripting Guys it’s this:

$intScore = 13
$blnDuplicate = $False

$strPassword = $args[0]

$arrContents = Get-Content "C:\Scripts\WordList.txt"
$strWordList = [string]::join(" ", $arrContents)

$strPassword2 = " " + $strPassword + " "

$match = $strWordList -match $strPassword2

if ($match -eq $True)
    {
        Write-Host "Password found in the dictionary."
        $intScore --
    }

$intLength = $strPassword.Length
$strWord = $strPassword.substring(1, $intLength - 1)

$strPassword2 = " " + $strWord + " "

$match = $strWordList -match $strPassword2

if ($match -eq $True)
    {
        Write-Host "Password, minus the first letter, found in the dictionary."
        $intScore --
    }

$intLength = $strPassword.Length
$strWord = $strPassword.substring(0, $intLength - 1)

$strPassword2 = " " + $strWord + " "

$match = $strWordList -match $strPassword2

if ($match -eq $True)
    {
        Write-Host "Password, minus the last letter, found in the dictionary."
        $intScore --
    }

$strPassword2 = " " + $strPassword + " "
$match2 = $strPassword2 –match "[0]"

If ($match2 –eq $true)
    {
        $strWord = $strPassword2 -replace("0","o")
        $match = $strWordList -match $strWord

        if ($match -eq $True)
            {
                Write-Host "Improper letter substitution (0 for o)."
                $intScore --
            }
    }

$strPassword2 = " " + $strPassword + " "
$match2 = $strPassword2 –match "[1]"

If ($match2 –eq $true)
    {
        $strWord = $strPassword2 -replace("1","l")
        $match = $strWordList -match $strWord

        if ($match -eq $True)
            {
                Write-Host "Improper letter substitution (1 for l)."
                $intScore --
            }
    }

$intLength = $strPassword.Length

if ($intLength -lt 10 -or $intLength -gt 20)
    {
         write-host "Improper password length."
         $intScore --
    }

$match = $strPassword -match "[0-9]"

if ($match -eq $False)
    {
        Write-Host "No numbers in password."
        $intScore --
    }

$match = $strPassword -cmatch "[A-Z]"

if ($match -eq $False)
    {
        Write-Host "No uppercase letters in password."
        $intScore --
    }

$match = $strPassword -cmatch "[a-z]"

if ($match -eq $False)
    {
        Write-Host "No lowercase letters in password."
        $intScore --
    }

$match = $strPassword -match "[^A-Za-z0-9]"

if ($match -eq $False)
    {
        Write-Host "No symbols in password."
        $intScore --
    }

$match = $strPassword -cmatch "[a-z]{4}"

if ($match -eq $True)
    {
        Write-Host "Four consecutive lowercase letters in password."
        $intScore --
    }

$match = $strPassword -cmatch "[A-Z]{4}"

if ($match -eq $True)
    {
        Write-Host "Four consecutive uppercase letters in password."
        $intScore --
    }

$dictionary = @{}

for ($i = 0; $i -lt $strPassword.Length; $i ++)
    {
        $strLetter = $strPassword.substring($i, 1)
        $strLetter = [byte][char] $strLetter
        $blnCheck = $dictionary.Contains($strLetter)

        if ($blnCheck -ceq $False)
            {$dictionary.Add($strLetter, 1)}
        else
            {$blnDuplicate = $True}

    }

if ($blnDuplicate -eq $True)
    {
        Write-Host "Duplicate letters in password."
        $intScore --
    }

Write-Host

if ($intScore -le 6)
    {Write-Host "A password score of $intScore indicates a weak password."; break}
elseif ($intScore -lt 11)
    {Write-Host "A password score of $intScore indicates a moderately-strong password."; break}
else
    {Write-Host "A password score of $intScore indicates a strong password."}

You know, you’re right: there is a lot going on in this script, isn’t there? Let’s see if we can figure out how it all works.

To begin with, we assign values to a couple of variables. $intScore gets a value of 13, which is the best score a proposed password can attain; $blnDuplicate, which we’ll use in one of our later tests (for duplicate letters) is assigned a value of False ($False). After taking care of these two variables we then use this line of code to retrieve the first command-line argument used when we started the script, a command line argument that also represents the password we want to test:

$strPassword = $args[0]

Next, we need to read in the contents of our list of official words; that’s something we do using the Get-Content cmdlet:

$arrContents = Get-Content "C:\Scripts\WordList.txt"

By default, any time Get-Content reads a text file each line in that file is stored as an individual item in an array. Because it’s easier (at least for us) to search a string value than it is to search an array, once we’ve read the file we then use this line of code (and the join method) to turn our array into a giant string value named $strWordList, using a blank space to separate each word in the string:

$strWordList = [string]::join(" ", $arrContents)

At this point we’re ready to start running some password tests. To begin with, we add blank spaces to the beginning and end of our password:

$strPassword2 = " " + $strPassword + " "

Why do we do that? Well, suppose we supplied a password like whistl. Is whistl a real word? No. However, the string whistl can be found in our word list; after al, those are the first 6 characters in the word whistle. By bracketing our password with blank spaces we help guarantee that, if we find anything at all, it will be a complete word, and not just part of a word.

That, by the way, is what this block of code is for:

$match = $strWordList -match $strPassword

if ($match -eq $True)
    {
        Write-Host "Password found in the dictionary."
        $intScore --
    }

All we’re doing here is use PowerShell’s –match operator, which runs a regular expression test against a string value, to see if the value stored in $strPassword2 can be found in our word list. If the –match operator returns True ($True) that means that the word was found in the word list. Consequently, we echo back the fact that the password was found in the dictionary, and subtract 1 from the variable $intScore.

Our next task is to remove the final character from the password to see if the user came up with a password simply by sticking a character to the front of a real word (e.g., 2whistle). We can extract all but the first character in a string by using these two lines of code:

$intLength = $strPassword.Length
$strWord = $strPassword.substring(1, $intLength - 1)

In line 1 we’re using the Length property to determine the number of characters in the password. In line 2, we’re using the substring method to return just a portion of the string: we’re starting with the second character (character 1) and continuing through to the end of the string.

Yes, that does sound crazy, doesn’t it? But the first character in a string is character 0 and the last character in a string is the number of characters minus 1. Our syntax, crazy as it might sound, skips the first character in the string, then returns all the other characters. In other words; everything except the first character.

After adding our blank spaces we then check to see if the password, minus the first letter, can be found in the dictionary:

$match = $strWordList -match $strPassword2

if ($match -eq $True)
    {
        Write-Host "Password, minus the first letter, found in the dictionary."
        $intScore --
    }

If it can, then we echo back the appropriate message and subtract 1 from $intScore. We then repeat this same process, this time removing the last character from the password and then checking to see if the remaining characters make up a real word.

That brings us to this block of code:

$strPassword2 = " " + $strPassword + " "
$match2 = $strPassword2 –match "[0]"

If ($match2 –eq $true)
    {
        $strWord = $strPassword2 -replace("0","o")
        $match = $strWordList -match $strWord

        if ($match -eq $True)
            {
                Write-Host "Improper letter substitution (0 for o)."
                $intScore --
            }
    }

What’s going on here? Well, after tacking on the blank spaces we next check to see if the password contains a 0 (zero). Why? Well, another common “trick” employed when creating a password is to replace the letter O with a zero; thus, reboot becomes reb00t. That’s why we’re checking to see if the password contains any zeroes. If it does, we then use this line of code to replace those zeroes with the letter ):

$strWord = $strPassword2 -replace("0","o")

We then check to see if this modified version of the word (with all the zeroes replaced by the letter O) happens to be a real word. If it is, we echo back a message to that effect and, again, decrement $intScore.

We then perform a similar check to see if the user simply replaced any L’s in the password with the number 1.

Once we’ve done that we’re finished with all the check-to-see-if-this-is-a-real-word tests. In just a moment we’re going to start a series of tests to determine whether the password contains various types of characters. Before we do that, however, we need to verify that the password has at least 10 characters, but no more than 20 characters. That’s what this block of code is for:

$intLength = $strPassword.Length

if ($intLength -lt 10 -or $intLength -gt 20)
    {
         write-host "Improper password length."
         $intScore --
    }

Now it’s time to check for the presence of various characters. In rapid-fire succession we’re going to run a series of regular expression tests:

$match = $strPassword -match "[0-9]". Here we’re trying to ensure that the password includes at least one number; that is, one character in the range 0-9.

$match = $strPassword -cmatch "[A-Z]". Here we’re trying to ensure that the password includes at least uppercase letter; that is, one character in the range A-Z. Note that we use the –cmatch operator here. That enables us to do a case-sensitive match, a match that views the letters a and A as being separate and distinct.

$match = $strPassword -cmatch "[a-z]". Here we’re trying to ensure that the password includes at least one lowercase letter; that is, one character in the range a-z. If the password does not contain a lowercase letter we’ll echo back a message and decrement $intScore by 1. Again, we use the –cmatch operator.

$match = $strPassword -match "[^A-Za-z0-9]". Here we’re trying to ensure that the password contains at least one symbol; for purposes of this event we defined “symbol” as being anything that isn’t an uppercase letter (A-Z), a lowercase letter (a-z), or a number (0-9). The caret symbol (^) is a negation operator: it tells the script to look for a character that isn’t a member of the specified character sets.

$match = $strPassword -cmatch "[a-z]{4}". Here we’re trying to ensure that the password does not include 4 lowercase letters in succession. The [a-z] means “look for a lowercase letter;” the {4} means “look for 4 of those lowercase letters, one right after the other.”

$match = $strPassword -cmatch "[A-Z]{4}". Here we’re trying to ensure that the password does not include 4 uppercase letters in succession. The [A-Z] means “look for a uppercase letter;” the {4} means “look for 4 of those uppercase letters, one right after the other.”

If and when our password fails one of these tests we echo back the appropriate message and decrement the value of $intScore by 1.

All that’s left now is to determine whether or not the password includes any duplicate characters. That’s what this block of code does:

$dictionary = @{}

for ($i = 0; $i -lt $strPassword.Length; $i ++)
    {
        $strLetter = $strPassword.substring($i, 1)
        $strLetter = [byte][char] $strLetter
        $blnCheck = $dictionary.Contains($strLetter)

        if ($blnCheck -ceq $False)
            {$dictionary.Add($strLetter, 1)}
        else
            {$blnDuplicate = $True}
    }

What are we doing here? Well, in the first line of code we’re setting up an empty hash table named $dictionary. After that we set up a for loop that loops through all the characters in the password, one-by-one. Inside that loop we grab the first character, then use this line of code to return the ASCII value for this character:

$strLetter = [byte][char] $strLetter

Why do we need the ASCII value? Well, that makes it easy for us distinguish between an uppercase A and a lowercase a. For the purposes of this event, we’re treating uppercase and lowercases letters as being separate characters. Thus the password telL does not include duplicate letters. However, the password telldoes include duplicate letters.

Next we use this line of code to determine whether or not our hash table already contains an entry for that character:

$blnCheck = $dictionary.Contains($strLetter)

If $blnCheck is False, we use the Add method to add the character to the hash table:

{$dictionary.Add($strLetter, 1)

If $blnCheck is True that means that we have a duplicate character: we’ve found a character that’s already in the hash table. In turn, we use this line of code to set the value of $blnDuplicate to True:

{$blnDuplicate = $True}

Once we’ve finished looping through all the characters in the password we then check the value of $blnDuplicate. If $blnDuplicate is True we echo back the fact that a duplicate character was found and, as usual, subtract 1 from $intScore:

if ($blnDuplicate -eq $True)
    {
        Write-Host "Duplicate letters in password."
        $intScore --
    }

All that’s left now is to take a look at the value of $intScore and then issue our official password analysis report:

if ($intScore -le 6)
    {Write-Host "A password score of $intScore indicates a weak password."; break}
elseif ($intScore -lt 11)
    {Write-Host "A password score of $intScore indicates a moderately-strong password."; break}
else
    {Write-Host "A password score of $intScore indicates a strong password."}

As you can see, if $intScore is less than or equal to 6 the password is rated as weak. If $intScore is greater than 6 but less than 11 the password is rated as moderately-strong. And if $intScore is 11 or more, the password is rated as strong.

And that, as they say, is that.


Top of pageTop of page