One of the challenges that we have in our organization is dealing with local administrator passwords. Through the years, we have used various models for our local admin password to various affect. One of the biggest issues we have run across, though, is that typically we have had one password applied across all of our workstations in our enterprise. We all know that is bad, but finding an alternative can be difficult. At one point, we ran a script that would generate a unique password for the local admin account. Unfortunately, this password had to be written to a single file and was a pain to use as it used any keyboard characters (determining what some of those characters were was difficult).
Over the years, I have also read various advice. Jesper Johansson (a now former MS employee) recommends disabling the local administrator account (http://technet.microsoft.com/en-us/magazine/2006.01.securitywatch.aspx ). His theory is that if you get to a point where everything is broken and you need the local admin account, rebuild the machine. It sounds OK in theory, but I don’t know that we are willing to take that step. Perhaps if we had our SCCM environment all setup so that reimaging a workstation was easy and we knew that all important local data was backed up, it might be a different story.
Instead, we have opted for a different method. We are running a script against workstations to give each workstation a unique custom local admin password. (Our service desk techs all have their own workstation admin account giving them local admin rights on workstations with a named account.) In this posting, I will set out the method that we use to generate this unique custom password.
First, let me make a note of what I mean when I say unique custom password. We use a tool from Jesper Johansson called passgen. We can then feed into this tool “seeds”. In this case, one of the seeds is the workstation name and the second is a passphrase. The tool then takes these two parts and creates a unique password. (In our case, we have also set the length of the password and limited it to using alpha characters, capital and lowercase, and numbers.) Because this tool generates the password based upon known input, we can easily recreate what the local admin password is on a given machine. (Note that less than a handful of people know what the passphrase seed is, so we use scripts for the techs to run if they need to find the local admin password. In fact, we have this script integrated into a custom Active Directory Users and Computers mmc snap-in that we publish for each tech. They are able to click on the workstation name and it will give them an option to generate the local admin password.) Here is the passgen command we run passgen -g ComputerName Phrase -l 10 -e 2 -c 500 –h.
One of the other features we wanted to include in the script is the ability to change the custom password if we needed to give it out to the end user. We have done this by using group membership. Initially, all workstations are not in a group. Because of this, the script sets variables for what the passgen command will be. If we need to iterate the password to the next level, we can add the workstation to a group named WKS_Local Administrator Password x (where x is a number 1-9). This changes the workstation name in the passgen command to have an _X (again where X is the matching number 1-9). Doing this creates an entirely new unique password for that workstation using the new seed. (For example, a workstation may generate the following password when in none of the groups: YOJ9N0NWvZ . When we add it to group 1 (and so the workstation name becomes WKS12345_1 , the new generated password becomes 26lcaK9DWO .) This gives us the ability to iterate the password up to 10 times for each workstation.
I also have the script set so that it will only run once. It does this by creating a file on the local machine. If that file exists, it does not run. I primarily do this for performance reasons, but want to do some further testing to see if this is necessary. I actually think it would be better to continuously run this script at every startup so that if it gets changed, we are changing it back to what it should be. (Eventually, if I do continue to set it to run only once, I would prefer to use a registry key to do so. I am still working on learning the best and most reliable method for that.)
So, now let’s get to the point and show you the script.
1: '==========================================================================
2: '
3: ' NAME: ChangePasswordLocalAdmin.vbs
4: '
5: 'VERSION: 1.2.0
6: 'MODIFIED: Doug Neely
7: 'DATE: 02/27/09
8: ' This will change the password of the local admin account so that every
9: ' computer is different, but can be easily retrieved using passgen.
10: '
11: 'Ideally, I am wanting this to be able to check group membership. This will
12: ' be an easy way for us to increment the password changes using groups.
13: ' Group membership will change the variables of the computer name.
14: 'This starts by going through and checking if C:\CONTOSO exists. If not, it
15: ' creates it. Next, it looks to see if the strSuccess exists. If it does,
16: ' it ends. If it doesn't, it runs the passgen command. If it is successful,
17: ' it creates a success file and if it fails it creates a failure file.
18: '
19: 'MODIFIED: Doug Neely
20: 'DATE: 06/08/09
21: ' Revised this script to use a different method for checking group
22: ' membership. This method will place all groups into a dictionary
23: ' item for later checking. It can handle nested groups, primary group
24: ' and cross-domain group membership. This has been pulled from
25: ' Robert Mueller's site at http://www.rlmueller.net/IsMember6.htm
26: '
27: 'Additional Notes:
28: 'passgen -g strComputer strPhrase -l 10 -e 2 -c 500 -h
29: ' -g generates a password using the computer name and phrase as a seed
30: ' -l 10 - length of 10 characters
31: ' -e 2 - limits the password characters to upper case, lower case and numbers
32: ' -c 500 - changes the password of the "500" account - administrator
33: ' -h - output is hidden
34: '==========================================================================
35: Option Explicit
36: ' Declare objects and variables with global scope.
37: Dim objGroupList, adoCommand, adoConnection, objRootDSE
38: Dim adoRecordset, strAttributes, strFilter, strQuery
39: Dim oADSysInfo, objFSO, objShell, strCONTOSO
40: Dim sComputerName, strpassgen, strPhrase, strKeyPath, strValueName, strValue
41: Dim strComputerPath, objComputer, strGroup, shellcmd, strSuccess, sCMD, ND, objFile
42: '==========================================================================
43: 'On Error Resume Next
44: Set oADSysInfo = CreateObject("ADSystemInfo")
45: Set objFSO = CreateObject ("Scripting.FileSystemObject")
46: Set objShell = CreateObject("WScript.Shell")
47: '==========================================================================
48: 'Configuration Block
49: strCONTOSO = "c:\CONTOSO\"
50: sComputerName = GetComputerName
51: strpassgen="\\contoso.com\netlogon\passgen.exe"
52: strPhrase="HereIsWhereYouWouldPlaceYourUniquePassPhrase!"
53: '==========================================================================
54:
55: strComputerPath = "LDAP://" & oADSysInfo.ComputerName
56: Set objComputer = GetObject(strComputerPath)
57:
58: 'Here this looks for group membership.
59:
60: strGroup = "WKS_Local Administrator Password 1"
61: If (IsMember(objComputer, strGroup) = True) Then
62: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_1 " & strPhrase & " -l 10 -e 2 -c 500 -h"
63: strValue = "1"
64: strSuccess="C:\CONTOSO\ads1.admp"
65: Else
66:
67: End If
68:
69: strGroup = "WKS_Local Administrator Password 2"
70: If (IsMember(objComputer, strGroup) = True) Then
71: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_2 " & strPhrase & " -l 10 -e 2 -c 500 -h"
72: strValue = "2"
73: strSuccess="C:\CONTOSO\ads2.admp"
74: Else
75:
76: End If
77:
78: strGroup = "WKS_Local Administrator Password 3"
79: If (IsMember(objComputer, strGroup) = True) Then
80: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_3 " & strPhrase & " -l 10 -e 2 -c 500 -h"
81: strValue = "3"
82: strSuccess="C:\CONTOSO\ads3.admp"
83: Else
84:
85: End If
86:
87: strGroup = "WKS_Local Administrator Password 4"
88: If (IsMember(objComputer, strGroup) = True) Then
89: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_4 " & strPhrase & " -l 10 -e 2 -c 500 -h"
90: strValue = "4"
91: strSuccess="C:\CONTOSO\ads4.admp"
92: Else
93:
94: End If
95:
96: strGroup = "WKS_Local Administrator Password 5"
97: If (IsMember(objComputer, strGroup) = True) Then
98: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_5 " & strPhrase & " -l 10 -e 2 -c 500 -h"
99: strValue = "5"
100: strSuccess="C:\CONTOSO\ads5.admp"
101: Else
102:
103: End If
104:
105: strGroup = "WKS_Local Administrator Password 6"
106: If (IsMember(objComputer, strGroup) = True) Then
107: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_6 " & strPhrase & " -l 10 -e 2 -c 500 -h"
108: strValue = "6"
109: strSuccess="C:\CONTOSO\ads6.admp"
110: Else
111:
112: End If
113:
114: strGroup = "WKS_Local Administrator Password 7"
115: If (IsMember(objComputer, strGroup) = True) Then
116: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_7 " & strPhrase & " -l 10 -e 2 -c 500 -h"
117: strValue = "7"
118: strSuccess="C:\CONTOSO\ads7.admp"
119: Else
120:
121: End If
122:
123: strGroup = "WKS_Local Administrator Password 8"
124: If (IsMember(objComputer, strGroup) = True) Then
125: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_8 " & strPhrase & " -l 10 -e 2 -c 500 -h"
126: strValue = "8"
127: strSuccess="C:\CONTOSO\ads8.admp"
128: Else
129:
130: End If
131:
132: strGroup = "WKS_Local Administrator Password 9"
133: If (IsMember(objComputer, strGroup) = True) Then
134: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & "_9 " & strPhrase & " -l 10 -e 2 -c 500 -h"
135: strValue = "9"
136: strSuccess="C:\CONTOSO\ads9.admp"
137: Else
138:
139: End If
140:
141: If shellcmd = "" Then
142: shellcmd="\\contoso.com\netlogon\passgen -g " & sComputerName & " " & strPhrase & " -l 10 -e 2 -c 500 -h"
143: strValue = "0"
144: strSuccess="C:\CONTOSO\ads.admp"
145: Else
146: End If
147:
148: If NOT objFSO.FileExists(strSuccess) Then
149: 'Deletes any previous success/failure files
150: Set objShell = CreateObject("WScript.Shell")
151: sCMD = "%COMSPEC% /c del C:\CONTOSO\*.admp"
152: objShell.Run sCMD, 0, True
153: 'Set objShell = nothing
154:
155: ND = objShell.Run(Shellcmd,,True)
156: 'Finally, we will create a success file. We will inventory this file with SCCM so we can verify success.
157: Set objFile = objFSO.CreateTextFile(strSuccess, False)
158: objFile.Close
159:
160: Else
161: 'We do nothing and end
162: End If
163:
164: ' Clean up.
165: adoConnection.Close
166: Set objGroupList = Nothing
167: Set objRootDSE = Nothing
168: Set adoCommand = Nothing
169: Set adoConnection = Nothing
170: Set adoRecordset = Nothing
171:
172: Function GetComputerName()
173: Dim WshNetwork
174: Set WshNetwork = WScript.CreateObject("WScript.Network")
175: ' default value
176: GetComputerName = "."
177: GetComputerName = WshNetwork.ComputerName
178: End Function
179:
180: 'The Function IsMember and the Sub LoadGroups are a more robust way to check for group membership
181: 'I have pulled these functions from http://www.rlmueller.net/Programs/IsMember6.txt
182: Function IsMember(ByVal objADObject, ByVal strGroup)
183: ' Function to test for group membership.
184: ' objADObject is a user or computer object.
185: ' strGroup is the NT name (sAMAccountName) of the group to test.
186: ' objGroupList is a dictionary object, with global scope.
187: ' ADO is used to retrieve all group objects from the domain, with
188: ' their PrimaryGroupToken. Each objADObject has a PrimaryGroupID.
189: ' The group with the matching PrimaryGroupToken is the primary group.
190: ' Returns True if the user or computer is a member of the group.
191: ' Subroutine LoadGroups is called once for each different objADObject.
192:
193: Dim strPrimaryGroup, strDNSDomain
194: Dim intPrimaryGroupToken, intPrimaryGroupID
195:
196: If (IsEmpty(objGroupList) = True) Then
197: ' Create dictionary object.
198: Set objGroupList = CreateObject("Scripting.Dictionary")
199: objGroupList.CompareMode = vbTextCompare
200:
201: ' Use ADO to retrieve all group "primaryGroupToken" values.
202: Set adoConnection = CreateObject("ADODB.Connection")
203: Set adoCommand = CreateObject("ADODB.Command")
204: adoConnection.Provider = "ADsDSOObject"
205: adoConnection.Open "Active Directory Provider"
206: Set adoCommand.ActiveConnection = adoConnection
207: adoCommand.Properties("Page Size") = 100
208: adoCommand.Properties("Timeout") = 30
209: adoCommand.Properties("Cache Results") = False
210: strAttributes = "sAMAccountName,primaryGroupToken"
211: Set objRootDSE = GetObject("LDAP://RootDSE")
212: strDNSDomain = objRootDSE.Get("defaultNamingContext")
213: strFilter = "(objectCategory=group)"
214: strQuery = "<LDAP://" & strDNSDomain & ">;" & strFilter & ";" _
215: & strAttributes & ";subtree"
216: adoCommand.CommandText = strQuery
217: Set adoRecordset = adoCommand.Execute
218: End If
219: If (objGroupList.Exists(objADObject.sAMAccountName & "\") = False) Then
220: ' Call LoadGroups for each different objADObject.
221: ' Add object name to dictionary object so groups need only be
222: ' enumerated once.
223: Call LoadGroups(objADObject, objADObject)
224: objGroupList.Add objADObject.sAMAccountName & "\", True
225:
226: ' Determine which group is the primary group for this object.
227: intPrimaryGroupID = objADObject.primaryGroupID
228: adoRecordset.MoveFirst
229: Do Until adoRecordset.EOF
230: intPrimaryGroupToken = adoRecordset.Fields("primaryGroupToken").Value
231: If (intPrimaryGroupToken = intPrimaryGroupID) Then
232: strPrimaryGroup = adoRecordset.Fields("sAMAccountName").Value
233: objGroupList.Add objADObject.sAMAccountName & "\" _
234: & strPrimaryGroup, True
235: Exit Do
236: End If
237: adoRecordset.MoveNext
238: Loop
239: adoRecordset.Close
240: End If
241:
242: ' Check group membership.
243: IsMember = objGroupList.Exists(objADObject.sAMAccountName & "\" _
244: & strGroup)
245: End Function
246:
247: Sub LoadGroups(ByVal objPriADObject, ByVal objSubADObject)
248: ' Recursive subroutine to populate dictionary object with group
249: ' memberships. When this subroutine is first called by Function
250: ' IsMember, both objPriADObject and objSubADObject are the user or
251: ' computer object. On recursive calls objPriADObject still refers to the
252: ' user or computer object being tested, but objSubADObject will be a
253: ' group object. The dictionary object objGroupList keeps track of group
254: ' memberships for each user or computer separately.
255: ' For each group in the MemberOf collection, first check to see if
256: ' the group is already in the dictionary object. If it is not, add the
257: ' group to the dictionary object and recursively call this subroutine
258: ' again to enumerate any groups the group might be a member of (nested
259: ' groups). It is necessary to first check if the group is already in the
260: ' dictionary object to prevent an infinite loop if the group nesting is
261: ' "circular". The MemberOf collection does not include any "primary"
262: ' groups.
263:
264: Dim colstrGroups, objGroup, j
265: colstrGroups = objSubADObject.memberOf
266: If (IsEmpty(colstrGroups) = True) Then
267: Exit Sub
268: End If
269: If (TypeName(colstrGroups) = "String") Then
270: ' Escape any forward slash characters, "/", with the backslash
271: ' escape character. All other characters that should be escaped are.
272: colstrGroups = Replace(colstrGroups, "/", "\/")
273: Set objGroup = GetObject("LDAP://" & colstrGroups)
274: If (objGroupList.Exists(objPriADObject.sAMAccountName & "\" _
275: & objGroup.sAMAccountName) = False) Then
276: objGroupList.Add objPriADObject.sAMAccountName & "\" _
277: & objGroup.sAMAccountName, True
278: Call LoadGroups(objPriADObject, objGroup)
279: End If
280: Set objGroup = Nothing
281: Exit Sub
282: End If
283: For j = 0 To UBound(colstrGroups)
284: ' Escape any forward slash characters, "/", with the backslash
285: ' escape character. All other characters that should be escaped are.
286: colstrGroups(j) = Replace(colstrGroups(j), "/", "\/")
287: Set objGroup = GetObject("LDAP://" & colstrGroups(j))
288: If (objGroupList.Exists(objPriADObject.sAMAccountName & "\" _
289: & objGroup.sAMAccountName) = False) Then
290: objGroupList.Add objPriADObject.sAMAccountName & "\" _
291: & objGroup.sAMAccountName, True
292: Call LoadGroups(objPriADObject, objGroup)
293: End If
294: Next
295: Set objGroup = Nothing
296: End Sub