Skip to content

Instantly share code, notes, and snippets.

@Bill-Stewart
Created October 17, 2019 14:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Bill-Stewart/ef603d7ff87f5380e6309bbfd2f8b8a1 to your computer and use it in GitHub Desktop.
Save Bill-Stewart/ef603d7ff87f5380e6309bbfd2f8b8a1 to your computer and use it in GitHub Desktop.
' EnforceLocalAdmin.vbs
' Written by Bill Stewart (bstewart@iname.com)
'
' This VBScript script allows you to update the membership of the
' Administrators group on one or more computers.
'
' The first unnamed argument is a comma-delimited list of accounts.
'
' Without /addonly or /removeonly, the Administrators group should contain only
' the named accounts.
'
' If specifying /addonly, the account list specifies a list of accounts that
' should be added to the group, and no accounts are removed.
'
' If specifying /removeonly, the account list specifies a list of accounts to
' remove, and no accounts are added.
'
' Accounts are in "domain\name" format. Omit "domain\" to specify a local
' account.
'
' The Administrators group is determined by SID (S-1-5-32-544), not by name.
' The script also ignores the Administrator account (RID 500) when removing
' accounts.
'
' The script ignores domain accounts when removing accounts unless you use the
' /domain parameter.
'
' The script runs silently unless you specify /v (verbose).
'
' Of course, you must run the script at high privilege level/elevated ("Run as
' administrator" from Explorer or "Run with highest privileges" from Task
' Scheduler are two names for this).
'
' You can specify one or more remote computers with the /computer parameter.
'
' Version history:
'
' 1.2 (2014-09-04)
' * Added /removeonly parameter.
'
' 1.1 (2014-07-30)
' * Added /addonly parameter.
' * Minor bugfix - added computer name if we fail to bind to group.
'
' 1.0 (2014-07-22)
' * Initial version.
Option Explicit
' Outputs a usage message and ends the script.
Sub HelpAndExit()
WScript.Echo "Updates the Administrators group membership on computers." & vbNewLine _
& vbNewLine _
& "Usage:" & vbTab & "EnforceLocalAdmin.vbs ""[domain\]account[,...]""" & vbNewLine _
& vbTab & "[/computer:""name[,...]""] [/addonly | /removeonly] [/domain] [/v]" & vbNewLine _
& vbNewLine _
& "Specify a comma-delimited list of account names enclosed in quotes (""""). Use" & vbNewLine _
& "[domain\]name format (omit the 'domain\' part for local accounts)." & vbNewLine _
& vbNewLine _
& "To update the Administrators group on remote computers, specify the /computer" & vbNewLine _
& "parameter and a comma-delimited list of computer names, enclosed in quotes." & vbNewLine _
& vbNewLine _
& "* Without /addonly or /removeonly, the Administrators group will be updated to" & vbNewLine _
& "contain only the named accounts as members." & vbNewLine _
& vbNewLine _
& "* With /addonly, the named accounts will be added to the Administrators group" & vbNewLine _
& "if they are not members." & vbNewLine _
& vbNewLine _
& "* With /removeonly, the named accounts will be removed from the" & vbNewLine _
& "Administrators group if they are members." & vbNewLine _
& vbNewLine _
& "Domain accounts are ignored when removing accounts unless you specify the" & vbNewLine _
& "/domain parameter." & vbNewLine _
& vbNewLine _
& "No output is generated unless you specify the /v (verbose) parameter."
WScript.Quit
End Sub
' Main procedure.
Sub Main()
Dim Args
Set Args = WScript.Arguments
' If no parameters or /? specified, show help and exit script.
If Args.Named.Exists("?") Or (Args.Unnamed.Count = 0) Then
HelpAndExit
End If
' Get list of accounts.
Dim AccountList
AccountList = Split(Args.Unnamed.Item(0), ",")
' Get list of computer names. If not specified, assume current computer.
Dim ComputerList
If Args.Named.Exists("computer") Then
ComputerList = Split(Args.Named.Item("computer"), ",")
Else
ComputerList = Array("")
End If
' Include domain accounts?
Dim IncludeDomain
IncludeDomain = Args.Named.Exists("domain")
' Skip remove step?
Dim AddOnly
AddOnly = Args.Named.Exists("addonly")
Dim RemoveOnly
RemoveOnly = Args.Named.Exists("removeonly")
' Mutually exclusive options; /addonly wins.
If AddOnly And RemoveOnly Then
RemoveOnly = False
End If
' Produce output?
Dim Verbose
Verbose = Args.Named.Exists("v")
' Create an AdminGroupManagerClass instance.
Dim AdminGroupMgr
Set AdminGroupMgr = New AdminGroupManagerClass
' Create a ListCompareClass instance.
Dim ListCompare
Set ListCompare = New ListCompareClass
' Create a ListBuilderClass instance.
Dim ListBuilder
Set ListBuilder = New ListBuilderClass
Dim ComputerName, Status, Members, ToRemove, Results, ToAdd
For Each ComputerName In ComputerList
Status = AdminGroupMgr.Connect(ComputerName)
If Status = 0 Then
If Not RemoveOnly Then
If Not AddOnly Then
' Get members, including domain accounts if requested.
Members = AdminGroupMgr.GetMembers(IncludeDomain)
' Get members of group that should be removed.
ToRemove = ListCompare.GetItemsNotInList(AccountList, Members)
' Remove members and capture the results.
Results = AdminGroupMgr.ChangeMembership("Remove", ToRemove)
' If result array is empty, nothing happened.
If UBound(Results) = -1 Then
Results = Array("No accounts removed from '" & AdminGroupMgr.AdminGroupName & "' on " & AdminGroupMgr.ComputerName)
End If
' Add results to list.
ListBuilder.AddList Results
End If
' Get all members.
Members = AdminGroupMgr.GetMembers(True)
' Get list of accounts that are not already members.
ToAdd = ListCompare.GetItemsNotInList(Members, AccountList)
' Add members and capture the results.
Results = AdminGroupMgr.ChangeMembership("Add", ToAdd)
' If result array is empty, nothing happened.
If UBound(Results) = -1 Then
Results = Array("No accounts added to '" & AdminGroupMgr.AdminGroupName & "' on " & AdminGroupMgr.ComputerName)
End If
' Add results to list.
ListBuilder.AddList Results
Else ' /removeonly specified.
' Get members, including domain accounts if requested.
Members = AdminGroupMgr.GetMembers(IncludeDomain)
' Get members of group that should be removed.
ToRemove = ListCompare.GetItemsInList(Members, AccountList)
' Remove members and capture the results.
Results = AdminGroupMgr.ChangeMembership("Remove", ToRemove)
' If result array is empty, nothing happened.
If UBound(Results) = -1 Then
Results = Array("No accounts removed from '" & AdminGroupMgr.AdminGroupName & "' on " & AdminGroupMgr.ComputerName)
End If
' Add results to list.
ListBuilder.AddList Results
End If
Else
' Add connect error to list.
ListBuilder.AddList Array("Error " & CStr(Status) & " connecting to " & AdminGroupMgr.ComputerName)
End If
Next
If Verbose Then
WScript.Echo ListBuilder.ToString()
End If
End Sub
' Class for comparing two lists.
' Interface:
' * Method GetItemsInList(List1, List2)
' Returns an array of items from List2 that exist in List1.
' * Method GetItemsNotInList(List1, List2)
' Returns an array of items from List2 that do not exist in List1.
Class ListCompareClass
Private c_Hash
Private Sub Class_Initialize()
Set c_Hash = CreateObject("Scripting.Dictionary")
End Sub
' Returns True if List contains TestItem (not case-sensitive).
Private Function Contains(ByRef List, ByVal TestItem)
Contains = False
Dim Item
For Each Item In List
If UCase(Item) = UCase(TestItem) Then
Contains = True
Exit Function
End If
Next
End Function
' Returns an array of items from List2 that are in List1
' (not case-sensitive).
Public Function GetItemsInList(ByRef List1, ByRef List2)
c_Hash.RemoveAll
Dim I
For I = 0 To UBound(List2)
If Contains(List1, List2(I)) Then
c_Hash.Add I, List2(I)
End If
Next
GetItemsInList = c_Hash.Items()
End Function
' Returns an array of items from List2 that are not in List1
' (not case-sensitive).
Public Function GetItemsNotInList(ByRef List1, ByRef List2)
c_Hash.RemoveAll
Dim I
For I = 0 To UBound(List2)
If Not Contains(List1, List2(I)) Then
c_Hash.Add I, List2(I)
End If
Next
GetItemsNotInList = c_Hash.Items()
End Function
End Class
' Class for building a list.
' Interface:
' * Method AddList(List)
' Adds a list of items to the list.
' * Method ToString()
' Returns the list as a newline-delimited string.
Class ListBuilderClass
Private c_Hash
Private c_Index
Private Sub Class_Initialize()
Set c_Hash = CreateObject("Scripting.Dictionary")
c_Index = 0
End Sub
' Adds a list of items to the list.
Public Sub AddList(ByRef List)
Dim Item
For Each Item In List
c_Hash.Add c_Index, Item
c_Index = c_Index + 1
Next
End Sub
' Returns list as a newline-delimited string.
Public Function ToString()
Dim Result, Item
Result = ""
For Each Item In c_Hash.Items()
If Result = "" Then
Result = Item
Else
Result = Result & vbNewLine & Item
End If
Next
ToString = Result
End Function
End Class
' Class for converting ADSI objectSid attribute to a string.
' Interface:
' * Method ToString(ByteArray)
' Returns the SID as a string (e.g., S-1-5-...).
Class SIDConverterClass
' Reverses the "endian-ness" of the hex string.
Private Function SwapEndian(ByVal HexStr)
Dim Result, I
Result = ""
For I = Len(HexStr) To 1 Step -2
Result = Result & Mid(HexStr, I - 1, 2)
Next
SwapEndian = Result
End Function
' Returns a SID byte array as a string (e.g., S-1-5-...).
Public Function ToString(ByVal ByteArray)
Const SID_VERSION_LENGTH = 2
Const SID_SUBAUTHORITY_COUNT = 2
Const SID_AUTHORITY_LENGTH = 12
Const SID_SUBAUTHORITY_LENGTH = 8
' Converts array of bytes into a raw hex string.
Dim HexStr, I
HexStr = ""
For I = 1 To LenB(ByteArray)
HexStr = HexStr & Right("0" & Hex(AscB(MidB(ByteArray, I, 1))), 2)
Next
' Step through the hex string and convert it to a SID string.
I = 1
Dim Version
Version = CByte("&H" & Mid(HexStr, I, SID_VERSION_LENGTH))
I = I + SID_VERSION_LENGTH
Dim SubAuthorityCount
SubAuthorityCount = CByte("&H" & Mid(HexStr, I, SID_SUBAUTHORITY_COUNT))
I = I + SID_SUBAUTHORITY_COUNT
Dim Authority
Authority = CLng("&H" & Mid(HexStr, I, SID_AUTHORITY_LENGTH))
I = I + SID_AUTHORITY_LENGTH
Dim Result
Result = "S-" & CStr(Version) & "-" & CStr(Authority)
Dim J
Do Until SubAuthorityCount = 0
J = CLng("&H" & SwapEndian(Mid(HexStr, I, SID_SUBAUTHORITY_LENGTH)))
If J < 0 Then
J = (2 ^ 32) + J
End If
Result = Result & "-" & CStr(J)
I = I + SID_SUBAUTHORITY_LENGTH
SubAuthorityCount = SubAuthorityCount - 1
Loop
ToString = Result
End Function
End Class
' Class for managing the Administrators group on a computer. Requires the
' SIDConverterClass class.
' Interface:
' * Method Connect(ComputerName)
' Returns 0 if successfully connected to the computer.
' * Method GetMembers()
' Returns an array containing the members of the Administrators group.
' * Method ChangeMembership(Action, MemberList)
' Adds or removes a list of members from the Administrators group.
' * Property ComputerName
' Gets the computer name.
' * Property AdminGroupName
' Gets the Administrators group name.
Class AdminGroupManagerClass
Private c_RE ' RegExp object
Private c_Hash ' Dictionary object
Private c_SIDConverter ' SIDConverter object
Private c_ComputerName ' Computer name
Private c_AdminGroupName ' Admin group name
Private c_DomainName ' Domain or workgroup name
Private Sub Class_Initialize()
Set c_RE = New RegExp
Set c_Hash = CreateObject("Scripting.Dictionary")
Set c_SIDConverter = New SIDConverterClass
c_ComputerName = ""
c_AdminGroupName = ""
c_DomainName = ""
End Sub
' Returns low word to get Win32 error code.
Private Function GetWin32Error(ByVal ErrorCode)
GetWin32Error = ErrorCode And 65535
End Function
' Connects to a computer to manage its Administrators group.
Public Function Connect(ByVal ComputerName)
If (ComputerName = "") Or (ComputerName = ".") Then
' Empty string or "." is shorthand for local computer.
c_ComputerName = CreateObject("WScript.Network").ComputerName
Else
c_ComputerName = ComputerName
End If
Dim ADsContainer
On Error Resume Next
Set ADsContainer = GetObject("WinNT://" & c_ComputerName & ",Computer")
If Err.Number <> 0 Then
Connect = GetWin32Error(Err.Number)
Exit Function
End If
On Error GoTo 0
ADsContainer.Filter = Array("Group")
Dim Group
For Each Group In ADsContainer
If c_SIDConverter.ToString(Group.objectSid) = "S-1-5-32-544" Then
c_AdminGroupName = Group.Name
c_RE.Pattern = "^WinNT://([^/,]+)"
Dim Matches, Match
Set Matches = c_RE.Execute(Group.ADsPath)
For Each Match In Matches
c_DomainName = Match.SubMatches.Item(0)
Exit For
Next
Exit For
End If
Next
End Function
' Returns 'domainname\accountname' from WinNT ADsPath.
Private Function GetNetBIOSName(ByVal ADsPath)
Dim Matches, Match, Authority, Name
c_RE.Pattern = "^WinNT://(?:([^/,]+)/)?([^/,]+)/([^/,]+)(?:,([^/,]+))?$"
Set Matches = c_RE.Execute(ADsPath)
For Each Match In Matches
Authority = Match.SubMatches.Item(1)
Name = Match.SubMatches.Item(2)
Next
' If authority = local computer name, return name only. Otherwise,
' return domain\name.
If StrComp(Authority, c_ComputerName, vbTextCompare) = 0 Then
GetNetBIOSName = Name
Else
GetNetBIOSName = Authority & "\" & Name
End If
End Function
' Returns WinNT ADsPath from NetBIOS name ('domainname\accountname').
Private Function GetADsPath(ByVal NetBIOSName)
Dim Names
Names = Split(NetBIOSName, "\")
If UBound(Names) = 0 Then
GetADsPath = "WinNT://" & c_DomainName & "/" & c_ComputerName & "/" & Names(0)
ElseIf UBound(Names) >= 1 Then
GetADsPath = "WinNT://" & Names(0) & "/" & Names(1)
End If
End Function
' Returns an array containing the membership of the Administrators group.
' Names are returned in NetBIOS format ('domainname\accountname'). If
' IncludeDomain = False, then domain accounts are excluded from the array.
Public Function GetMembers(ByVal IncludeDomain)
GetMembers = Array()
On Error Resume Next
Dim AdminGroup
Set AdminGroup = GetObject("WinNT://" & c_DomainName & "/" & c_ComputerName & "/" & c_AdminGroupName & ",Group")
If Err.Number <> 0 Then
Exit Function
End If
Dim Members
Set Members = AdminGroup.Members()
If Err.Number <> 0 Then
Exit Function
End If
On Error GoTo 0
Dim I, Member, NetBIOSName, DomainAccount
I = 0
c_Hash.RemoveAll
For Each Member In Members
NetBIOSName = GetNetBIOSName(Member.ADsPath)
DomainAccount = InStr(NetBIOSName, "\") >= 1
If IncludeDomain Or ((Not IncludeDomain) And (Not DomainAccount)) Then
c_Hash.Add I, NetBIOSName
End If
I = I + 1
Next
GetMembers = c_Hash.Items()
End Function
' Adds members to (Action = "Add") or removes members from (Action =
' "Remove") the Administrators group. Input list must specify names in
' NetBIOS name format ("[domain\]name"). If Action = "Remove", the
' function ignores the built-in Administrator account (RID 500).
' Returns an array of result strings.
Public Function ChangeMembership(ByVal Action, ByVal MemberList)
On Error Resume Next
Dim AdminGroup
Set AdminGroup = GetObject("WinNT://" & c_DomainName & "/" & c_ComputerName & "/" & c_AdminGroupName & ",Group")
If Err.Number <> 0 Then
ChangeMembership = Array("Error " & GetWin32Error(Err.Number) & " getting group '" & c_AdminGroupName & "' on " & c_ComputerName)
Exit Function
End If
c_Hash.RemoveAll
Dim I, Skip, ADsPath, Member, ErrorCode
For I = 0 To UBound(MemberList)
Skip = False
ADsPath = GetADsPath(MemberList(I))
If Action = "Add" Then
AdminGroup.Add ADsPath
ElseIf Action = "Remove" Then
Set Member = GetObject(ADsPath)
' Ignore the built-in Administrator account.
Skip = Right(c_SIDConverter.ToString(Member.objectSid), 4) = "-500"
If Not Skip Then
AdminGroup.Remove ADsPath
End If
End If
If Not Skip Then
ErrorCode = Err.Number
If ErrorCode = 0 Then
If Action = "Add" Then
c_Hash.Add I, "Added '" & MemberList(I) & "' to '" & c_AdminGroupName & "' on " & c_ComputerName
ElseIf Action = "Remove" Then
c_Hash.Add I, "Removed '" & MemberList(I) & "' from '" & c_AdminGroupName & "' on " & c_ComputerName
End If
Else
If Action = "Add" Then
c_Hash.Add I, "Error " & GetWin32Error(ErrorCode) & " adding '" & MemberList(I) & "' to '" & c_AdminGroupName & "' on " & c_ComputerName
ElseIf Action = "Remove" Then
c_Hash.Add I, "Error " & GetWin32Error(ErrorCode) & " removing '" & MemberList(I) & "' from '" & c_AdminGroupName & "' on " & c_ComputerName
End If
End If
End If
Err.Clear
Next
ChangeMembership = c_Hash.Items()
End Function
' Returns the computer name.
Public Property Get ComputerName()
ComputerName = c_ComputerName
End Property
' Returns the name of the Administrators group.
Public Property Get AdminGroupName()
AdminGroupName = c_AdminGroupName
End Property
End Class
' Execute Main procedure.
Main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment