Created
October 17, 2019 14:33
-
-
Save Bill-Stewart/ef603d7ff87f5380e6309bbfd2f8b8a1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
' EnforceLocalAdmin.vbs | |
' Written by Bill Stewart ([email protected]) | |
' | |
' 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