
Windows PowerShell solution to Event 5 in the 2008 Winter Scripting Games.
Solutions are also available for Windows PowerShell and Perl.
This was an event that rewarded perseverance over everything else. You didn’t have to be particularly clever to come up with a solution; however, you did have to have the patience to work through all 13 of the password tests and make sure they all functioned as expected. And even that wasn’t too bad considering the fact that 6 of the 13 tests required a simple regular expression check.
Anyway, perseverance has always been a Scripting Guys strong suit: we’re way too stubborn (or silly) to ever give up on anything. And, in this case at least, that was to our advantage: it let us come up with a script that could rate the strength of a password.
And where is that script? Why, right here, of course:
Const ForReading = 1
intScore = 13
strPassword = Wscript.Arguments(0)
blnDuplicate = False
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("C:\Scripts\WordList.txt", ForReading)
strWordList = objFile.ReadAll
objFile.Close
strWordList = Replace(strWordList, vbCrLf, " ")
strWord = " " & LCase(strPassword) & " "
intWordFound = InStr(strWordList, strWord)
If intWordFound Then
Wscript.Echo "Password found in the dictionary."
intScore = intScore - 1
End If
intLength = Len(strPassword) - 1
strWord = Left(strPassword, intLength)
strWord = " " & LCase(strWord) & " "
intWordFound = InStr(strWordList, LCase(strWord))
If intWordFound Then
Wscript.Echo "Password, minus the last letter, found in the dictionary."
intScore = intScore - 1
End If
strWord = Right(strPassword, intLength)
strWord = " " & LCase(strWord) & " "
intWordFound = InStr(strWordList, LCase(strWord))
If intWordFound Then
Wscript.Echo "Password, minus the first letter, found in the dictionary."
intScore = intScore - 1
End If
If InStr(strPassword, "0") Then
strWord = Replace(strPassword, "0", "o")
strWord = " " & LCase(strWord) & " "
intWordFound = InStr(strWordList, strWord)
If intWordFound Then
Wscript.Echo "Improper letter substitution (0 for o)."
intScore = intScore - 1
End If
End If
If Instr(strPassword, "1") Then
strWord = Replace(strPassWord, "1", "l")
strWord = " " & LCase(strWord) & " "
intWordFound = InStr(strWordList, strWord)
If intWordFound Then
Wscript.Echo "Improper letter substitution (1 for l)."
intScore = intScore - 1
End If
End If
If Len(strPassword) < 10 OR Len(strPassword) > 20 Then
Wscript.Echo "Improper password length."
intScore = intScore - 1
End If
Set objRegEx = CreateObject("VBScript.RegExp")
objRegEx.Pattern = "[0-9]"
Set colMatches = objRegEx.Execute(strPassword)
If colMatches.Count = 0 Then
Wscript.Echo "No numbers in password."
intScore = intScore - 1
End If
objRegEx.Pattern = "[A-Z]"
Set colMatches = objRegEx.Execute(strPassword)
If colMatches.Count = 0 Then
Wscript.Echo "No uppercase letters in password."
intScore = intScore - 1
End If
objRegEx.Pattern = "[a-z]"
Set colMatches = objRegEx.Execute(strPassword)
If colMatches.Count = 0 Then
Wscript.Echo "No lowercase letters in password."
intScore = intScore - 1
End If
objRegEx.Pattern = "[^A-Za-z0-9]"
Set colMatches = objRegEx.Execute(strPassword)
If colMatches.Count = 0 Then
Wscript.Echo "No symbols in password."
intScore = intScore - 1
End If
objRegEx.Pattern = "[a-z]{4}"
Set colMatches = objRegEx.Execute(strPassword)
If colMatches.Count > 0 Then
Wscript.Echo "Four consecutive lowercase letters in password."
intScore = intScore - 1
End If
objRegEx.Pattern = "[A-Z]{4}"
Set colMatches = objRegEx.Execute(strPassword)
If colMatches.Count > 0 Then
Wscript.Echo "Four consecutive uppercase letters in password."
intScore = intScore -1
End If
Set objDictionary = CreateObject("Scripting.Dictionary")
For i = 1 to Len(strPassword)
strLetter = Mid(strPassword, i, 1)
strLetter = ASC(strLetter)
If objDictionary.Exists(strLetter) Then
blnDuplicate = True
Else
objDictionary.Add strLetter, strLetter
End If
Next
If blnDuplicate = True Then
Wscript.Echo "Duplicate letters in password."
intScore = intScore - 1
End If
Wscript.Echo
If intScore <= 6 Then
Wscript.Echo "A password score of " & intScore & " indicates a weak password."
ElseIf intScore < 11 Then
Wscript.Echo "A password score of " & intScore & " indicates a moderately-strong password."
Else
Wscript.Echo "A password score of " & intScore & " indicates a strong password."
End If
Let’s quickly run through how we got the script started, then we’ll take a peek at how we performed each of the password checks. As you can see, the scripts starts out by defining a constant named ForReading and setting the value to 1; we’ll use this constant when we open the file C:\Scripts\WordList.txt. After defining the constant, we assign values to two variables:
intScore = 13 blnDuplicate = False
intScore is a variable we’ll used to keep track of the password rating; the variable starts out equal to 13 because a password that passed every single test gets a value of 13. blnDuplicate, meanwhile, is a variable that will help us keep track of whether any duplicate letters are used in the password.
Next up we use this line of code to retrieve the first command-line argument used when we started the script; per the event instructions, that argument just happens to be the password we need to check:
strPassword = Wscript.Arguments(0)
From there we create an instance of the Scripting.FileSystemObject object, then use the OpenTextFile method to open the file C:\Scripts\WordList.txt. We use the ReadAll method to read the entire contents of the file into a variable named strWordList, then use the Close method to close the text file.
That brings us to these two lines of code:
strWordList = Replace(strWordList, vbCrLf, " ") strWordList = " " & strWorldList
What’s this block of code for? Well, the file WordList.txt is simply a collection of words, one word per line:
aback abacus abaft abalone abandon abandoned
When we compare our password against this file we want to make sure that we’re always working with complete words. For example, bac is not a word; however, the string bac does appear in the word aback. If we search for bac, we run the risk of findings those characters in the middle of a word and then mistakenly reporting bac as being a real word. How can we guard against that? Well, one way is to remove all the carriage return-linefeed characters from strWordList and replace them with blank spaces. That will turn strWordList into a big long string that looks like this:
aback abacus abaft abalone abandon abandoned
Does that help? As a matter of fact, it does: now we can search for _bac_, with the underscores (_) representing blank spaces. That helps us zero in on actual words as opposed to strings of characters. The Replace function, which we use in the first line, replaces all the carriage return-linefeed characters (vbCrLf) with blank spaces (“ “). We then use the second line to add a blank space to the very beginning of strWordList. This ensures that the first word in strWordList will also be bracketed by blank spaces.
Note. Yes, there are other ways we could have approached this problem; this seemed like the easiest way to tackle it, however. |
Let’s now jump to the very end of the script. After we’ve run all the password checks, intScore will be equal to some value; we’ll use that value to determine the strength of the password. If intScore is less than or equal to 6 this means we have a weak password. A score between 7 and 10 represents a moderately-strong password, and a score of 11 or more represents a strong password. This block of code takes a look at the value of intScore and then echoes back the appropriate message:
If intScore <= 6 Then
Wscript.Echo "A password score of " & intScore & " indicates a weak password."
ElseIf intScore < 11 Then
Wscript.Echo "A password score of " & intScore & " indicates a moderately-strong password."
Else
Wscript.Echo "A password score of " & intScore & " indicates a strong password."
End If
OK, so much for all that. Now let’s take a look at our individual password tests:
Make sure that the password is not an actual word.
When it comes to creating passwords, the first thing a good security person will tell you is this: don’t use a real word as your password. Good advice. But how are we supposed to know whether or not a password is also a real word? One way is to use these two lines of code:
strWord = " " & LCase(strPassword) & " " intWordFound = InStr(strWordList, strWord)
In line 1 we’ve simply added a blank space to the beginning and the end of our password; in addition, we’ve used the LCase function to convert the password to its lowercase equivalent. (Supposedly case doesn’t matter when searching for a string value. But we prefer to err on the side of caution.) We take this new string and store it in a variable named strWord; we then use the InStr function to see if that value can be found anywhere in our word list (the variable strWordList). If the string can be found in the word list, well, that’s bad: that means that our password is a real word. Consequently, we use these two lines of code to echo back the fact that the password is a real word, and to decrement the value of intScore by 1:
Wscript.Echo "Password found in the dictionary." intScore = intScore - 1
Make sure that the password, minus the last letter, is not an actual word.
One very common “trick” users employ when creating a password is to simply add a letter to the end of an actual word. For example, instead of using the password banana, they use the password bananaX, tacking an X onto the end of the word. How can we check to see if our users are trying this little trick? (Which, of course, is as well known to hackers and crackers as it is to anyone else.) That’s actually pretty easy: all we have to do is remove the last letter and then check the resulting string against our word list.
Ah, good point: how do we remove that last letter? Like this:
intLength = Len(strPassword) - 1 strWord = Left(strPassword, intLength)
What we’re doing here is using the Len function to determine the number of characters in the password; we’re then subtracting 1 from that value and assigning the result to a variable named intLength. Why do we subtract 1 from the length? Well, bananaX has 7 characters; we want to chop off the last character, meaning that we want only the first 6 (7 – 1) characters. That, by the way, is what we do in line 2,: we use the Left function to grab the first 6 characters in strPassword and store them in the variable strWord.
After we add blank spaces to the beginning and end of strWord we again use the InStr function to see if strWord can be found anywhere in our word list. If it can, we echo back a message to this effect and decrement intScore by 1.
Make sure that the password, minus the first letter, is not an actual word.
As we noted, when it comes time to create a password some people try to “cheat” by simply tacking a character onto the end of an actual word. Others try the opposite tack: they simply attach a character to the beginning of an actual word, resulting in a password like Xbanana. How can we check to see if a password is nothing more than an actual word with a character tacked on to the beginning? You got it: we need to delete the first letter, then see if the remaining string is an actual word.
As you probably guessed, the approach we use to delete the first letter of the password is very similar to the approach we just showed you, the one that deletes the last letter of the password. To grab all the letters except the very first letter we use this line of code:
strWord = Right(strPassword, intLength)
Here we’re using the Right function to start at the end of the string and grab all the characters except the first one. (Remember, intLength is the number of characters in the password minus 1.) We add blank spaces to the beginning and the end of strWord, then use the InStr function to see if strWord can be found anywhere in our word list. If it can, then we echo back a message to this effect and decrement intScore by 1.
It’s that simple.
Make sure that the password does not simply substitute 0 (zero) for the letter o (either an uppercase O or a lowercase o).
Another common technique people use when creating a password is to take an actual word and replace any instance of the letter O with a zero; thus the word cooler gets turned into the string c00ler. How can we check to see if people are trying this old trick? Well, the easiest way we could think of was to take the password, replace all the zeroes with O’s, then see if the resulting string is a real word. This command performs that search-and-replace operation, storing the returned value in the variable strWord:
strWord = Replace(strPassword, "0", "o")
And then we do the same old thing: add blank spaces to the beginning and end of the string, then search through our word list to see if we have an actual word here. If we do, then we echo back the fact that the user has simply replaced all the O’s with zeroes, and then decrement intScore by 1.
Make sure that the password does not simply substitute 1 (one) for the letter l (either an uppercase L or a lowercase l).
Instead of replacing O’s with zeroes some people replace L’s with ones; thus a word like llama gets transformed into 11ama. To check for that behavior we need to replace all the ones in the password with L’s; that’s what this line of code is for:
strWord = Replace(strPassword, "1", "l")
From there we add our blank spaces and then check to see if the value can be found in the word list; if it can, we report this fact and decrement intScore by 1.
Note. To keep this event reasonably simple – and to add an extra condition or two to the set of password tests – we’ve assumed that users will replace O’s with zeroes or replace L’s with ones; however, they won’t do both. If you checked for that possibility – that a user might replace both the O’s and the L’s – well, that’s fine: you’ll receive full credit for the event. |
Make sure that the password is at least 10 characters long but no more than 20 characters long.
This was an easy one: all we have to do here is use the Len function to determine the number of characters in our password:
If Len(strPassword) < 10 OR Len(strPassword) > 20 Then
If the password has less than 10 characters or the password has more than 20 characters we report back the fact that the password does not meet the length requirements, and we decrement intScore by 1.
Make sure that the password includes at least one number (the digits 0 through 9).
Now it’s time to use our first regular expression. To begin with, we use this line of code to create an instance of the VBScript.RegExp object (we’ll use this same object over and over again):
Set objRegEx = CreateObject("VBScript.RegExp")
After that we assign a value to the Pattern property:
objRegEx.Pattern = "[0-9]"
As you probably know, the Pattern represents the text we’re looking for. In order to pass the test, the password must contain at least one number; that is, one of the digits from 0 to 9. The regular expression syntax [0-9] tells the script to look for any character between 0 and 9; in other words, 0, 1, 2, 3, 4, 5, 6, 7, 8, or 9.
After defining the Pattern we then use this line of code to execute the actual regular expression search:
Set colMatches = objRegEx.Execute(strPassword)
If the search turns up any instances of the target text (that is, any digit between 0 and 9) those “hits” will be stored in the collection colMatches. How can we tell if any numbers appear in our password? Well, one easy way is to check the value of the colMatches’ Count property, which tells us how many items are in the collection:
If colMatches.Count = 0 Then
If colMatches is greater than 0 that means that at least one match (i.e., one number) could be found in the password. If colMatches is 0, however, that means that the password doesn’t include any numbers. Therefore, we use this block of code to report back this finding, and to decrement intScore by 1:
Wscript.Echo "No numbers in password." intScore = intScore – 1
Make sure that the password includes at least one uppercase letter.
Regular expressions are also the easiest way to check for the presence of at least one uppercase letter. To do that we simply set our Pattern to this:
objRegEx.Pattern = "[A-Z]"
This pattern causes the script to look for any character in the range A to Z; that is, any uppercase character. If no such character can be found we echo back a message to that effect, then decrement intScore by 1.
Make sure that the password includes at least one lowercase letter.
As you might expect, this block of code is all-but identical to the preceding test, where we checked for the presence of at least one uppercase letter. The only difference? In this case we use the following pattern, which checks for a lowercase letter (that is, a character in the range a-z):
objRegEx.Pattern = "[a-z]"
If our search comes up empty we echo back a message stating that the password does not contain any lowercase letters, then decrement intScore by 1.
Make sure that the password includes at least one symbol.
For this event we’ve defined “symbol” as being any character that isn’t an uppercase letter (A to Z), a lowercase letter (a to z), or a number (0 to 9). How can we define a Pattern that will search for one of these symbols? Why, like this, of course:
objRegEx.Pattern = "[^A-Za-z0-9]"
The secret to this Pattern is the ^ character: that tells the script to search for any characters that are not in the Pattern. In this case, that means search for any character that doesn’t fall into one of these three ranges:
| • | A-Z |
| • | a-z |
| • | 0-9 |
If we can’t find such a character, we report back that the password doesn’t contain any symbols, then we decrement intScore by 1.
Make sure that the password does not include four (or) more lowercase letters in succession.
Yet another test that can easily be carried out using a regular expression; to be more specific, it can be carried out using this regular expression:
objRegEx.Pattern = "[a-z]{4}"
The first part of this Pattern – [a-z] – searches for any characters in the range a to z; in other words, any lowercase letters. The {4}, meanwhile, simply tells the script to look for four of these characters in succession; in other words, we’re looking for four lowercase letters in a row. If we find them, we echo back this fact, and decrement intScore by 1.
Make sure that the password does not include four (or more) uppercase letters in succession.
This is pretty much the same test as the one we just ran; the only difference is that we use the following pattern to look for four consecutive uppercase characters:
objRegEx.Pattern = "[A-Z]{4}"
If we find four such characters we echo back a message stating that the password contains four uppercase letters in a row, then decrement intScore by 1.
Make sure that the password does not include any duplicate characters.
One more to go and then we’re done! For our final check we want to see if all the characters in the password are unique. A password like commitment fails this test because the characters are not unique: the word includes two T’s and three M’s. At first glance this might seem a tricky thing to test for; as you’re about to see, however, it’s actually pretty easy to check for something like this.
So how do we determine whether or not our password includes any duplicate letters? To begin with, we use this line of code to create an instance of the Scripting.Dictionary object:
Set objDictionary = CreateObject("Scripting.Dictionary")
Next we set up a For Next loop that starts at 1 and runs to the end of the string, a value we can determine by using the Len function:
For i = 1 to Len(strPassword)
Each time through the loop we’ll use the Mid function to grab a letter from the password:
strLetter = Mid(strPassword, i, 1)
For example, if the password is banana then the first time through the loop strLetter will be equal to b. The second time through the loop, strLetter will be equal to a. Etc.
Next we use the ASC function to convert this character to its ASCII value:
strLetter = ASC(strLetter)
Why do we do that? Well, we need to distinguish between uppercase letters and lowercase letters; for the purposes of this event an uppercase A and a lowercase a are considered separate characters. (In other words, baTter does not include duplicate characters, while batter does.) Using the ASCII values makes it easy to differentiate between an uppercase letter and a lowercase letter.
So what are going to do with these letters? (Or, more correctly, their ASCII values.) Well, for starters, we’re going to check to see if the letter is already in our Dictionary:
If objDictionary.Exists(strLetter) Then
Suppose the Exists method returns True; what then? Well, that can mean only one thing: we’ve discovered a duplicate letter in the password. In that case, we simply set the value of the variable blnDuplicate to True:
blnDuplicate = True
If the Exists method returns False that means that the letter is – so far, anyway – unique. Therefore, we use the Add method to add this letter to the Dictionary:
objDictionary.Add strLetter, strLetter
After we’ve looped through all the letters in the password we check the value of blnDuplicate:
If blnDuplicate = True Then
If blnDuplicate is true that means our password includes duplicate letters. We echo back that fact, decrement intScore by 1, and then reach the part of the script where we analyze the score and report the results.
And that, believe it or not, is all it takes to get credit for Event 5!