Securing Active Directory Using a Roles-Based Approach

While Windows 2000 and 2003 offered a few new Built-In security groups to use in granting permissions to administrative resources, they both still leave much to be desired when designing security models. With the advent of Sarbanes-Oxley (thanks again Enron), System Administrators are now tasked with not only increasing security, but also auditing changes to the environment. (Note – change management also plays a major role regarding Sarbanes-Oxley; however, that topic is too expansive to be discussed here.)

Let’s take a slight step backwards and define exactly what role-based security is. In a nutshell, role-based security is creating ACE’s (Access Control Entries or users and groups) based upon job function and then using very specific ACL’s(Access Control Lists) to lock them down allowing administrators to have the ability to perform only their job function and nothing more. For example, if we have a junior administrator that performs basic user creation and administration as well as basic workstation management, we probably don’t want this person to have the ability to delete server accounts from AD, or modify Cisco Unity attributes on user accounts, or alter PKI certificate settings in the environment, etc. Using the built-in groups, this is quite cumbersome to do but using a role-based approach, we suddenly discover a new level of granularity that makes such detailed permissions possible.

First Steps

Before we can start creating groups for our different permissions, we must first define our roles. This will more than likely entail some interaction with the management team and HR staff to obtain the company accepted titles and their required job functions. (If you are fortunate enough to have input into this, now is a great time to redefine job responsibilities if they are misaligned to new technologies because they haven’t been updated in a while.)

Once we’ve obtained the roles within the company, we can begin to create the role-based groups in AD. These groups will mimic the roles in title so we know what they represent. For example, if we have a Sr. AD Engineer, AD Engineer II, System Administrator, and Jr. System Administrator we might want to use the following naming schemes for the groups:

* GG_Sr_AD_Eng
* GG_AD_Eng
* GG_SysAdmin
* GG_Jr_SysAdmin

We prepend the “GG” to the name so that we know it’s a Global Group. We will use Global Security Groups to nest within Domain Local Groups explained next. Remember, we can only nest Global Groups with Domain Local Groups, not the other way around. In addition, this allows us the ability to nest the Global Group within Domain Local Groups in other domains if we have a multi-domain forest (must be at least 2000 native mode).

Defining the Access Groups

Now that we have our role-based groups, it is time to create the groups that will allow the actual access to the AD resources. Since domains are simply security boundaries we would create the access groups as Domain Local Groups within each domain in our forest. (Note – we can also apply this same functionality to Forest and Site objects as well. In addition, in Windows2003 we could use Universal Groups as the replication traffic is greatly reduced since only attribute level deltas are replicated instead of the entire object.)

This part is a bit trickier than before as we need to be careful to allow for all necessary permissions for each role to be able to perform their job functions. An easy method to use in visualizing this is a spreadsheet matrix format. Let’s define the job functions of the aforementioned roles as follows (these examples are vastly simplified as there are thousands of possible configurations):

  • Sr_AD_Eng
    • Create, modify, delete users
    • Create, modify, delete workstations and servers
    • Create, modify, delete groups
    • Create, modify, delete OU
    • Create, modify, delete GPO
  • AD_Eng
    • Create, modify, delete users
    • Create, modify, delete workstations and servers
    • Create, modify, delete groups
    • Create, modify OU
    • Create, modify GPO
  • SysAdmin
    • Create, modify users
    • Create, modify workstations and servers
    • Create, modify groups
  • Jr_SysAdmin
    • Modify users
    • Create, modify workstations
    • Modify groups

Remember, each of these are Domain Local Groups that need to be created as listed below.

Our matrix would be as seen in this table.

By using our matrix, we can better visualize who has permissions to do what in the environment. Each cell in the leftmost column represents a Domain Local Group that will be created and the cells in the top row represent the Global Groups we used to define roles. This can be easily be tailored to meet any needs for any AD environment and depending upon the security definition requirements can grow to be quite large depending on how granularly the permissions in the left column are defined.

Implementing the Security Model

The next step is to actually modify the ACL’s on AD resources to meet our security requirements defined in our matrix. To do this we initially use the Delegation Wizard to create the security framework and then the ACL Editor to “tweak” the settings. We will use the DLG_Create_Modify_Delete_Users Domain Local Group in our example.

1. First, open up ADUC and navigate to the Users OU. (It’s always a good idea to move users out of the default Users container into a separate custom OU so that more granular GPO’s can be applied.) Right click on the OU and select Delegate Control. Next, click the Add button and select the DLG_Create_Modify_Delete_Users Domain Local Group created earlier then click Next to get to the Tasks to Delegate window.

2. We will want to select Create a custom task to delegate as the standard common tasks are too broad and will grant more permissions than we want which will require us to go back and make more edits in the ACL Editor later. Click Next.

3. As we are only interested in modifying the ACL’s for User objects in this OU, select the Only the following objects in this folder button. In the object window, select the User objects checkbox. Next, select the Create and Delete checkboxes underneath as we want the Domain Local Group to be able to both create and delete user objects in the Users OU. Click Next.

4. In the Permissions window, select the Property-Specific checkbox in addition to the already checked General checkbox. This brings up many more attribute options for us to work with.

Note – The list of attributes in the Permissions window is NOT a complete list of all the attributes for an object. This list is controlled by the dssec.dat file located in the %SYSTEMROOT%\System32 directory on the Domain Controllers. This file can be edited using Notepad and defines what appears in this list. For example, the City attribute does not appear by default. To modify the file to enable this attribute to appear, open the dssec.dat file in Notepad and navigate towards the bottom to the [user] section. Look for the “l” (lowercase L) attribute and change the number from a 7 to a 0 (zero) and save the file. This setting will make the City attribute appear in the list and be Read/Write enabled. You must close and open the ADUC for the changes to take effect.

5. Select the following settings in the Permissions window to grant the DLG_Create_Modify_Delete_Users Domain Local Group the ability to create, modify, and delete user accounts:

  • Read
  • Write
  • Read all Properties (enables the Read flag on all other attributes)
  • Write all Properties (enables the Write flag on all other attributes)
  • Change Password
  • Reset Password

Review the rest of the permissions and then click Next and then Finish to apply the changes.

6. Follow steps one through four for the other Domain Local Groups. Modify the selections in step five to reflect the correct settings in the matrix we used earlier for each access role.

Review and Edit Permissions using the ACL Editor

1. Right click on the User OU again and select Properties. Next, select the Security tab and then the Advanced button to bring up the Advanced Security Settings window.

2. Highlight the entry for the DLG_Create_Modify_Delete_Users Domain Local Group that has Special underneath the Permissions column and select Edit. In the editor we can view the currently selected Permissions as well as make modifications to the settings.

3. Modify the existing ACL permissions that were created by the Delegation Wizard as needed.

Configure Auditing

In order to track changes made to an object in AD, we must enable Auditing on that object and then specify the ACE (a user or group object) that will be audited as well as what parameters we are going to audit.

1. From the Advanced Security Settings window select the Auditing tab.

2. Select the Add button and add the DLG_Create_Modify_Delete_Users Domain Local Group. Click OK.

3. This brings up the Auditing Entry window in which we will define the level of auditing we want to perform for the DLG_Create_Modify_Delete_Users Domain Local Group on User objects in the Users OU. In the following example, we have selected to audit whenever a member of this group deletes a user object from this OU.

4. Repeat these steps for each additional Domain Local Group on each object that needs to be audited.

Final Steps

To bring it all together we will now nest the Global Groups into the appropriate Domain Local Groups. Using the Jr. SysAdmin role as an example we would place the GG_Jr_SysAdmin Global Group into the DLG_Modify_Users, DLG_Create_Modify_Computers, and DLG_Modify_Groups Domain Local Groups.

Documenting Domain Controller Information

Documentation is definitely one of the more tedious tasks of system administration. Documents relating to domain controller information used to be rather static; however, with the advent of new functionality in Windows 2003 such as the ability to rename domain controllers and domains themselves, even these documents have become a dynamically changing part of document maintenance.

To save time and eliminate possible errors related to documenting this information, I wrote a VBScript that will query domain controllers throughout the forest and enumerate name, IP address, GC functionality, OS version, and SP version. This information is formatted into an Excel spreadsheet and automatically places information related to different domains in the forest into a different worksheet within the workbook. The script queries the forest using RootDSE and dynamically enumerates forest and domain information without changing static variables in the script. In a nutshell; you don’t have to continuously change the code to work within different forests.

Obviously, Excel needs to be installed on the workstation where the script is run and you will need administrative privileges on the DC’s to enumerate all the information as WMI will use the credentials of the currently logged in user. The output file is placed at the root of C:\.

To run the script you can either double click on the script or run it from the command line using cscript. Click here to obtain this script in TXT format (be sure to save it with a VBS extension to use it).

Windows Password Recovery and Reset Tool

It’s your first day on the job and you’re rearing to go. The previous administrator left two weeks ago so the servers have been running on their own with no administrative maintenance. Microsoft decides that today is also the day they are going to release a number of critical update patches to the Windows Server platform. You head into the server room ready to update the servers but realize that you don’t know the administrative password to log on to the machines. To make matters even more interesting, it appears that no one else in the office does either and the previous admin didn’t document them. Thankfully, you are a dedicated reader of the articles on the 2000 Trainers site and have a solution.

Note – The following utility is not supported by Microsoft and does pose the remote possibility of permanently damaging the registry. Use at your own risk and please read all the online material before attempting. In addition, while this utility can be used maliciously, it is meant to be a “save the day” tip for administrators. Please use it responsibly.

The “Offline NT Password and Registry Editor” is located at http://home.eunet.no/~pnordahl/ntpasswd/ and can be used to reset the local administrator password on Windows platforms from Windows 3.51 to Windows 2003. The first thing you want to do is download either the floppy image or the ISO image for a CD-ROM depending on your preference. If you download the floppy image, be sure to grab the SCSI drivers if your boot partition is located on SCSI drives. For this high level walkthrough I used the floppy image.

Once you’ve unzipped the binaries, put a floppy in the drive and run the install.bat file. It will create the floppy image using the included rewrite utility. Place the floppy in the server and restart the server. After the linux kernel loads you will see the following screen:

In our example, we only have a single partition to select so we will choose device number one. The next prompt will be for the location of the registry. Just accept the default and press Enter. Since we want to reset the local administrator password, select option one at the next prompt.

At the next prompt, select option one again as we are editing user data and passwords. Notice how the local administrator account appears as an editable account at the next screen. Select the appropriate option for the administrator.

At the next screen we can change the password to whatever we want or use the asterisk wildcard to blank out the current password. Save your changes and write it back to the registry. Eject the floppy, restart the machine and log on as the administrator using the password you selected when modifying the account.

Extending the Capabilities of Active Directory Users and Computers Using VBScript

Have you ever wished that you could add functionality to the graphical tools for managing user or computer objects, but unfortunately didn’t know C++ to write the code? Even if you did had the necessary skills, you would be lucky if the Schema Admins would let you extend the schema in to utilize the new classes. The good news is that there is another way. You guessed it – VBScript to the rescue!

A while back, I needed a way to allow junior administrators the ability to modify a user attribute called employeeID. The only way they knew how to modify objects was via the Active Directory Users and Computers snap-in, so I did a little research and came across a method that I modified to enhance the tool’s functionality. Essentially, this solution gets Active Directory Users and Computers (ADUC) to call a script that allows these administrators to view or modify this hidden attribute. All these users ultimately have to do is right-click on a user object, select the Employee-ID shortcut, and then set or change its value in the pop-up dialog box that appears.

Creating the Script

The first thing to do is to write a script that handles the necessary functionality as follows:

  1. On Error Resume Next
2. Dim objemployeeID
3. Dim objUser
4. Dim objTemp
5. Set objemployeeID = Wscript.Arguments
6. Set objUser = GetObject(objemployeeID(0))
7. objTemp = InputBox("Current Employee-ID: " & objUser.employeeID & VbCrLf _

& vbCRLF & "If you would like enter a new number or modify the existing number, enter the new number in the textbox below")

