Irecently received an email from a reader who said, "I saw your article on Perl scripting, and I want to know if there is an automated utility I can use to change the administrator password on the local Security Accounts Manager (SAM) for every workstation within my domains. I must change my administrator password every 90 days, so it's unrealistic to walk to each machine to accomplish this task. Also, using User Manager for Domains to access each local SAM is very inefficient. If no utility exists, how do I go about creating such a tool?"
Sound familiar? I'm aware of only one commercial solution that helps with this classic Windows NT systems administration chore. Furthermore, the chore isn't limited to workstations, but includes domain and member server accounts. The good news is that you can write a simple script to automate this task.
Choose Your Weapon
You can take many different routes to write the password automation script. For example, you might use:
Because of limitations with ADSI's and WSH's support of NT 3.51, the first two options might not be your best choice. And if you don't have a third-party scripting solution, the third option isn't viable. So, Perl for Win32's AdminMisc extension is a good option to use. AdminMisc provides a variety of NT systems administration functions that are not part of the standard Perl for Win32 distribution.
Listing 1, page 192, contains an excerpt from a script, PWManager.pl, written with Perl for Win32's AdminMisc extension. (For the entire PWManager.pl script, a complete list of AdminMisc functions, and information about how to obtain and install AdminMisc, go to Windows NT Magazine's Web site at http://www.winntmag.com.) PWManager.pl is a Perl for Win32 script that manages the passwords for identically named accounts (e.g., NT's Administrator accounts) located in multiple-user account databases (e.g., SAM databases). Specifically, the script uses AdminMisc's UserChangePassword to generate a random eight-character alphanumeric password for each account. It maintains the passwords and other related data in a comma-delimited file (i.e., database) that you access in either interactive or batch mode. Interactive mode lets you change one account password in the database; batch mode lets you change all account passwords in the database. At any time, you can generate a formatted report showing the current database contents.
Requirements for Success
You must satisfy two requirements for PWManager.pl to run successfully. First, you must give the account or service that invokes the script (e.g., the NT Scheduler service) administrative privileges on the domain's Primary Domain Controller (PDC), member servers, and workstations. Second, you must properly set up the password database. The database needs to be a Comma Separated Values (.csv) file that resides in the working directory and contains one record per line, five comma-delimited fields per record. Table 1 (page 193) lists the five fields and what they must contain. One field contains the current account password. Maintaining account passwords outside NT's security subsystem is certain to raise eyebrows within many companies. Thus, make sure you have a well-defined security policy that protects and audits this file. (At a minimum, you need to store this file on an NTFS partition.) NT provides the tools with which to develop and implement this security policy.
You need to manually create the password database, ensuring that the passwords in the second field are the current passwords. If you don't know the current passwords and have administrative access to the systems, you can use AdminMisc's SetPassword function to reset the account passwords to a known state. Listing 2 contains an example script, SetPW.pl, that changes the administrator account password on each system listed in the input file, servers.txt. If SetPassword succeeds, the script displays the server name followed by a 1, which is the value of $result. The script assigns $result the return value from the SetPassword function. SetPassword returns 1 if the function succeeds or 0 if it fails. By printing the value of $result along with the hostname, the user can easily see which accounts changed and which did not. If SetPassword fails, the script displays only the server name.
SetPassword doesn't require the current password as an argument, whereas UserChangePassword does. SetPassword is equivalent to using NT's User Manager for Domains to change a password, while UserChangePassword is equivalent to using NT's Change Password dialog box. Another difference between SetPassword and UserChangePassword is who can use them. Only Administrator accounts can use SetPassword, and they can change any account password. User accounts can use UserChangePassword, but they can change only their account password.
How PWManager.pl Works
Now that you know the strategy for automating password changes and how to set up the password database, let's examine how PWManager.pl works. This script uses command-line arguments to determine what task to perform. Table 2 (page 194) lists the supported command-line arguments and corresponding actions. Figure 1 (page 194) contains examples of how you use the options with PWManager.pl.
PWManager.pl has three sections: the main script block, the format definition, and the subroutines. Because the main script block calls the subroutines, let's look at the subroutines first.
PWManager.pl uses seven subroutines: CreateHostList, CreateReport, Password, ReadRecord, SortPasswordDB, Usage, and WriteRecord. Here's a brief description of each subroutine.
CreateHostList. When the script passes a valid password database filename to this subroutine as an argument, CreateHostList returns an array of hostnames from the first field of the password database. CreateHostList uses Perl's map function to extract the server names.
CreateReport. This subroutine generates the formatted password report. CreateReport accepts two arguments: the password database filename and the username.
Password. This subroutine generates a random eight-character alphanumeric password. The script writes the new password to the second field of the password database.
ReadRecord. This subroutine fetches and returns the current password for a specified system from the password database. ReadRecord accepts two arguments: the password database filename and the server name.
SortPasswordDB. When the script passes a valid password database filename to the subroutine as an argument, SortPasswordDB sorts the password database. SortPasswordDB sorts the database records without regard to individual fields. Because field one contains the hostname, it sorts the database by hostname.
Usage. When the question mark character (?) appears in the first command-line argument, Usage prints script instructions on the console of the person executing PWManager.pl (i.e., the user). The instructions state that the user must have Administrator privileges, describe how to set up the CSV database, and provide other information and examples.
WriteRecord. This subroutine writes one data record to the password database. WriteRecord requires six values: the password database filename, server name, new password, old password, time stamp, and status.
The Main Script Block and Format Definition
Now that you know what the subroutines do, let's look at how the main script block uses them. The main script block begins by importing the AdminMisc module with the use command. The script tests to see whether the first command-line argument includes the ? character. If it does, the script calls the Usage subroutine, which displays the script instructions on the console of the person executing the script, and then exits.
The foreach block at callout A in Listing 1 parses the command-line arguments, assigning the user-assigned arguments to corresponding scalar variables. If a username or database filename is missing, the two lines after the foreach block set the $sUsername and $sPasswordDB scalars to the script defaults.
The if block at B executes when the user specifies the f=hostname option in the command line. This option retrieves the account password from the corresponding hostname record in the password database. Inside the if block, the script calls the ReadRecord subroutine, passing it the $sPasswordDB (the filename) and $sFetchPasswordFor (the hostname) scalars.
Let's look at how the script passes these parameters to the ReadRecord subroutine, which Listing 3 shows. The first statement in the ReadRecord subroutine includes the @_ symbol, which is Perl's special subroutine argument array. Perl has no concept of named formal parameters. Instead, Perl passes values to all subroutines via the built-in @_ array. The @_ array's first element, @_\[0\], contains the first parameter. Its second element, @_\[1\], contains the second parameter, and so on. Typically, a script copies the contents of the @_ array into locally scoped variables. In the ReadRecord subroutine, the first statement copies $sPasswordDB and $sFetchPasswordFor into the locally scoped variables of $db and $server. (You'll see this type of initialization statement in most of the script's subroutines.)
The ReadRecord subroutine then opens the database for read and uses the grep command (which finds matching elements in a list) to find the record containing $server, initializing @dbrecord with the result. After closing the database, the subroutine uses the split command to extract the current password (the second field of the data record) from @dbrecord. The subroutine then returns the password to $sPassword at B in Listing 1.
The print statement at B signals the end of the if block. The script prints the username and password information on the console of the person executing the script.
The if block at C changes the $sUsername password on one or all hosts based on the use of the c=hostname and c=ALL options. The process begins when the script tests the contents of $sChangePasswordOn. If the scalar contains a value, the script checks the scalar to see whether it contains the string ALL and initializes the @hosts array with the list of hostnames the CreateHostList subroutine creates. (CreateHostList uses Perl's map function to extract the server names from the password database.) If $sChangePasswordOn doesn't contain the string ALL, the block initializes the @hosts array with $sChangePasswordOn, a single hostname.
Before any password changes take place, the script performs a backup of the password database. The script then seeds Perl's rand function, which the script uses to generate random passwords in the Password subroutine.
The foreach loop at D carries out the password changes. The loop begins by assigning the first element of the @hosts array to the $host scalar. ReadRecord retrieves the current password from the password database, and the script stores it in $sOldPassword. Next, the script uses the Password subroutine to generate a new, random, eight-character alphanumeric password and stores it in $sNewPassword. The script uses Perl's time function to obtain the current timestamp and stores it in $t. The script passes the hostname, username, old password, and new password to AdminMisc's UserChangePassword function. If UserChangePassword succeeds (indicated by a return value of 1), $bResult remains TRUE; otherwise $bResult is set to FALSE.
The main body of the script then uses the WriteRecord subroutine to update the password database. On success, WriteRecord updates all the fields (except hostname) of the appropriate server record. In the event UserChangePassword fails, WriteRecord updates only the status field. The script then proceeds to the next host in the @hosts array. After the script exhausts the @hosts list, it falls out of the foreach loop and change block.
The final block at E generates the formatted report as defined by the r option in the command line. This option uses the SortPasswordDB and CreateReport subroutines to sort the database. The CreateReport subroutine uses a format definition to produce the report.
Automate Your Chores
PWManager.pl demonstrates how you can use Perl to automate an important, but time-consuming NT systems administration task. If you perform an NT administration chore that you automate with a script, drop me an email. I'm interested in your solutions; if you can't figure out a solution, I might be able to come up with one.