Use Perl for Win32's AdminMisc extension
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:
- Microsoft's Active Directory Service Interfaces (ADSI) 2.0 and Perl for
Win32's OLE Automation extension
- ADSI 2.0 and Windows Scripting Host (WSH)
- Third-party scripting and command languages, such as Advanced Systems
Concepts' eXtended Language for Windows NT (XLNT) or FastLane Technologies'
FINAL
- Perl for Win32's AdminMisc extension
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.
The Subroutines
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.
I read Bob Wells’ interesting August Scripting Solutions column, “How to Manage Your Enterprise’s Passwords the Easy Way,” about changing the administrator’s password across Windows NT systems in one or more domains. In my situation, most of the NT systems are in a large workgroup, not a domain. Can you tell me how to modify the code to work in that environment?
--Rich Lichvar
If you have administrator access on the target machines the accounts reside on, the script should work without any changes. Of course, in a workgroup environment, you need to have multiple administrator accounts (one on each workstation, member server, domain) with the same name and password. If you tried the script and it failed, check to see that you have an administrator account on the target system with the same name and password as the account you’re running the script under.
--Bob Wells
Some customers have asked me questions about the script in “How to Manage Your Enterprise’s Passwords the Easy Way” (August), Bob Wells’ article about managing remote accounts via Perl. My customers were confused about the effects of changing the passwords of accounts that they run various services under (e.g., MTS, SQL Server, IIS). They were thinking (hoping) that if they changed the password for an account, they would not have to bounce the service. I hated to tell them they would have to bounce the service after they change the account password.
--Jim Carroll
Thanks for the feedback. As you point out, I didn’t design the password-manager script to manage service-related accounts. That is, the script doesn’t handle updating the Service Control Manager (SCM). Thus, if a service account password changes, the user will have to update the applicable service in Control Panel’s Services applet and restart the service. Thanks for reading, and keep your eyes peeled for a Service Account Password Manager script in the near future.
--Bob Wells