8. If objTemp <> "" then objUser.Put "employeeID",objTemp
9. objUser.SetInfo
10. If Err.Number = "-2147024891" Then
11. MsgBox "You current account does not have permission" & VbCrLf _
12. & "to modify the Employee ID attribute. Please" & VbCrLf _
13. & "log on with an account with appropriate permissions.", 16, "Permission Denied"
14. End If
15. Set objUser = Nothing
16. Set objemployeeID = Nothing
17. Set objTemp = Nothing
18. WScript.Quit

(The lines are wrapped so copy and paste the script into your favorite editor for actual viewing)

Let’s briefly break the script down to review functionality:

Lines 1-4: Disable error control and declare variable names.

Line 5: Set the objemployeeID variable name to the value of the Arguments method of the WScriptobject. This variable will be used to store the information from the AD object when the menu item is selected from ADUC (covered later in the article).

Line 6: Bind the objUser variable name to the user object instantiated in Line 5.

Line 7: Use an InputBox constant to hold input and assign it to the objTemp variable name.

Line 8: Check to see if input exists for objTemp and assign that value to the employeeID attribute of the user object if the value is not NULL.

Line 9: Commit the changes to the user object.

Lines 10-14: Error handling in case the admin does not have permissions to modify the employeeID attribute of the user object.

Lines 15-18: Script cleanup and closure.

Next we need save the script (we’ll call it employeeID.vbs) and place it in a location that will be accessible by anyone who launches it when the ADUC menu item is selected. The NETLOGON share is a good location for this since everyone has read access to this share to run logon scripts. You can also create your own share on another server and ACL it accordingly to hold the script should you prefer.

Modifying the Active Directory Users and Computers Shortcut Menu

Now that we have the script created, we need a way to call it from the ADUC GUI. To do this we will need to edit the Properties of the user-Display object. The following steps will facilitate this functionality:

  1. Open ADSI Edit (located in the Support Tools folder of the Windows Server 2003 CD)
  2. Expand the CN=Configuration node and navigate to CN=DisplaySpecifiers, CN=409. Select the 409 node in the left hand pane.
  3. In the right-hand pane, select the CN=user-Display object. Right click and select Properties.
  4. Select the adminContextMenu attribute and click Edit.
  5. We now need to add the value that will be used to create the additional menu item and direct it to the employeeID.vbs script. The syntax is very important. Be sure to include the comma at the beginning and after the menu name (Employee-ID). Add the following syntax to the Value to Add: line:
    ,&Employee-ID,\\servername\sharename\employeeID.vbs
  6. Change the servername and sharename items to reflect your current environment and then click Add.
  7. Click OK to accept the changes and close ADSI Edit.
  8. Allow some time for replication to populate the changes throughout the directory.
  9. Open ADUC and select a user. Right click on the user and notice the new menu item now available.
  10. Select Employee-ID to launch the script from within the ADUC
  11. From here we can either enter a new value for the employeeID attribute for the user or hit Cancel to leave the current value intact. (Note: If no value is present in the field, then the attribute value is empty for that user.)

That’s it; we have extended the schema functionality to expose a hidden attribute for editing via the ADUC interface using VBScript. If the popup is not visible, be sure replication has occurred and double check that the path given in ADSI Edit is valid.

This opens up almost endless possibilities for modifications without knowledge of C++ or advanced coding languages that will enhance functionality of the ADUC snap-in for your administrators. The script code:


On Error Resume Next
Dim objemployeeID
Dim objUser
Dim objTemp
Set objemployeeID = Wscript.Arguments
Set objUser = GetObject(objemployeeID(0))
objTemp = InputBox("Current Employee-ID: " & objUser.employeeID & VbCrLf _
& vbCRLF & "If you would like enter a new number or modify the existing number, enter the new number in the textbox below")
if objTemp <> "" then objUser.Put "employeeID",objTemp
objUser.SetInfo
If Err.Number = "-2147024891" Then
MsgBox "You current account does not have permission" & VbCrLf _
& "to modify the Employee ID attribute. Please" & VbCrLf _
& "log on with an account with appropriate permissions.", 16, "Permission Denied"
End If
Set objUser = Nothing
Set objemployeeID = Nothing
Set objTemp = Nothing
WScript.Quit

Querying Active Directory for Computer Operating System, Service Pack, and Hotfix Details

One of the most painful tasks for administrators is keeping track of the service pack level of their servers and workstations. Thankfully, WMI affords a simple and automated method to retrieve this information from computers. Using ADSI to enumerate all machines within a domain, it is extremely simple to automate the WMI query to obtain this information as the following script demonstrates. Comments have been added for description of each process:

On Error Resume Next
'Instantiate objects
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set outfile = objFSO.OpenTextFile("C:\SP_Output.txt", 2, True)

'Obtain current domain name via RootDSE
Set objRootDSE = GetObject("LDAP://rootDSE")
strADsPath = "'"&"LDAP://" & objRootDSE.Get("defaultNamingContext")&"'"

'Connect to the current domain partition in AD and enumerate all computer
'objects via the filter to enumerate computer objects.
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Open "Provider=ADsDSOObject;"
Set objCommand = CreateObject("ADODB.Command")
objCommand.ActiveConnection = objConnection
objCommand.CommandText = "SELECT Name FROM " & strADsPath & _
" WHERE objectClass='computer'"
objCommand.Properties("Page Size")= 1000
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst

'Control Loop to cycle through enumeration results from ADO query.
Do Until objRecordSet.EOF
strCompName = objRecordSet.Fields("Name").Value
outfile.writeline UCase(strCompName)
outfile.writeline "-"
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strCompName & "\root\cimv2")

'Error handling in case the script is unable to connect to the computer.
If Err.Number <> 0 Then
On Error GoTo 0
outfile.writeline Ucase(strCompName ) & " is not responding to the query."
outfile.writeline ""
outfile.writeline "==================="
outfile.writeline ""
On Error Resume Next
Else
Set colOperatingSystems = objWMIService.ExecQuery _
("Select * from Win32_OperatingSystem")
For Each objOperatingSystem in colOperatingSystems
outfile.writeline "Operating System: " &objOperatingSystem.Caption _
& " " & objOperatingSystem.Version
outfile.writeline "Service Pack: " & objOperatingSystem.ServicePackMajorVersion _
& "." & objOperatingSystem.ServicePackMinorVersion
Next
outfile.writeline ""
outfile.writeline "==================="
outfile.writeline ""
End If
objRecordSet.MoveNext
Loop
MsgBox “Script Completed.” & VbCrLf _
& “Results located at c:\SP_Output.txt”
WScript.Quit
'Clean up
Set objFSO = Nothing
Set objConnection = Nothing
Set objCommand = Nothing

In addition to service packs, another enjoyable task is staying current with the deluge of hotfixes as they are released. Thankfully, WMI also offers a class to enumerate installed hotfixes. The above script can be modified to include hotfixes by adding a query to the appropriate class, Win32_QuickFixEngineering. For example:

Set colHotFixes = objWMIService.ExecQuery _
("Select * from Win32_QuickFixEngineering")

After adding this query to the script, add the properties to the script to output the results to the text file. Something similar to this example would work:

For Each objHotFix in colHotFixes
outfile.writeline “Description: “ & objHotFix.Description
outfile.writeline “HotFix ID : “ & objHotFix.HotFixID
outfile.writeline “Install Date: “ & objHotFix.Install.Date
Next

Don’t bother cutting and pasting the text from this file. Instead, get a copy of the completed script by accessing the text version found here. Don’t forget to save it with a VBS extension!

Then, when you’re ready to run the script, just use the cscript scriptname.vbs command.

There are other properties available for this class; however these examples are a good start. The additional properties can be found at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/win32_quickfixengineering.asp.

When it comes to automation, WMI is an extremely powerful tool that can save literally hours of administrative work as is easily demonstrated in these examples.

VBScript Code Snippets

To follow up after the last few articles on VBScript fundamentals, this article contains several different code snippets that can be used with very little modification in any Windows Active Directory environment. This article will focus on scripts and can be used to administer users and groups with ADSI. The final example is a list of several different code snippets and a logon script written using VBScript instead of batch files.

The scripts will vary a little in format which is intended to demonstrate the different methods possible in writing scripts. Some scripts declare variables while some don’t, some are broken into sections to demonstrate a template style of scripting while some are not, etc. Obviously, when taking pieces of these snippets and incorporating them into a larger script, it is best to declare variables at the beginning, group subroutines and functions together, group the main body of the script together, add comments, and use a template style of scripting that is easy to read by someone other than the script author.

Previous articles that can be used for reference with the code contained herein are:

Windows Scripting – VBScript Fundamentals and

Windows Scripting – VBScript Fundamentals (ADSI)

The following will give high-level explanations of the different examples in addition to the commented details contained within the scripts themselves:

Enable a disabled user account

  1. Assign the value of disabled to the userAccountControl attribute flag. (this value will be reversed using a bitwise operator later in the script).
  2. Bind to the user object with the GetObject method. This script demonstrates two different syntax types for this initial binding; use either LDAP or WinNT, but not both.
  3. Initialize the user object with the userAccountConrol attribute and the associated flag.
  4. Use the AND operator to determine if the flag is set to enabled (meaning the account is enabled)
  5. Use the XOR bitwise operator to reverse the flag on the userAccountControl attribute. Since it was set as a value of 2 (disabled) initially, this changes the flag to enable the account.


'-- LDAP path syntax
'===================
Set objOU = GetObject("LDAP://OU=MIS,dc=mydomain,dc=com")
Set objUser = objOU.Create("User", "cn=jsmith")
objUser.Put "sAMAccountName", "jsmith"
objUser.SetInfo

‘– Domain path syntax
‘=====================
Set objDomain = GetObject(“WinNT://Mydomain”)
Set objUser = objDomain.Create(“user”, “jsmith”)
objUser.SetInfo

‘– Enabling the user account after it has been
‘– created using the flag in the userAccountControl
‘– attribute and the XOR bit.
‘===================================================
Const ADS_UF_ACCOUNTDISABLE = 2
Set objUserACL = GetObject _
(“LDAP://cn=” & objUser & “,ou=MIS,dc=mydomain,dc=com”)
intUAC = objUserACL.Get(“userAccountControl”)
If intUAC And ADS_UF_ACCOUNTDISABLE Then
objUserACL.Put _
& “userAccountControl”, intUAC Xor ADS_UF_ACCOUNTDISABLE
objUserACL.SetInfo
End If
Next

‘– A simpler way to enable a user account
‘=========================================
Set objUser = GetObject _
(“LDAP://cn=jsmith,ou=MIS,dc=mydomain,dc=com”)
objUser.AccountDisabled = FALSE
objUser.SetInfo

Deletes a user account from Active Directory

  1. Bind to the Active Directory container that contains the user object to be deleted.
  2. Use the delete method to remove the account.
  3. The additional example uses an input box for user input when the script is run.


'-- Delete a user object from Active Directory
'=============================================

Set objOU = GetObject(“LDAP://ou=MIS,dc=mycompanydc=com”)
objOU.Delete “User”, “cn=Joe Smith”

‘– Script could also be changed to request input
‘– via a pop-up screen as follows:
‘================================================

Set objOU = GetObject(“LDAP://ou=MIS,dc=mycompanydc=com”)
Set oName = InputBox(“Enter the name of the user to be deleted”)
objOU.Delete “User”, “cn=” & oName

Add a user to a local group

  1. Bind to the local Administrators group.
  2. Bind to the user object to be added to the local group.
  3. Add the user to the local group with the Add method and the IADs path.


'Adds user object to local group by using IADs
'Properties to return AdsPath.
'=============================================
strComputer = "computer_name"
Set objGroup = GetObject _
("WinNT://" & strComputer & "/Administrators,group")
Set objUser = GetObject _
("WinNT://" & strComputer & "/jsmith,user")
objGroup.Add(objUser.ADsPath)

‘Adds user object to local group by specifying
‘object path within domain context.
‘=============================================
DomainString = “mydomain”
UserString = “jsmith”
GroupString = “Administrators”
Set objGroup = GetObject _
(“WinNT://” & DomainString & “/” & GroupString)
objGroup.Add (“WinNT://” & DomainString & “/” & UserString)

Set a user password and force user to change password at next logon.

Set a password
1. Bind to user the object
2. Set the password as specified in script

Change password at next logon
1. Bind to the user object
2. Enable the pwdLastSet attribute on the user account
3. Set the information within AD for the user account.


'-- Set password for a user object
'=================================
Set objUser = GetObject _
("LDAP://cn=jsmith,ou=MIS,dc=mydomain,dc=com")
objUser.SetPassword "P@$$w0rD"

‘– Force user to change password at next logon
‘==============================================
Set objUser = GetObject _
(“LDAP://cn=jsmith,ou=MIS,dc=mydomain,dc=com”)
objUser.Put “pwdLastSet”, 0 ‘change to 1 to disable
objUser.SetInfo

Modify user profile and home directory properties

  1. Bind to the user object
  2. Set the properties for the attributes
  3. Set the information within AD for the user object.


'-- Modify user profile and home directory
'-- properties.
'=========================================
Set objUser = GetObject _
("LDAP://cn=jsmith,ou=MIS,dc=mydomain,dc=com")
objUser.Put "profilePath", "\\ServerA\Profiles\jsmith"
objUser.Put "scriptPath", "logonscript.vbs"
objUser.Put "homeDirectory", "\\ServerA\HomeDirs\jsmith"
objUser.Put "homeDrive", "H:"
objUser.SetInfo

Create 1,000 user accounts for testing purposes

  1. Bind to the RootDSE of the directory (binds to a DC without specifying the LDAP path)
  2. Build the ADsPath to the current domain with defaultNamingContext.
  3. Using a For…Next control loop, a counter is used and a each time a user is created, the counter is incremented by one until it reaches the limit of 1000.


'-- Create 1000 User accounts for testing
'========================================
Set objRootDSE = GetObject("LDAP://rootDSE")
Set objContainer = GetObject("LDAP://cn=Users," & _
objRootDSE.Get("defaultNamingContext"))
For i = 1 To 1000
Set objLeaf = objContainer.Create("User", "cn=User" & i)
objLeaf.Put "sAMAccountName", "User" & i
objLeaf.SetInfo
Next
WScript.Echo "1000 Users created."

Adding users and global groups to a local group using a text file list as input into an array and calling a command line from within the script.

  1. Declare the variables used in the script
  2. Bind to the Scripting.FileSystemObject and use the TextFile method to create a text file for output as well as specify the text file to be used as input.
  3. Bind to the WScript.Shell object to be used in calling the command line executables.
  4. Initialize the array by using the ReadAll method on the input text file.
  5. Using a control loop…
  1. Assign the path to the executable to be called from the command line with the appropriate switches to the commandline variable.
  2. Use the Run method of the WScript.Shell object to run the command from within the script.
  3. Bind to the machine accounts in the array to determine local group memberships within the specified group and print the output in the text file.

'-- Examples of adding groups and users to
'-- the local Administrators group.
'=========================================

'-- Declare variables
'====================
Dim fso
Dim outfile
Dim infile
Dim Shell
Dim str
Dim SvrArray
Dim Group
Dim Domain
Dim oDomain

On Error Resume Next

'-- Assign values to variables
'=============================
Set fso = CreateObject("Scripting.FileSystemObject")
Set outfile = fso.OpenTextFile _
("C:\Scripting\AddDomainUsers.txt", 2, True)
Set infile = fso.OpenTextFile _
("C:\Scripting\ComputerNames.txt")
Set Shell = CreateObject("Wscript.Shell")

outfile.writeline " "
outfile.writeline "Addition of MYCOMPANY accounts" _
& "account to Local Administrators "
outfile.writeline "group started at " & Now()
outfile.writeline " "

'-- Initialize and populate array
'================================
str = infile.ReadAll()
SvrArray = Split(str, vbCrLf)

oDomain = "MY_DOMAIN"
Set Domain = GetObject("WinNT://" & oDomain)

'-- Control Loop
'===============
For Each Server In SvrArray
commandline = "C:\WINNT\System32\NET LOCALGROUP " _
& "Administrators MY_DOMAIN\Domain Users /delete"
Shell.Run commandline, 1, True
commandline = "C:\WINNT\System32\NET LOCALGROUP " _
& "Administrators MY_DOMAIN\Desktop Admins /add"
Shell.Run commandline, 1, True
commandline = "C:\WINNT\System32\NET LOCALGROUP " _
& "Administrators MY_DOMAIN\Domain Admins /add"
Shell.Run commandline, 1, True

Set User = GetObject _
("WinNT://" & Server.Name & "/Administrators")
If User.IsMember("WinNT://MY_DOMAIN/Domain Admins") Then
outfile.writeline " MY_DOMAIN\Domain Admins already exists on " _
& Server & " or computer is not reachable."
ElseIf
User.IsMember("WinNT://MY_DOMAIN/Desktop Admins") Then
outfile.writeline " MY_DOMAIN\Desktop Admins already exists on " _
& Server & " or computer is not reachable."
Else
outfile.writeline " Adding accounts to Local Adminstrators on " & Server
User.Add("WinNT://MY_DOMAIN/username")
End If
outfile.writeline " "
Next

outfile.writeline "Script completed at " & Now()
MsgBox "Add_DomainUsers script has completed."

Lists the group memberships of all users within a specified container

This script is best run using cscript your_script.vbs from the command line as it will generate large amounts of data. Another option is to incorporate the TextFile method used in the last example to re-direct the output to a text file.

  1. Set the value of the constant to the number of the error that will be returned by ADSI if the user is not a member of any groups.
  2. Bind to the users container in Active Directory.
  3. Initialize the dynamic array to contain the users within the specified container.
  4. Using a control loop…
    1. Obtain the primary group ID
    2. Initialize another array to contain the group memberships pertaining to current user in the first array. This array uses the GetEX method as users can be members of more than one group making the memberOf attribute of the user a multivalued attribute. (Basically a user can belong to more than one group and since memberOf is the attribute specifying group membership, it may have more than one value requiring the use of GetEX instead of simply Get which is used for single values.
    3. Check to see if ADSI raised the error mentioned earlier specifying no group membership. If no error is raised, display the memberships of the user.


'-- Lists group membership of all users within the
'-- specified container. Note - This script will generate
'-- A LOT of data and is best run using cscript.
'=========================================================
'=========================================================

'On Error Resume Next
'-- Uncomment the previous line to suppress ADSI run-time
'-- errors as they occur.

'-- Number value of the error return by ADSI if the
'-- memberOf attribute cannot be found.
'==================================================
Const E_ADS_PROPERTY_NOT_FOUND = &h8000500D

'-- Bind to the Users container
'==============================
Set objOU = GetObject _
("LDAP://cn=Users,dc=mydomain,dc=com")

'-- Initialize the array for user accounts.
'==========================================
ObjOU.Filter= Array("user")

'-- Control Loop
'===============
For Each objUser in objOU
Wscript.Echo objUser.cn & " is a member of: "
Wscript.Echo vbTab & "Primary Group ID: " & _
objUser.Get("primaryGroupID")

'-- Use the GetEX method to intialize the array for group
'-- membership. Get method cannot be used as it does not
'-- multivalued attributes (user can be member of many groups.)
'==============================================================
arrMemberOf = objUser.GetEx("memberOf")

'-- If the error is not raised from ADSI, then list the
'-- groups that are entries within the arrMemberOf array.
'-- If error is raised, display notification on screen.
'========================================================
If Err.Number <> E_ADS_PROPERTY_NOT_FOUND Then
For Each Group in arrMemberOf
Wscript.Echo vbTab & Group
Next
Else
Wscript.Echo vbTab & objUser.cn & " is not a member of any groups."
Err.Clear
End If
Next

Determine when a user password was last set.

  1. Bind to the user object in Active Directory
  2. Read the passwordLastChanged attribute and display results.


'-- Determine when a user password was last set
'==============================================

oUser = "John Smith"
Set objUser = GetObject _
("LDAP://cn=" & oUser & ",cn=Users,dc=mycompany,dc=com")
oPassVal = objUser.PasswordLastChanged
Wscript.Echo "Password was last set: " & oPassVal

Setting multiple values for multiple user accounts within a single container.

This example might be used if a company was acquired requiring a company name change and email address change.
  1. Bind to the container containing the user accounts to be modified.
  2. Initialize the dynamic array.
  3. Using a control loop…
    1. Change the company attribute of each user in the array to the name of the new company; in this example… NewCompany.
    2. Change the email address of each user in the array by concatenating (bringing together) the mailNickName attribute of the user and appending @NewCompany.com to the end.
    3. Set the information in Active Directory.


'-- Set multiple values for multiple accounts within
'-- a common Active Directory container.
'===================================================

'-- Change this path to reflect appropriate information.
'=======================================================
Set objOU = GetObject _
("LDAP://cn=Users,dc=mycompany,dc=com")

objOU.Filter = Array("user")

'-- Modify NewCompany and @NewCompany.com to reflect
'-- the appropriate information.
'===================================================
For Each objUser In objOU
WScript.Echo "Modified " & objUser.Name
objUser.Put "company", "NewCompany"
objUser.Put "mail", mailNickname & "@NewCompany.com"
objUser.SetInfo
WScript.Echo "The new company name is: " & _
objUser.Get("company")
WScript.Echo "The new email address is: " & _
objUser.Get("mail")
Next

Additional Code Snippets

This page is simply several different snippets of code that can be used in administering users and groups as well as an example of how VBScript can be used in logon scripting in place of batch files. The code snippets are small, easy to understand, and can be combined to create a larger script performing multiple administrative functions.


'-- Set new value for Maximum Bad Passwords Allowed
'==================================================
DomainName = "Your_Domain"
Set Domain = GetObject("WinNT://" & DomainName)
NewValue = 3 'Change this
Domain.MaxBadPasswordsAllowed = NEW_VALUE
Domain.SetInfo

'-- Set a new value for Maximum Password Age
'===========================================
DomainName = "Your_Domain"
Set Domain = GetObject("WinNT://" & DomainName)
NewValue = 2592000 'Change this
Domain.MaxPasswordAge = NewValue
Domain.SetInfo

'-- Set new value for Minimum Password Age
'=========================================
DomainName = "Your_Domain"
Set Domain = GetObject("WinNT://" & DomainName)
NewValue = 3 'Change this
Domain.MinPasswordAge = NewValue
Domain.SetInfo

'-- Set new value for Minimum Password Length
'============================================
DomainName = "Your_Domain"
Set Domain = GetObject("WinNT://" & DomainName)
NewValue = 6 'Change this
Domain.MinPasswordLength = NewValue
Domain.SetInfo

'-- Set new value for Password History
'=====================================
DomainName = "Your_Domain"
Set Domain = GetObject("WinNT://" & DomainName)
NewValue = 5 'Change this
Domain.PasswordHistoryLength = NewValue
Domain.SetInfo

'-- Enumerate computer accounts within a container
'=================================================
ContainerName = "LDAP_path_to_container"
Set Container = GetObject("LDAP://" & ContainerName)
Container.Filter = Array("Computer")
For Each Computer in Container
WScript.Echo Computer.Name
Next

'-- Enumerate user accounts within a container
'=============================================
ContainerName = "LDAP_path_to_container"
Set Container = GetObject("LDAP://" & ContainerName)
Container.Filter = Array("User")
For Each User in Container
WScript.Echo User.Name
Next

'-- Enumerate groups within a container
'======================================
ContainerName = "LDAP_path_to_container"
Set Container = GetObject("LDAP://" & ContainerName)
Container.Filter = Array("Group")
For Each Group in Container
WScript.Echo Group.Name
Next

'-- Rename a computer account
'============================
oldPC = "Current_name_of_PC"
newPC = "New_name_of_PC"
PCPath = "LDAP_path_container_PC_resides_within"
Set objNewOU = GetObject("LDAP://" & PCPath)
Set objMoveComputer = objNewOU.MoveHere _
("LDAP://CN=" & oldPC & ", OU=MIS,DC=mycompany,DC=com", _
& newPC)

'-- Create a new group
'=====================
NewGroup = "Enter_new_group_name"
Set objOU = GetObject("LDAP://ou=MIS,dc=mycompany,dc=com")
Set objGroup = objOU.Create("group", "cn=" & NewGroup)
objGroup.Put "sAMAccountName", NewGroup
objGroup.SetInfo

'-- Enumerating all disabled user accounts in a container
'-- and list results in a text file.
'========================================================
Const ADS_UF_ACCOUNTDISABLE = 2
Set FSO = CreateObject("Scripting.FileSystemObject")
Set outfile = FSO.OpenTextFile("C:\Scripts\AcctDisabled.txt", 2, True)
ContainerName = "LDAP_path_to_container"
Set Container = GetObject("LDAP://" & ContainerName)
Container.Filter = Array("User")
For Each User in Container
intUAC = User.Get("userAccountControl")
If intUAC AND ADS_UF_ACCOUNTDISABLE Then
outfile.writeline User.Name & " is disabled"
End If
Next

'-- Unlocking a user account
'===========================
UserPath = "LDAP_path_to_container"
UserName = "User_CN_name" 'i.e. John Smith
Set User = GetObject("LDAP://"& UserName & UserPath ,user")
If User.IsAccountLocked = True Then
User.IsAccountLocked = False
User.SetInfo
End If

'-- Enumerate user account expiration information
'================================================
On Error Resume Next
UserPath = "LDAP_path_to_container"
UserName = "User_CN_name" 'i.e. John Smith
Set objUser = GetObject("LDAP://" & UserName & UserPath)
dtmAccountExpiration = objUser.AccountExpirationDate

'-- Number and date is recognized standard for expired accounts.
If err.number = -2147467259 Or dtmAccountExpiration = "1/1/1970" Then
WScript.echo "Account is set to never expire."
Else
WScript.echo "Account expiration: " & objUser.AccountExpirationDate
End If

'-- Forcing Logoff and Computer Shutdown
'=======================================
'0 - Logoff
'1 - Shutdown
'2 - Reboot
'4 - Forced Logoff
'5 - Forced Shutdown
'6 - Forced Reboot
'8 - Power Off
'12 - Forced Power Off

Const SHUTDOWN = 1 'Change to reflect method
strComputer = "." 'Change to relect computer
Set objWMIService = GetObject("winmgmts: {(Shutdown)}" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colOperatingSystems = objWMIService.ExecQuery _
("SELECT * FROM Win32_OperatingSystem")
For Each objOperatingSystem in colOperatingSystems
ObjOperatingSystem.Win32Shutdown(SHUTDOWN)
Next

'-- Example Logon Script
'===============================================
'-- This script maps drives, checks for the existence of SMS and
'-- launches the SMS client installation if it does not exist as
'-- well as logging the computer name to a network text file, automates
'-- the installation of anti-virus via network installation (in this
'-- example, Trend Micro), checks for a RAS connection and bypasses
'-- A/V installation if present, and checks certain computer names
'-- to bypass installations on servers that should be skipped (i.e.
'-- Terminal Servers).
'
'-- Obviously, this would require a bit of modification to tailor to
'-- different environments, but it does illustrate how VBScript can
'-- be used in Logon Scripting instead of batch files.
'===============================================
On Error Resume Next

'-- Set variables --
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")
Set objNetwork = Wscript.CreateObject("WScript.Network")
Set colWinDir = objShell.Environment("System")
Set outfile = objFSO.OpenTextFile("\\ServerA\public\sms\" _
& objNetwork.ComputerName & ".txt", 8, True)

' === START SUBROUTINES ===

' -- SMSSub SubRoutine --
Sub SMSSub
objNetwork.MapNetworkDrive "X:", "\\ServerA\public"
WinTemp = colWinDir("WINDIR")
objBin = WinTemp & "\ms\sms\core\bin\"
objBinA = WinTemp & "\ms\sms\clicomp\apa\bin\"
objBinB = WinTemp & "\ms\sms\clicomp\hinv\"
objBinC = WinTemp & "\ms\sms\clicomp\sinv\"
objBinD = WinTemp & "\ms\sms\clicomp\swdist32\bin\"

If objFSo.FileExists(objBin & "clicore.exe") Then
WScript.Sleep 100
Else SMSCompLog
End If

If objFSo.FileExists(objBinA & "smsapm32.exe") Then
WScript.Sleep 100
Else SMSCompLog
End If

If objFSo.FileExists(objBinB & "hinv32.exe") Then
WScript.Sleep 100
Else SMSCompLog
End If

If objFSo.FileExists(objBinC & "sinv32.exe") Then
WScript.Sleep 100
Else SMSCompLog
End If

If objFSo.FileExists(objBinD & "smsmon32.exe.exe") Then
WScript.Sleep 100
Else SMSCompLog
End If

WScript.Quit
End Sub

' -- SMSCompLog SubRoutine --
Sub SMSCompLog
outfile.writeline "SMS components not installed properly on " _
& objNetwork.ComputerName & vbCrLf
objShell.Run "\\DC-01\netlogon\smsls.bat",,True
End Sub

' === END SUBROUTINES ===

' === BEGIN MAIN SCRIPT ===

'-- Remove Current mapped drives --
objNetwork.RemoveNetworkDrive "F:"
objNetwork.RemoveNetworkDrive "G:"
objNetwork.RemoveNetworkDrive "P:"
objNetwork.RemoveNetworkDrive "V:"

'-- Map Network Drives --
objNetwork.MapNetworkDrive "F:", "\\FS-01\public"
objNetwork.MapNetworkDrive "G:", "\\FS-01\group"
objNetwork.MapNetworkDrive "P:", "\\FS-01\apps"
objNetwork.MapNetworkDrive "V:", "\\FS-01\data"

'-- Check for Terminal Servers and call Subroutine --
If objNetwork.ComputerName = "TERM-SVR-01" Then
WScript.Quit
ElseIf objNetwork.ComputerName = "TERM-SVR-02" Then
WScript.Quit
ElseIf objNetwork.ComputerName = "TERM-SVR-03" Then
WScript.Quit
Else WScript.Sleep 100
End If

'-- Check for RAS using the CheckRAS utility from --
'-- the Resource Kit. --
objShell.Run "\\DC-01\netlogon\checkras.exe",,True

If Err.Number = 1 Then
SMSSub
Else objShell.Run "\\AV-SVR-01\ofcscan\autopcc"

End If

Err.Clear

SMSSub

WScript.Quit

VBScript Fundamentals for Windows Scripting – WMI

Before we begin, be forewarned that WMI encompasses A LOT of material and this will be a long article as is required to cover the material at even a high level. I will also preface the article with the fact that many of the diagrams and references included herein were derived from “The Windows Scripting Guide” by Microsoft Press which, in my opinion, is about the best book on Windows Scripting available at this time. Microsoft has done a great job in diagramming the many layers of the architecture that were used for modeling some of the diagrams seen later in this article. In addition, this article requires an intermediate to advanced knowledge of Widows Scripting and COM (Component Object Model) fundamentals. If you are not comfortable with the foundations of VBScript, please review these first two articles in this series:

VBScript Fundamentals for Windows Scripting – The Basics

VBScript Fundamentals for Windows Scripting – ADSI

In regards to managing resources on the network, you can’t find a more convenient tool than scripting with Windows Management Instrumentation, or WMI. WMI facilitates queries, monitoring, and changing settings on network resources including software, hardware, services, accounts, and many other objects. What used to require GUI interfaces to obtain information, such as how much physical memory was installed on a computer using System Properties, can now be done with one script launched from one machine against several remote machines.

The scripting syntax used with WMI is not quite as similar to that used in previous articles. It is still VBScript; however, the syntax used to connect to classes within WMI is more involved and looks more complicated. Don’t let that confuse you as you read. The majority of the code used in scripting WMI works as a template in that you can simply cut and paste the “meat and potatoes” of the code into almost every WMI script you write with minor editing to achieve the desired results. In fact Microsoft released a very cool tool called SCRIPTOMATIC that reveals the WMI classes on your machine as well as displays informational code pertaining to the WMI classes which you can modify directly or cut and paste into your own code.

Now, let’s dive into Windows Management Instrumentation beginning with a look at the WMI architecture itself.

Intro to WMI Architecture

The key to working with WMI is not actually writing the code, but finding out what classes are available to use within the script to access the required resources. For example, the Win32_Service class enables the collection of information about services, but how do find information on what other resource WMI can manage? Understanding the WMI architecture is the key to knowing what classes WMI can manage as well as what methods and properties can be used in each class. (if you need a refresher in properties and methods, please see my previous article here.)

WMI is made up of three main layers:

  1. Managed Resources
  2. WMI Infrastructure
  3. Consumers

These layers and their associated WMI components are displayed in the following diagram to demonstrate interoperability:

Figure

To obtain a better understanding, we will briefly look at the layers in reverse, beginning at the bottom as this is where the actual managed objects reside.

Managed Resources

This layer of the WMI model is quite easy to understand. It is simply the layer where items that you want to query or manage reside such as: disk drives, printers, registry, network interface cards, services, file system structures, hardware devices, etc. You can start to get an idea of how powerful WMI is by simply reading the incomplete list of resources WMI can manage in the previous sentence.

Let’s look at a simple WMI code to display the description of the loaded network adapters:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer)
Set colItems = objWMIService.InstancesOf("Win32_NetworkAdapterConfiguration")
For Each objItem in colItems
Wscript.Echo objItem.Description
Next

Depending on how many NIC’s you have installed and what types of settings are used in the network, you should receive an output similar to the following:

Figure

Note – To demonstrate the re-usability of the code, if we were to change Win32_NetworkAdapterConfiguration to read Win32_LogicalDisk and change the word Description to read DeviceID the output would list all the available drive letters on the machine.

WMI Providers

The Providers are one the main components of the core WMI architecture. Basically, the providers facilitate communication between the Consumer (the script itself) and the Managed Resources. The script example above uses the Win_32 Provider to obtain information about the network adapter as a managed resource. It is the Providers that allow the scripts to access the Win_32 API (Application Programming Interfaces) via scripting instead of needing a more robust programming language such as C++ or Visual Basic.

CIMOM

An acronym for Common Information Model Object Manager, the CIMOM facilitates interaction between Providers and Consumers. All requests sent from the WMI script to the Managed Resource pass through the CIMOM layer of the WMI architecture. It is the CIMOM layer that directs requests from the script to the appropriate Provider which then returns the requested information or performs the requested task. Let’s view this using a scripting example:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.InstancesOf("Win32_Process")
For Each objItem In colItems
Wscript.Echo objItem.Description
Next

In this script, we are returning a list of all running services on the local machine. CIMOM does not actually return the information, but rather forward the request to the Win32_Process Provider which actually does the legwork and then returns the information to the CIMOM layer of the architecture. CIMOM then forwards the returned information into the output you see as the results.

Note – You may notice that this script differs a little from the previous one in that the objWMIService variable has “\root\cimv2” appended to the end of the statement. This simply specifies the namespace that the query should be run against in the WMI Provider model. We will cover namespaces a little later. In this sample, you may remove the “\root\cimv2” and still obtain the same results providing you have not changed the default WMI namespace paths after installation.

CIM Repository

This is the object repository, or the schema, which defines all the data that is exposed by WMI. Think of the schema as a map that defines the objects stored in the repository. Using Active Directory as an example, First Name is an attribute of the User object. It is the schema that defines not only what type of information may be assigned to the First Name attribute, but also what the relationship is between the First Name attribute and the User object.

It is important to note here that while objects in the Active Directory are static for the most part, objects referenced in WMI are not. The state of the computer is dynamic and as such, a true repository, or snapshot, of the system is not possible. It is this design that can cause WMI scripts to perform slowly as they must retrieve new values each time they are run by sending requests to the CIM Repository. Querying or changing large amounts of information from several machines can take a while to complete using WMI due to the need to constantly obtain refreshed information.

The easiest way to learn about the classes, properties, and methods in the CIM repository and their relationships is to browse the repository itself. There are two tools that allow you to open the repository and visualize the interaction of these items. The WMI Tester is available as part of the default WMI install and can be located by searching for Wbemtest.exe on your machine. The second tool is called CIM Studio and may be obtained by downloading and installing the WMI Administration Tools here. Both are beneficial in examining the repository, but the CIM Studio utility offers a little more information and is a little easier to read.

Consumers

This is the highest layer in the Infrastructure model and the easiest to understand. In a nutshell, the Consumer is the script you write or the application you run that launches WMI code in the background.

CIM (Common Information Model)

This is the WMI schema that is used to store the class definitions that are used in modeling managed resources. It is the CIM definitions that allow us to make minor modifications to scripts and obtain completely different output as shown in the following two scripts:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer)
Set colItems = objWMIService.InstancesOf("Win32_OperatingSystem")
For Each objItem in colItems
Wscript.Echo "Name: " & objItem.Name
Wscript.Echo "BuildNumber: " & objItem.BuildNumber
Wscript.Echo "Manufacturer: " & objItem.Manufacturer
Next

The above script returns information regarding the Operating System currently installed on the local machine. Making some slight modifications to the script as shown below will return information about shares on the local machine.

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer)
Set colItems = objWMIService.InstancesOf("Win32_Share")
For Each objItem in colItems
Wscript.Echo "Description: " & objItem.Description
Wscript.Echo "Name: " & objItem.Name
Wscript.Echo "Path: " & objItem.Path
Next

The only changes made were to the class name (changing Win32_OperatingSystem to Win32_Share) and to the property values to return the appropriate information related to the different classes. The CIM allows us to obtain completely different results from a script without re-writing the entire script.

Note – If you are wondering how to figure out what properties are associated with what classes, it is here that the SCRIPTOMATIC tool comes into play. This tool enumerates every property for all available classes on the machines by simply selecting the appropriate class.

Namespaces

Namespaces are used to logically contain similar classes within the CIM. These are similar to folder path structures on a logical drive and follow the same rules in that while you cannot have two files with the same name in the same folder path, it is not possible to have two classes with the same name in a shared namespace. For a detailed view of the WMI Schema namespace, click here.

The default namespace is \root\cimv2 within WMI. This can be changed, but unless necessary for advanced scripting, it will usually be found within that context. You can find out what the current default namespace is on a machine by running the following script:

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colWMISettings = objWMIService.InstancesOf("Win32_WMISetting")

For Each objWMISetting in colWMISettings
Wscript.Echo "Default namespace for scripting: " & _
objWMISetting.ASPScriptDefaultNamespace
Next

Similarly, you can retrieve a list of all available namespaces by running the following script:

strComputer = "."
Call EnumNameSpaces("root")

Sub EnumNameSpaces(strNameSpace)
Wscript.Echo strNameSpace
Set objSWbemServices = _
GetObject("winmgmts:\\" & strComputer & "\" & strNameSpace)
Set colNameSpaces = objSWbemServices.InstancesOf("__NAMESPACE")
For Each objNameSpace In colNameSpaces
Call EnumNameSpaces(strNameSpace & "\" & objNameSpace.Name)
Next
End Sub

Running this using cscript from the command line returns the following output:

Figure

This script recursively searches through all available namespaces on the target computer (in this example, the local machine since we defined the strComputer variable as “.”) using a subroutine. When called, the subroutine uses the GetObject function to connect to the initial namespace identified as “root”, establishes a connection to the WMI provider, and then returns all instances of namespace beneath the current namespace using the For…Next control loop.

WMI Class Components

The CIM Repository contains several class categories. While the details of the categories are beyond the scope of this article, understanding the differences in the classes and the class types can be useful in determining classes to use in your scripting. More information on class categories and class types can be found here.

The WMI classes represent the templates mentioned earlier that define the manageability of the resource instances within WMI. Take for example the Win32_Process class. All processes in this class (which are the processes on the target machine) share the same properties which can be queried or managed using WMI. Classes represent tangible components on a computer which can be both hardware and/or software related (e.g. fixed disks, PCI cards, services, printers, network shares, etc.).

WMI Class Components – Properties

Properties are used to further describe the managed resources. For example, a NIC card has a description name, an OS installation has a build number, a monitor has a refresh rate, etc. In the following code snippet, the properties of the Win32_Process class are in bold:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.InstancesOf("Win32_Process")
For Each objItem In colItems
Wscript.Echo "Description: " & objItem.Description
Wscript.Echo "ExecutablePath: " & objItem.ExecutablePath
Wscript.Echo "Name: " & objItem.Name
Next

WMI Class Components – Methods

Methods are actions that can be taken directly upon a managed resource. Services present the best examples to demonstrate methods because of the actions that can be performed on them including stop, start, pause, install, delete, etc. In the following example we will SWbemObject in conjunction with a method to stop the Alerter service:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & _ \root\cimv2:Win32_Service.Name='Alerter'")
objWMIService.StopService

For this example the SWbemObject is actually a reference to objWMIService and the method used on the managed resource (the Alerter service) is StopService. Objects, such as the SWbemObject, declared in WMI are resources managed using the WMI Scripting Library object model which we’ll cover a little later.

WMI Class Components – Qualifiers

Qualifiers offer additional information about properties, classes, and methods. They define the operations of the called method, class or property. Using the example in the Properties section earlier, it is the qualifier that defines the properties used in the fact that the information retrieved is to be stored in an array within the control loop.

SWbemObject Concept Explained

To better understand SWbemObject, it helps to understand what an object path is. An object path is the logical path that defines an object’s location in the WMI Repository. The object path can be represented by the following:

[\\Computer Name] [\Namespace] [:Class Name] [.Property=Value]

Used in code, it would look like this:

… GetObject(“winmgmts:\\ServerA\root\cimv2:Win32_Service.Name=’Alerter’”)

The underlined portion in bold represents the object path defines as follows:

[\\Computer Name] = \\ServerA
[\Namespace] = \root\cimv2
[:Class Name] = :Win32_Service
[.Property=Value] = .Name=’Alerter’

To put it all together, consider the following script:

1. strComputer = "."

2. strNameSpace = "root\cimv2"

3. strClass = "Win32_Service"

4.

5. Set objClass = GetObject("winmgmts:\\" & strComputer & _

"\" & strNameSpace & ":" & strClass)

6.

7. For Each objClassProperty In objClass.Properties_

8.     Wscript.Echo objClassProperty.Name

9. Next

In the above script, lines 1-3 are initializing variables and assigning values, line 5 uses the GetObject function to connect to the WMI service and return the SWbemObject reference to the target class (in this example, the Win32_Service class), lines 7-9 used a For… Next control loop to display the properties collection for the Win32_Service class property. The output would resemble the following:

AcceptPause
AcceptStop
Caption
CheckPoint
CreationClassName
Description
DesktopInteract
DisplayName
ErrorControl
ExitCode
InstallDate
Name
PathName
ProcessId
ServiceSpecificExitCode
ServiceType
Started

Each of the above properties describe the Win32_Service class in that every service has a Description, a DisplayName, a Name, a value of Started (or stopped), etc…

To again illustrate the versatility and ease of use of the WMI code structure, if we were to use the same code as previously executed and simply change the three instances of the word “Properties” to the word “Methods” as seen here:

strComputer = "."

strNameSpace = "root\cimv2"

strClass = "Win32_Service"

Set objClass = GetObject("winmgmts:\\" & strComputer & _

"\" & strNameSpace & ":" & strClass)

For Each objClassMethod In objClass.Methods_

Wscript.Echo objClassMethod.Name

Next

We would return the following results using cscript from a command prompt:

Figure

As explained in the Methods section earlier, these are the actions that may be performed upon a managed resource within the Win32_Service class. In this instance, a managed resource might be the Alerter service, the Spooler service, the DHCP Server service, etc.
WMI Object Model

Now that we’ve touched on some of the WMI architecture, it is a good time to introduce the object model. Again, Microsoft has a nice flowchart of the library object model for WMI which can be found here. This will greatly aid in visualizing the process flow between the objects and instances mentioned so far. Take a few minutes to view the diagram and understand the processes occurring between objects in the library object model when different methods are called. This article will not go into details surrounding the object model; however, the following script will demonstrate the object calls made within the model.

strComputer = "ServerA"
Set objSWbemLocator = CreateObject("WbemScripting.SWbemLocator")
Set objSWbemServices = objSWbemLocator.ConnectServer _
(strComputer, "root\cimv2", "alan", "p@ssw0rd")
Set colSWbemObjectSet = objSWbemServices.InstancesOf("Win32_Process")
For Each objSWbemObject In colSWbemObjectSet
Wscript.Echo "Name: " & objSWbemObject.Name
Next

In the first line, we define the variable strComputer, next we create a reference to the SWbemLocator which resides at the top of the object model library (use the diagram for reference) using the CreateObject function. In the third line, we do two things: first we use the ConnectServer method of the SWbemLocator object to connect to the \root\cimv2 WMI namespace and second, pass user credentials to authenticate against the namespace on the remote server. This is only necessary if you are running the script in the context of a user account without administrative privileges on the remote computer. Now that we have connected to the WMI service, we again use the reference to connect to the Win32_Process class in line 4 (line 3 wraps around). Finally, we use the For… Next control loop in lines 5-7 to display the name property of each instance of the Win32_Process class.

For general WMI scripts used for tasks such as finding all the services on a machine, it is not completely necessary to understand every aspect of the WMI object model. However, a solid knowledge of the Automation objects (SWbem*) within the model is the beginning to writing more advanced WMI scripts.

Specifying Return Parameters in Queries

When using WMI to obtain information, it is not always desired to obtain all information regarding every instance of the object class being queried. Take the Event Logs for example. If you were to run the following script:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & _ "\root\cimv2")
Set colItems = objWMIService.InstancesOf("Win32_NTLogEvent")
For Each objItem in colItems
Wscript.Echo "EventCode: " & objItem.EventCode
Wscript.Echo "User: " & objItem.User
Wscript.Echo "Logfile: " & objItem.Logfile
Wscript.Echo "Message: " & objItem.Message
Next

You would receive information on every Event ID from every Event Log on the local system. While that is a lot of information, let’s take it a step further. Let’s say that every machine in an organization has a log file size limit of 30MB and that we use an array to query 50 different machines in one script. Since WMI scripts store their data in memory as they run, guess what’s going to occur. That’s right… system crash when we run out of available memory with the script process still hung running.

To narrow down the returned information in our script, we can use the WHERE clause in our variable statements. Up to now, we have been using the InstancesOf method in our script. To specify tighter query parameters, we will now use a new method called ExecQuery which replaces the InstancesOf method in the syntax. Compare the method below using our previous script with some modifications:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & _
"\root\cimv2")
Set colItems = objWMIService.ExecQuery("SELECT * FROM " & _
"Win32_NTLogEvent WHERE Logfile = 'System'")
For Each objItem in colItems
Wscript.Echo "EventCode: " & objItem.EventCode
Wscript.Echo "User: " & objItem.User
Wscript.Echo "Logfile: " & objItem.Logfile
Wscript.Echo "Message: " & objItem.Message
Next

This would return specified information from the System Log only. Let’s examine the script more closely to identify the processes associated with the new ExecQuery method:

The first line sets the strComputer variable to the local machine. The second line (wrapped) uses the GetObject method to attach to the \root\cimv2 namespace on the local machine using the winmgmts:// WMI moniker (think of moniker as another word for name) and assign this value to the objWMIService variable. The third line (wrapped) uses the ExecQuery method of the SWbemObject (objWMIService) to specify the information query parameters. If you were reading the statement in plain English, it would read as follows, “Select all (*) from the Win32NTLogEvent class where the Logfile property equals System.” So basically, only information in the System log will be returned.

Finally we use the For… Next control loop to return specified property information on the objects return from the query.

We can even drill down further for more detailed returns with another slight modification to the ExecQuery clause as demonstrated below in the same script using bold text:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & _
"\root\cimv2")
Set colItems = objWMIService.ExecQuery("SELECT * FROM " & _
"Win32_NTLogEvent WHERE Logfile = 'System' AND " & _
"EventCode = ‘6005’")
For Each objItem in colItems
Wscript.Echo "EventCode: " & objItem.EventCode
Wscript.Echo "User: " & objItem.User
Wscript.Echo "Logfile: " & objItem.Logfile
Wscript.Echo "Message: " & objItem.Message
Next

This is the exact same script as before; however, using the AND clause we are now filtering the query for not only information specific to the System Event Log, but also only System Event Log information with the Event ID of 6005.

As you can see, using clauses can dramatically narrow your results and thereby limit the amount of data you would have to filter through to find the desired results. You may also notice that the query clauses resemble Structured Query Language (SQL). While the syntax is very similar, it is actually WMI Query Language (WQL) and not really SQL. Basically it doesn’t have the SQL functionality to UPDATE or INSERT or modify database tables as SQL does and is used for SELECT query statements.

Conclusion

You can definitely see how powerful WMI can be in Windows Scripting from the many previous examples. With a robust object model, access to an extensive amount of classes and methods, the ability to make very minor modifications to scripts to obtain completely different results, and the ability to easily specify finite target information in WMI queries it becomes apparent why more and more applications are leveraging WMI via scripting to retrieve information about the current state computing environment.

If you have experience with WMI, then hopefully this article clarified some behind the scenes processes that you may have had taken for granted when running your scripts. If you are new to WMI, don’t get discouraged. WMI is more in-depth than the other subjects we have covered in scripting so far and takes a little more effort to learn. Don’t let any confusion you may have now dissuade you from continuing to learn WMI as it can make your administration tasks immensely easier not to mention being an invaluable addition to your skill set. Take advantages of the many resources on WMI listed in this article to help and post any questions to the forums as you test your new WMI scripts.

The following are simply a few script examples that can be used as templates demonstrating a few common tasks that may be performed using WMI Scripting with minor editing changes. Once you grasp the WMI concepts, you will find you only need to write a few scripts to have a whole arsenal of powerful tools for administration.

Stopping Services

strComputer = "." ‘Edit this for remote machines
strNamespace = "\root\cimv2" ‘Edit to change namespace
strServiceName = "Alerter" ‘Edit to reflect service to be stopped

Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & strNamespace)
Set colServices = objWMIService.ExecQuery _
("SELECT * FROM Win32_Service WHERE Name = '" & strServiceName & "'")
For Each objService in colServices
errReturnCode = objService.StopService() ‘StartService may also be used
Next

Forcing User Logoff

Const x = 4 'This can be changed to one of the following:
'0 = Logoff
'1 = Shutdown
'2 = Reboot
'4 = Forced Logoff
'5 = Forced Shutdown
'6 = Forced Reboot
'8 = Power Off Computer
'12 = Forced Power Off

strComputer = "." 'This can be changed for remote machines
strNamespace = "\root\cimv2" 'This can be changed for namespaces

Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" _
& strComputer & strNamespace)
Set colOperatingSystems = objWMIService.ExecQuery _
("SELECT * FROM Win32_OperatingSystem")
For Each objOperatingSystem in colOperatingSystems
ObjOperatingSystem.Win32Shutdown(x)
Next

Account Logon Failures

strComputer = "." ‘This can be changed for remote machines
strNamespace = “\root\cimv2\” ‘This can be changed for namespaces
strEventCode = “681” ‘Edit to reflect error code

Set objWMIService = GetObject("winmgmts:\\" & strComputer & _
"\root\cimv2")
Set colItems = objWMIService.ExecQuery("SELECT * FROM " & _
"Win32_NTLogEvent WHERE Logfile = 'Security'AND " & _
"EventCode = ‘" & strEventCode & “’”)
For Each objItem in colItems
Wscript.Echo "EventCode: " & objItem.EventCode
Wscript.Echo "User: " & objItem.User
Wscript.Echo "Logfile: " & objItem.Logfile
Wscript.Echo "Message: " & objItem.Message
Next

VBScript Fundamentals for Windows Scripting – ADSI

The previous article introduced the fundamental concepts of VBScript. If you are new to scripting and have not yet read this article, you should take a few moments to go over the information contained therein as it explains important fundamentals. This article will take that initial foundation and build on it to illustrate how VBScript can be applied in Windows Scripting in an applicable manner. Although a thorough knowledge of COM (Component Object Model) is not required, it is beneficial to be familiar with the ADSI object model, components, and provider architecture to create more efficient scripts. The next several sections will introduce the ADSI architecture to build a foundation that will aid in ADSI scripting.

Interfaces

Active Directory Service Interfaces, or ADSI, is the collection of DLL interfaces that allows the manipulation of different directory objects. This collection is used not only in scripting, but also with the snap-in MMC’s such as the AD Users & Computers snap-in. The interface is what provides information about an object such as its path, name, path to the parent object, etc. The most common interface is called IADs which is used by ADSI to interact with objects.

Figure

In the above example we used the LDAP provider (explained a little later) to instantiate (or create) the object we called oUser. The LDAP provider is able to determine that oUser is a User object in Active Directory so it creates the appropriate COM object, which we named oUser, along with the appropriate interfaces for that object which are listed to the left of the object. The IADs interface is what allows us to use the Get method to read the information about our User object, JoeUser. IADsUser is simply another common interface that might be used with this object and IUnknown is a base for other interfaces to be attached to the object which in turn are used to provide additional information about the object. We’ll look at an example shortly after discussing providers in more detail.

Namespaces

Everything in the directory has a specific place. The directory service maintains this information and makes it available to user queries, application calls, etc. But, how does Active Directory know where to go when you double click on JoeUser in the Users and Computers snap-in? It looks up where the JoeUser object is located within the AD namespace. Think of a namespace as a location in the directory to which an object name can be resolved. DNS is the best example to use for namespace. It is a hierarchical method of maintaining domains in a tree-like structure and is scalable and efficient. Active Directory uses not only DNS namespacing, but also LDAP (Lightweight Directory Access Protocol) to keep object organized into OU’s and containers. In the example above, we used the LDAP moniker (LDAP://) to point to where the JoeUser object resides in the AD namespace.

Providers

ADSI uses providers to connect to and interact with the directory in scripts and GUI’s. More specifically, the providers allow ADSI to interact with the directory namespaces. To bind to an object (if you remember from the VBScript Fundamentals – Part 1 Article, binding is how we connect and interact with an object using scripting as well as declaring a variable name to be used with that object), ADSI uses a specific syntax that is part of the ADsPath. The ADsPath is simply the name provider combined with the path to the object as illustrated in the following examples:

LDAP://dc=lab,dc=mycompany,dc=com
LDAP://server01/dc=lab,dc=mycompany,dc=com

The LDAP provider facilitates access to LDAP based directories. The first example demonstrates the syntax for serverless binding. This method simply binds to the path in the directory and does specify a specific server. This is the ideal method to use when binding as it does not require an additional operation to first bind to a specific domain controller before binding to the object.

WinNT://MYCOMPANY
WinNT://MYCOMPANY/dc/JoeUser

The WinNT provider facilitates access to a domain controller path in the MYCOMPANY domain. The first example simply binds a domain controller within whatever domain the script is run in, while the second method access the JoeUser account on the dc domain controller in the MYCOMPANY domain. This method is preferred for use in NT domains. It can be used with AD; however, the script’s performance is much better when the LDAP provider is used.

GC://dc=lab,dc=mycompany,dc=com
GC://server01/dc=lab,dc=mycompany,dc=com

The GC provider facilitates access to Global Catalog server in the directory path. The first example is one that uses the serverless binding mentioned earlier while the second binds to a specific server in the specified path.

NDS://NetTree/o=org01/dc=com,dc=mycompany,dc=lab,cn=JoeUser

The NDS provider facilitates directory path access within Novell NetWare directory services. Here we are referencing JoeUser who’s user object resides in the org01 organizational unit of the intraNetWare tree named NetTree. Since intraNetWare is also LDAP compliant, you can use the LDAP provider paths as well to facilitate access.

NWCOMPAT://SERVER01/JoeUser

NWCOMPAT provider facilitates access to the Novell NetWare bindery. In this example we are binding to the JoeUser object that exists on SERVER01 in the bindery.

IIS://server01.mycompany.com
IIS://localhost/w3svc/1

The IIS provider facilitates access to IIS service paths. The first example binds to the server server01 in the mycompany.com domain, while the second example references the w3svc on the local server.  The IIS:// provider references information in the IIS metabase.

NOTE – The ADSI provider names listed above are case-sensitive. All must be specified in uppercase with the exception of the WinNT provider.

Now let’s take a look at how binding to an object in the directory using a provider would appear in an actual script.

Set objOU = GetObject("LDAP://ou=Finance,dc=mycompany,dc=com")

objOU.Filter = Array("user")

For Each objUser In objOU

Wscript.Echo objUser.Name

Next

In the first line, we are binding to the Finance organizational unit in the mycompany.com domain using the Get method and assigning the operation to the variable name objOU. Remember, it is the LDAP provider that creates the COM object (called objOU) and the appropriate IADs interfaces (allowing us to use the Get method to read directory information) to facilitate interaction with the newly created object based upon the object type.

The next line uses the Filter method of our newly created (instantiated) object to put the user objects within the Finance OU into a dynamic array. We then use the For…Next control loop to display each user’s name within the array.

Consider the following graphic to better visualize the different ADSI layers and the logical architecture flow:

Figure

We have already discussed the Directory Namespace layer which resides at the server level (on the domain controllers in the AD partitions) as well as the provider layer which allows access to the namespace layer via the COM interfaces created by the ADSI DLL’s also mentioned earlier. The top three layers are easier to explain as they are the layers that process all the information created by the script.

Router

The router does most of the work when it receives the scripted request. Take for example, the GetObject method in the above example. The router is the layer that receives the request, recognizes the provider used in the request (LDAP://), looks up the provider information in the registry, and then loads the provider into memory. Next, the router layer creates the object and sends the object information back to the provider layer for use in the script. Once the router has created the object (the binding process), items processed with the object in the script are performed at the Provider layer. There may also be information passed directly between the Application and the Provider layer when applications such as the MMC are used for information searches or user/group creation.

Property Cache

The cache is simply a place in memory on the machine from which the script was run that is used to hold the object after the binding operation in the script. The object is not downloaded to the machine, but rather a virtual copy of the object is maintained locally for use in the script. When we use the Get method (i.e. GetObject), it is here that the script looks first for the information. If it cannot locate it in the local memory, or property cache, then a GetInfo call is initiated to AD to read the property information into cache.

Applications

This is the top layer of the architecture where all interaction with AD starts. A good example of interaction within this layer is the Users and Computers MMC snap-in. The MMC initiates at this layer and then interacts with the rest of the ADSI layers depending on what you do within the MMC.

Interfaces

Every object in Active Directory is defined explicitly by attributes. To administer these objects and attributes, we use a set of methods contained within the ADSI interfaces. There are 6 core properties common to all objects in ADSI called IADs. IADs is the most basic interface in ADSI and the 6 core properties are as follows:

IADs::GUID – Represents the Globally Unique Identifier of an object returned as a string.
IADs::Class – Represents the schema class of the object returned as a string.
IADs::ADsPath – Represents the full path of the object within the current namespace directory. The path is returned as a string and identifies the objects location in the directory.
IADs::Name – Represents the object’s relative name. Name is returned as a string.
IADs::Parent – Represents the path to the parent object of the current object in the directory returned as a string.
IADs::Schema – Represents the path to the object and its schema class in the directory returned as a string.

As each of the above IADs methods are fairly vague by themselves, so let’s illustrate this in a small script:

Set objDomain = GetObject("LDAP://dc=internal,dc=testdom,dc=com")
WScript.Echo "ADsPath:" & objDomain.ADsPath
WScript.Echo "Class:" & objDomain.Class
WScript.Echo "GUID:" & objDomain.GUID
WScript.Echo "Name:" & objDomain.Name
WScript.Echo "Parent:" & objDomain.Parent
WScript.Echo "Schema:" & objDomain.Schema

The first line binds to the internal.testdom.com domain using the LDAP provider and the Get method to read information from the object as discussed at the beginning of this article. Each line thereafter simply displays the results in a pop-up window on the screen. Cut and paste the script into notepad, edit the domain information accordingly, and run the script. You will get the following results tailored for your domain:

ADsPath:LDAP://dc=internal,dc=testdom,dc=com
Class:domainDNS
GUID:718b9aa6e578ab49b954f4afa230b9d2
Name:dc=internal
Parent:LDAP://dc=testdom,dc=com
Schema:LDAP://schema/domainDNS

You may be wondering, “With all this discussion surrounding IADs, I didn’t see it listed anywhere in the script”. This is because IADs is not an actual property or method, it’s an interface. So referring back to the script above, the objDomain variable becomes our IADs container after the binding process. For further visual illustration, you could view the above script as:

Set objDomain = GetObject("LDAP://dc=internal,dc=testdom,dc=com")
WScript.Echo "ADsPath:" & IADsContainer.ADsPath
WScript.Echo "Class:" & IADsContainer.Class
WScript.Echo "GUID:" & IADsContainer.GUID
WScript.Echo "Name:" & IADsContainer.Name
WScript.Echo "Parent:" & IADsContainer.Parent
WScript.Echo "Schema:" & IADsContainer.Schema

NOTE – This is NOT proper syntax and will not work. It is only for illustration of the topic.

Viewing the above, you can see that once the binding process has completed, the objDomain object essentially becomes our IADs container. I have substituted this in the second example for better visual representation of the concept.

IADsContainer Methods

We just learned the concept of IADsContainers and how in ADSI, once we bind to an object, it then becomes the IADsContainer. Let’s go even one step further to look at the methods used with IADsContainers to obtain results and functionality within the script.

IADsContainer::Filter (reading values)

This one is most commonly used methods when returning objects from an enumeration into an array. For example:

Set objOU = GetObject("LDAP://OU=MIS,dc=internal,dc=testdom,dc=net")
ObjOU.Filter = Array("user")
For each strObject in objOU
Wscript.Echo strObject.cn
Next

The first line is the binding process with the Get method. The second line is where we utilize the Filter method on the IADsContainer (objOU) to read the information into a dynamic array. The next three lines use a For…Next loop to display items in the array returned by the Filter method.

IADsContainer::Create

The Create method, as the name so intuitively implies, is used to create objects in the directory. Consider the following example to create a user object in the MIS Organization Unit:

Set objOU = GetObject("LDAP://ou=MIS,dc=internal,dc=testdom,dc=net")

Set objUser = objOU.Create("user", "cn=UserA")

objUser.Put "sAMAccountName", "UserA"

objOU.SetInfo

As you are getting used to by now, the first line is the binding process. The second line uses the Create method on the IADsContainer (objOU) to create a user object and assign it the name “UserA”. Since the sAMAccountName is a mandatory attribute for a user object (every user must have a logon account name), we set that attribute value in the third line. The fourth line commits the account to the Active Directory.

NOTE – Be careful when using SetInfo as this writes the results of the script directly into the Active Directory.

IADsContainer::Delete

The Delete method is just the opposite of the Create method in that it deletes objects from the directory. The next example will delete the user object we just created:

Set objOU = GetObject("LDAP://ou=MIS,dc=internal,dc=testdom,dc=net")

Set objUser = objOU.Delete "user", "cn=UserA"

IADsContainer::Filter (writing values)

Another method used with Filter is writing values to objects in the directory as demonstrated here:

Set objUser = GetObject("LDAP://cn=UserA,ou=MIS,dc=internal,dc=testdom,dc=net")

objUser.Put “description”, “MIS Dude”
objUser.SetInfo

In this example we initiate the binding process, add a description to the user object UserA using  Put, and then use SetInfo to commit to Active Directory.

IADsContainer::MoveHere and IADsContainer::GetObject

These are two more methods which may be utilized in IADsContainer. The GetObject method should be quite familiar now as this is the method we have used continuously to bind to directory objects in a single step. The MoveHere method can be extremely involved and usually entails using ADO (ActiveX Data Objects) to connect to the directory and retrieve records for better performance. This method is beyond the scope of this article; however, I wanted to list it as it is a method of the IADsContainer.

Conclusion

ADSI is a very powerful tool in scripting and this article only touches the surface of the possibilities of the versatility of the interfaces. You can find additional information regarding ADSI online at http://www.microsoft.com/technet/treeview/default.asp?url=/technet/scriptcenter/scrguide/sas_ads_overview.asp. The more you work with ADSI and become familiar with it, the more functional and robust scripts you will be able to write. Remember, robust doesn’t mean long and drawn out. Only a few paragraphs earlier, we deleted a user object with only two lines of code.

My next article will cover WMI (Windows Management Instrumentation) use in VBScript.

VBScript Fundamentals for Windows Scripting – The Basics

This article explains the general basics behind VBScript. Its purpose is to de-mystify VBScript for System Administrators, System Engineers, and anyone else who is interested in writing Windows Script but does not have a background in VB coding.

For many administrators, the thought of writing code throws up an immediate red flag. “That’s not the sort of thing we do, that’s for the developers who write all that gibberish”. The simple fact is that scripting can be as hard or as easy as you choose to make it. Can you write a long and involved script to map drives automatically? Sure. Do you have to? No, it can be as simple as two lines:

Set oDrives = CreateObject(“WScript.Network”)
oDrives.MapNetworkDrive “Z:”, \\SVR01\Home

We’ll go into more detail surrounding the above a little later in the article; the purpose here is to demonstrate that scripting can automate repetitive tasks. How many times have you mapped drives from the command prompt or written them in batch files? Write some script, save some time.

A little VBScript History

When VBScript first came out, it was part of the Microsoft Windows Script Technologies release. VBScript and Jscript (Java Script) were both released to facilitate client-side scripting in web pages. The initial target audience was obviously web developers so system administrators saw no need to get involved since it was used in Web Development. In addition, most of the information surrounding VBScript’s applications in technologies such as WMI (Windows Management Instrumentation) and ADSI (Active Directory Services Interface) was only available to developers in the SDK’s (Software Development Kit) and other targeted information. Lastly, when administrators were able to find some information on the technologies, it was written in pure coding jargon which was not discernable unless you had a developer background.

Today; however, the information on VBScript and scripting technologies is widely available and administrators are noticing the usefulness of the scripts to make their jobs easier.

VBScript Fundamentals

VBScript works with objects (specifically Automation objects) which are simply a smaller set of COM (Component Object Model). COM is what facilitates the functionality of items like .dll’s and .exe’s. Take our earlier example:

Set oDrives = CreateObject(“WScript.Network”)
oDrives.MapNetworkDrive “Z:”, \\SVR01\Home

In this example, we are using the CreateObject method to bind to the element (or object), WScript.Network, and give it the name of “oDrives” for use in the next line. Binding is the method used to make a connection to an Automation object (WScript.Network) and create a new instance of that object (oDrives). We now call the “MapNetworkDrive” method on our oDrives object followed by the arguments directing the method to map the Home share on SVR01 to the Z:\ drive.

Note – When I first learned this stuff, it was right about here that I thought, “You’ve got to be kidding! A few paragraphs ago, the author said this wasn’t that hard”. Keep reading through all the articles including the upcoming ones. These are new concepts for system administrators similar to jumping from NT4 to Active Directory and we all made that leap okay. It will make sense after a little practice, I promise.

Definitions

The following are a few definitions used in VBScript:

Methods –Functions that an object can perform. In the previous example, we used the CreateObject method to bind to the WScript.Network object.

Variables –Places in memory to store data. Note this example:

Comp = InputBox("Enter the Computer Name", "Computer Name")

In this example we are calling up an input box (this prompts the user to input data), giving it a description for the user which will read “Enter the Computer Name” above the input box, and assigning “Computer Name” to the title of the input window when it appears. All of this information is held in memory and stored as a value for the variable Comp. Unless this information is released from memory later in the script, anytime the variable Comp is called, the value that was entered by the user will be used. Variables can be given any descriptive name as long as it is not reserved for use as a scripting component such as WScript, Const, Set, etc.

Constants -Similar to Variables in the regard that they store data in memory; however, a constant must be defined within the script. In addition, the values cannot be changed within the script once they are defined and will maintain their values throughout the script. See the following example script:


Const FIRST_NAME = "Alan"
Const LAST_NAME = "Finn"
Wscript.Echo "My name is ", FIRST_NAME, LAST_NAME

Above, we are declaring two constants; FIRST_NAME and LAST_NAME and assigning them the values of Alan and Finn respectively. The third line uses the Echo method of the WScript object to print the string “My name is “, then the first constant value and lastly the second constant value in a pop up window. Copy and paste the script into notepad and change the constants to match your name; then save the file as MyFirst.vbs and double click on it to run.

Operators and Control Flow Statements – Operators are used to compare expressions. Basically, it’s logical math comparisons used in VBScript. As an example, let’s say that x = 1 and y = 2. An operator would compare the two and return a response or do something else based on the results such as:

If x > y then this statement is False
Else this statement is True.

Obviously 1 is not greater than 2 so the statement returns as True (even though 2 is greater than 1 because of the way the logic operator was used). As all things in VBScript, this can be as complex or as simple as you make it depending on what your needs are. In addition to the > (greater than) operator used, the above statement also uses a simple flow control example. Since the first line is false, the statement will “flow” into the second line which “controls” the behavior of the statements. Now, let’s put this together into some real VBScript:

1. x = True 'assign variable x a value of true
2. count = 0 'assign variable count a value of 0
3.
4. Do 'outer loop
5. Do While count < 10 'inner loop 6. count = count + 1 'increment the count by 1 7. Wscript.Echo count 'display current count on screen 8. If count = 10 Then 'if this condition is True 9. x = False 'set this value to False 10. Exit Do 'exit the inner loop 11. End If 'ending the If statement 12. Loop 'first loop statement for inner loop 13. Loop Until x = False 'exiting the outer loop

The lines have been numbered for ease of explanation. The “ ‘ “ sign is used to specify comments which are ignored while the script runs. Comments have been added to the right of each line to explain functionality. Okay, let’s break it down by line as follows:

1. Assign the variable called “x” a value of True
2. Assign the variable called “count” a value of 0.
3. Space inserted to separate the variable declaration statements from the script function. Simply makes the script easier to read and is not required.
4. Used the “Do” statement to start a loop. This type of loop (flow control) will continue until a condition becomes either True or False depending on what is specified.
5. Defining the conditional constraints of the loop statement. DO the loop WHILE the COUNT variable is LESS THAN 10. Remember, we assigned the COUNT variable a value of 0 earlier.
6. Incrementing the count variable by 1 each time the statement loops. The first few loops could be considered as COUNT = 0+1, COUNT =1+1, COUNT = 2+1, etc.
7. This displays the current count number in a pop-up window on your screen.
8. The conditional statement. IF COUNT equals 10 THEN do what is on the next line, otherwise go to the LOOP statement and loop back to the DO statement. Until the COUNT variable meets this condition, the statement will continue to loop and increment itself.
9. Assigning a new value to variable X. Once the conditional statement in line 8 is met, change the value of X to FALSE.
10. Since the condition has now been met, we exit the second, or inner, loop.
11. This statement is required to show the cessation of the IF loop in line 8. This is required for the script to know where and when to stop the flow control loop statement.
12. This is the closing statement for the Do loop in line 5. Also required to tell the script when to stop the flow control loop statement from the inner Do loop.
13. This statement closes the outer, or first, Do loop once the condition is met. By this time, the COUNT variable has been incremented to a value of 10 so the variable X has been changed to FALSE meeting the criteria of this condition, therefore the outer loop is closed and the script is complete.

Arrays – When you need to carry out administrative tasks multiple times, arrays offer a method to store multiple values. The Array function is used to assign a set of values to a variable. The following example assigns three colors to the array variable named Colors:

Colors = Array(“red”, ”white”, ”blue”)

It is also possible to create an array variable without using the actual Array function statement by declaring the variable at the beginning of your script using the Dim statement. Declaring a variable simply tells the script that the name you are declaring will be used as a variable within the script. For example:

Dim Colors(2)

This statement tells the script that we are declaring a variable named Colors as an array and that it will contain three values. Yes, I said three values. The first item in every array is actually designated as item 0 which must be taken into consideration when calculating the total amount of objects in the array. In this case, we have three colors so starting at zero we count 0, 1, 2 or simply subtract one from the total values to be used. 3-1=2.

Continuing on, we now will assign values to our array variable named Color starting with item 0:

Dim Colors(2)
Colors(0) = “red”
Colors(1) = “white”
Colors(2) = “blue”

Note – If you tried to assign a fourth variable without increasing the constraints of the array, your script would return an error stating “Subscript out of Range”.

Obviously, this would take a while to create for larger arrays and the goal of scripting is to simplify and automate your job tasks so now we’ll take this one step further and introduce Dynamic Arrays.

Dynamic Arrays are more useful that Arrays in the fact that they don’t require you to specify the number of values and the size of the array can change throughout the script. This next script will read all of the computers in the domain and write the results to a textfile:

1. Dim Container
2. Dim ContainerName
3. Dim Computer
4. Dim fso
5. Dim outfile
6.
7. ContainerName = "INTERNAL"
8. Set fso = CreateObject("Scripting.FileSystemObject")
9. Set outfile = fso.OpenTextFile("c:\DomainPC.txt", 2, True)
10. Set Container = GetObject("WinNT://" & ContainerName)
11. Container.Filter = Array("Computer")
12. For Each Computer In Container
13. outfile.writeline Computer.Name
14. Next

As we did earlier, let’s break it down line by line and review the script:

1. Declare the Container variable.
2. Declare the ContainerName variable.
3. Declare the Computer variable.
4. Declare the fso variable.
5. Declare the outfile variable.
6. Space for separation of declarations from the script function.
7. Assign the string value of INTERNAL (the domain being queried) to the variable ContainerName.
8. Use the CreateObject method to bind the Scripting.FileSystemObject object as a value for the fso variable. This means that fsois now an object for use within the script.
9. Use the OpenTextFile property of the new fso object to create a text file in the C:\ directory named DomainPC.txt and bind it to the outfile variable. Regarding the parameters at the end of the statement; the 2 means the file may be overwritten, and the True designates a Unicode file (false would create an Ascii file).
10. Use the GetObject method to search the domain INTERNAL for computers with accounts in that domain and assign them as values for the Container variable. Note – The WinNT:// parameter is case sensitive and is used in both NT and AD domains. In pure AD domains, it is preferable to use the LDAP:// designation with the LDAP path which we will go into more detail in a future article covering ADSI.
11. Here is where we declare the Dynamic Array. We now use the Filter method on the Container variable to place the output in the dynamic array we are naming as Computer.
12. Beginning of a For… Next loop for flow control. In this line we are telling the script that every value in the array variable Computer will be assigned as a value in the variable Container. We cannot simply use Container for further methods as it is already in use as a variable in the dynamic array so we must create another variable to use. The loop would read as For Each Computer in the array Container, do whatever follows in the script until Next is reached and there are no more Computer values left in the array.
13. Use the writeline property on the outfile object and print the Name property of the Computer object on each line.
14. Go to the Next Computer in the Container array until all array values have been written, then stop the loop.

Summary

If you are completely new to the world of VBScript, this is probably a bit overwhelming. The object (no pun intended) of this article is to introduce the main concepts of VBScript. There is entirely too much material pertaining to VBScript to cover in this article. Some portions of the script make sense because they are logical and other parts are foreign and confusing. The syntax of the language will become easier with practice so don’t get discouraged after reading this article. Take some time to analyze the scripts and their explanations and understand what is occurring on each line of the script. I guarantee that any administrator or engineer who writes VBScript (myself included) has a book or two that lists all the syntax and possible parameters available on their desk to refer to. A good starter site for reference is

http://www.devguru.com/Technologies/vbscript/quickref/vbscript_list.html.

Also, take a look at some of the following links to get a deeper initial understanding of the VBScript framework:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/vsgrpFeatures.asp?frame=true

http://www.winguides.com/article.php?id=2

http://www.winnetmag.com/Articles/Index.cfm?ArticleID=3026

http://www.winnetmag.com/Articles/Index.cfm?ArticleID=5410

There are many, many more resources on the Internet as well as books available to review for more information. The above are only a few locations of information.

VBScript isn’t something that will make sense overnight, but the little bit of work required to get familiar with it will pay off tenfold and then some in your environment.

My next article will look at ADSI (Active Directory Service Interfaces) and how VBScript can help you administer directories.

Managing Changes to DHCP with NETSH

How many times have you been approached by the Network Engineering Group with the news that the internal IP scheme needs to be changed or that VLAN’s are going to be implemented on all switching equipment? The first will require some configuration, but the second can mean large amounts of work to create and configure the new scopes and scope options needed to complement the additional subnets.

The DHCP MMC snap-in can definitely facilitate this procedure; however, the NETSH utility offers a method to script modifications to DHCP scopes automatically. For those of you who are still building your VBScript, WMI, and ADSI scripting skills, don’t fret. This is good old-fashioned command line work and doesn’t require knowledge of objects, methods, components, etc.

The NETSH utility has many different functional parameters within the DHCP context alone. As there are far too many options to cover at one time, this guide will focus on the following common administrative tasks in making changes to DHCP scopes that can be accomplished with NETSH:

  1. Dumping scopes and configurations from and existing DHCP server into a text file.
  2. Creating a new scope.
  3. Defining the range of the scope.
  4. Adding options to the scope.
  5. Assigning reservations for IP addresses.
  6. Listing authorized DHCP servers and authorizing DHCP servers.

Dumping Existing Scopes and Configurations

Getting your configuration in a text file is a great method to obtain as much or as little dhcp information as needed in a single document without navigating around the MMC. The following example run from the command line will dump all information for the scope 192.168.2.0 from the DHCP server 192.168.2.5 into a text file call test.txt. Note – You may also specify the DHCP server by name. For example: \\DHCP-SVR01.

C:\>netsh -c dhcp server 192.168.2.5 scope 192.168.2.0 dump > c:\test.txt

The first few lines of output will resemble the following:

# Changed the current scope context to 192.168.3.0 scope.Dhcp Server 192.168.2.5 add scope 192.168.3.0 255.255.255.0 "ScopeA" "First Scope"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set state 1

# ============================================================
#  Start Add Ipranges to the Scope 192.168.3.0, Server 192.168.2.5
# ============================================================

All lines with the hash mark (#) are ignored as input similar to the REM statement used in batch files. The two lines beginning with Dhcp Server are actual commands that can be edited to change information and then the file can be used as an input script which we will cover later in this work.

Creating a new scope

To create a new scope we will use one of the lines in the above example to create a scope for the network ID 192.168.3.0. For this example, the first non-commented line is copied to notepad and then the file is saved as C:\dhcp.txt.

Next, from the command line we run C:\>netsh exec c:\dhcp.txt. This creates and activates the new scope as shown below. Note – The scope does not have any Property Options set at this time, hence the blue information icon.

Defining the range of the scope

In order for the clients to lease IP’s, we must obviously assign a range from which leases will be distributed. This is done as follows in our script:

Dhcp Server 192.168.2.5 Scope 192.168.3.0 add iprange 192.168.3.1 192.168.3.254
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add excluderange 192.168.3.1 192.168.3.10

Note – that an exclusion range is included in the example to prevent a range of IP addresses from being assigned to the clients.

As we are executing these commands in steps from our script, we will save the lines above to C:\range.txt. Going back to the command line, we now run C:\>netsh exec c:\range.txt which adds the ranges to the scope as shown below in the MMC:

Adding options to the scope

Now that the scope is created, we need to add some options for the clients to better define their DHCP leases. Note – Normally, these lines would be included in the script (dhcp.txt) underneath the line that defines the range of the scope and actually run in the previous step. Since it is activated upon creation, clients that might lease IP addresses from this scope would need the defined options at the time of lease. This has been broken into steps in this article for subject demonstration. To create a scope that was deactivated initially, the following line would need to be added after the first line in the script:

Dhcp Server 192.168.2.5 Scope 192.168.3.0 set state 0

This example will add the standard options: router (gateway), DNS servers, and lease expiration to the scope. All possible options can be scripted, but are beyond the scope of this example. It is possible to get the syntax for all options for scripts by creating a dump file at the server level instead of the scope level. The syntax for the three options we will use is as follows in the script file:

Dhcp Server 192.168.2.5 Scope 192.168.3.0 set optionvalue 3 IPADDRESS "192.168.2.2"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set optionvalue 6 IPADDRESS "192.168.2.5" "192.168.2.6"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set optionvalue 51 DWORD "691200"

The first line sets the router for the scope. This is the gateway the clients will use to leave the defined network. The second line assigns DNS servers to the client leases and can include as many as needed. The third line in this example assigns the expiration for the lease. In this case 8 days expressed in seconds (691200 seconds/60 = 11520 minutes/60 = 192 hours/24 = 8 days). Again, the changes can be verified in the MMC.

Note – The lease expiration is not viewable in this screen as it is a property of the scope and not an option. This may be viewed by right-clicking on the scope and selecting Properties.

Assigning reservations to the scope

To assign the same IP to a client whenever the lease is renewed, we can define reservations via the netsh script.  As we are using multiple steps to create the scope in this example, we will use the following syntax and save the file as C:\reserve.txt.

Dhcp Server 192.168.2.5 Scope 192.168.3.0 add reservedip 192.168.3.20 00043c40fb6a SVR01
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add reservedip 192.168.3.21 0600ba34f50c SVR02
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add reservedip 192.168.3.22 02003b5d80ca SVR03

In this example, we are working on server 192.168.2.5 in scope 192.168.3.0. The number at the end of the line is the MAC (Media Access Control) address of the NIC card. This ensures that whenever this NIC requests a lease renewal, it will always get the same IP. The name on the end is simply for labeling the reservation in DHCP, it has no effect on the client. We now execute the script file with the following syntax from the command line: C:\>netsh exec c:\reserve.txt. Again, we can verify the results in the MMC.

Note – I have run into intermittent issues with reservation client types where the reservation will sometimes be assigned a lease type of BOOTP instead of DHCP. This can be forced by adding another entry to the end of each line of the script specifying any of the following options [BOOTP | DHCP | BOTH] as needed.

Listing and authorizing DHCP servers in Active Directory

It is possible to verify and list all the authorized DHCP servers in Active Directory from the command line using the following syntax: C:\netsh dhcp show server. This allows you to view all authorized servers to ensure that an over-eager administrator hasn’t added an unnecessary server to the network.

You may also authorize a DHCP server in AD remotely with the following command:

C:\ netsh dhcp add server DHCP-SVR01.yourdomain.com 10.2.2.2

This one can be extremely handy if you want to hand off the job of creating the DHCP scopes and/or server to a junior admin. As Enterprise rights are needed to authorize the server, the work could be verified before going into production and then authorized remotely from the command line. Note – Remember that it can take a DHCP server 15 minutes to authorize so if it doesn’t show up immediately, give it a little while to process.

Bringing it all together

This has been broken into steps for better demonstration; however, all of the steps can be combined into a single script after you are comfortable with the syntax. The completed script would look like the following:

Dhcp Server 192.168.2.5 add scope 192.168.3.0 255.255.255.0 "ScopeA" "First Scope"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add iprange 192.168.3.1 192.168.3.254
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add excluderange 192.168.3.1 192.168.3.10
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set optionvalue 3 IPADDRESS "192.168.2.2"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set optionvalue 6 IPADDRESS "192.168.2.5" "192.168.2.6"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set optionvalue 51 DWORD "691200"
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add reservedip 192.168.3.20 00043c40fb6a SVR01
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add reservedip 192.168.3.21 0600ba34f50c SVR02
Dhcp Server 192.168.2.5 Scope 192.168.3.0 add reservedip 192.168.3.22 02003b5d80ca SVR03
Dhcp Server 192.168.2.5 Scope 192.168.3.0 set state 1

This would all be saved to a single file and run using the C:\>netsh exec filename.txt command from the command line.

Netsh is a very powerful command line tool with MANY other options and uses. The syntax can be tricky but after a little practice, you’ll find that it simplifies several mundane administrative tasks.