In the second part of this series, we discussed how to retrieve the current security settings for a WMI namespace.   For this blog post, I’ll show a Powershell script for modifying the current security descriptor of a WMI namespace.  Note that everything I’m doing in the Powershell script can be done in vbscript, but I’ll leave that as an exercise to the reader.  Here’s the entire script.  I’ll discuss the various sections to explain why I did something.  Note that this is not intended to be a discussion about Powershell, so I’m focusing on the WMI specific parts.

# Copyright (c) Microsoft Corporation.  All rights reserved. 

# For personal use only.  Provided AS IS and WITH ALL FAULTS.

 

# Set-WmiNamespaceSecurity.ps1

# Example: Set-WmiNamespaceSecurity root/cimv2 add steve Enable,RemoteAccess

 

Param ( [parameter(Mandatory=$true,Position=0)][string] $namespace,

    [parameter(Mandatory=$true,Position=1)][string] $operation,

    [parameter(Mandatory=$true,Position=2)][string] $account,

    [parameter(Position=3)][string[]] $permissions = $null,

    [bool] $allowInherit = $false,

    [bool] $deny = $false,

    [string] $computerName = ".",

    [System.Management.Automation.PSCredential] $credential = $null)

   

Process {

    $ErrorActionPreference = "Stop"

 

    Function Get-AccessMaskFromPermission($permissions) {

        $WBEM_ENABLE            = 1

                $WBEM_METHOD_EXECUTE = 2

                $WBEM_FULL_WRITE_REP   = 4

                $WBEM_PARTIAL_WRITE_REP              = 8

                $WBEM_WRITE_PROVIDER   = 0x10

                $WBEM_REMOTE_ACCESS    = 0x20

                $WBEM_RIGHT_SUBSCRIBE = 0x40

                $WBEM_RIGHT_PUBLISH      = 0x80

        $READ_CONTROL = 0x20000

        $WRITE_DAC = 0x40000

       

        $WBEM_RIGHTS_FLAGS = $WBEM_ENABLE,$WBEM_METHOD_EXECUTE,$WBEM_FULL_WRITE_REP,`

            $WBEM_PARTIAL_WRITE_REP,$WBEM_WRITE_PROVIDER,$WBEM_REMOTE_ACCESS,`

            $READ_CONTROL,$WRITE_DAC

        $WBEM_RIGHTS_STRINGS = "Enable","MethodExecute","FullWrite","PartialWrite",`

            "ProviderWrite","RemoteAccess","ReadSecurity","WriteSecurity"

 

        $permissionTable = @{}

 

        for ($i = 0; $i -lt $WBEM_RIGHTS_FLAGS.Length; $i++) {

            $permissionTable.Add($WBEM_RIGHTS_STRINGS[$i].ToLower(), $WBEM_RIGHTS_FLAGS[$i])

        }

       

        $accessMask = 0

 

        foreach ($permission in $permissions) {

            if (-not $permissionTable.ContainsKey($permission.ToLower())) {

                throw "Unknown permission: $permission`nValid permissions: $($permissionTable.Keys)"

            }

            $accessMask += $permissionTable[$permission.ToLower()]

        }

       

        $accessMask

    }

 

    if ($PSBoundParameters.ContainsKey("Credential")) {

        $remoteparams = @{ComputerName=$computer;Credential=$credential}

    } else {

        $remoteparams = @{}

    }

       

    $invokeparams = @{Namespace=$namespace;Path="__systemsecurity=@"} + $remoteParams

 

    $output = Invoke-WmiMethod @invokeparams -Name GetSecurityDescriptor

    if ($output.ReturnValue -ne 0) {

        throw "GetSecurityDescriptor failed: $($output.ReturnValue)"

    }

 

    $acl = $output.Descriptor

    $OBJECT_INHERIT_ACE_FLAG = 0x1

    $CONTAINER_INHERIT_ACE_FLAG = 0x2

 

    $computerName = (Get-WmiObject @remoteparams Win32_ComputerSystem).Name

   

    if ($account.Contains('\')) {

        $domainaccount = $account.Split('\')

        $domain = $domainaccount[0]

        if (($domain -eq ".") -or ($domain -eq "BUILTIN")) {

            $domain = $computerName

        }

        $accountname = $domainaccount[1]

    } elseif ($account.Contains('@')) {

        $domainaccount = $account.Split('@')

        $domain = $domainaccount[1].Split('.')[0]

        $accountname = $domainaccount[0]

    } else {

        $domain = $computerName

        $accountname = $account

    }

 

    $getparams = @{Class="Win32_Account";Filter="Domain='$domain' and Name='$accountname'"} + $remoteParams

 

    $win32account = Get-WmiObject @getparams

 

    if ($win32account -eq $null) {

        throw "Account was not found: $account"

    }

 

    switch ($operation) {

        "add" {

            if ($permissions -eq $null) {

                throw "-Permissions must be specified for an add operation"

            }

            $accessMask = Get-AccessMaskFromPermission($permissions)

   

            $ace = (New-Object System.Management.ManagementClass("win32_Ace")).CreateInstance()

            $ace.AccessMask = $accessMask

            if ($allowInherit) {

                $ace.AceFlags = $OBJECT_INHERIT_ACE_FLAG + $CONTAINER_INHERIT_ACE_FLAG

            } else {

                $ace.AceFlags = 0

            }

                       

            $trustee = (New-Object System.Management.ManagementClass("win32_Trustee")).CreateInstance()

            $trustee.SidString = $win32account.Sid

            $ace.Trustee = $trustee

           

            $ACCESS_ALLOWED_ACE_TYPE = 0x0

            $ACCESS_DENIED_ACE_TYPE = 0x1

 

            if ($deny) {

                $ace.AceType = $ACCESS_DENIED_ACE_TYPE

            } else {

                $ace.AceType = $ACCESS_ALLOWED_ACE_TYPE

            }

 

            $acl.DACL += $ace.psobject.immediateBaseObject

        }

       

        "delete" {

            if ($permissions -ne $null) {

                throw "Permissions cannot be specified for a delete operation"

            }

       

            [System.Management.ManagementBaseObject[]]$newDACL = @()

            foreach ($ace in $acl.DACL) {

                if ($ace.Trustee.SidString -ne $win32account.Sid) {

                    $newDACL += $ace.psobject.immediateBaseObject

                }

            }

 

            $acl.DACL = $newDACL.psobject.immediateBaseObject

        }

       

        default {

            throw "Unknown operation: $operation`nAllowed operations: add delete"

        }

    }

 

    $setparams = @{Name="SetSecurityDescriptor";ArgumentList=$acl.psobject.immediateBaseObject} + $invokeParams

 

    $output = Invoke-WmiMethod @setparams

    if ($output.ReturnValue -ne 0) {

        throw "SetSecurityDescriptor failed: $($output.ReturnValue)"

    }

}

 

Setting security is a bit more complicated than retrieving it.  You should have noticed that there are many more parameters for this Powershell function.  Let’s go over each of them:

Param ( [parameter(Mandatory=$true,Position=0)][string] $namespace,

    [parameter(Mandatory=$true,Position=1)][string] $operation,

    [parameter(Mandatory=$true,Position=2)][string] $account,

    [parameter(Position=3)][string[]] $permissions = $null,

    [bool] $allowInherit = $false,

    [bool] $deny = $false,

    [string] $computerName = ".",

    [System.Management.Automation.PSCredential] $credential = $null)

 

1.       $namespace is the WMI namespace that you intend to modify. 

2.       $operation is either Add or Delete. 

a.       If you want to modify existing Ace, you need to Delete (which will delete all Aces for a user/group) and then Add back what you want. 

3.       $account is the name of the user/group

4.       $permission is an array of the permissions to grant to the user/group

5.       $allowInherit controls whether child namespaces will inherit the Ace

6.       $deny indicates that this is a Deny Ace

a.       By default, you are adding Allow Aces, but you can explicitly Deny a permission

7.       $computerName is optional and can be a remote system

8.       $credential is the credentials to the remote system

 

Similar to the Get-WmiNamespaceSecurity script, we want to handle the AccessMask in a friendly way.  This is essentially the reverse of the function in the Get script and uses a similar logic.  Based on the supplied permissions, we produce the accessmask value for the Ace.

    Function Get-AccessMaskFromPermission($permissions) {

        $WBEM_ENABLE            = 1

$WBEM_METHOD_EXECUTE = 2

                $WBEM_FULL_WRITE_REP   = 4

                $WBEM_PARTIAL_WRITE_REP              = 8

                $WBEM_WRITE_PROVIDER   = 0x10

                $WBEM_REMOTE_ACCESS    = 0x20

       $READ_CONTROL = 0x20000

        $WRITE_DAC = 0x40000

       

        $WBEM_RIGHTS_FLAGS = $WBEM_ENABLE,$WBEM_METHOD_EXECUTE,$WBEM_FULL_WRITE_REP,`

            $WBEM_PARTIAL_WRITE_REP,$WBEM_WRITE_PROVIDER,$WBEM_REMOTE_ACCESS,`

            $READ_CONTROL,$WRITE_DAC

        $WBEM_RIGHTS_STRINGS = "Enable","MethodExecute","FullWrite","PartialWrite",`

            "ProviderWrite","RemoteAccess","ReadSecurity","WriteSecurity"

 

        $permissionTable = @{}

 

        for ($i = 0; $i -lt $WBEM_RIGHTS_FLAGS.Length; $i++) {

            $permissionTable.Add($WBEM_RIGHTS_STRINGS[$i].ToLower(), $WBEM_RIGHTS_FLAGS[$i])

        }

       

        $accessMask = 0

 

        foreach ($permission in $permissions) {

            if (-not $permissionTable.ContainsKey($permission.ToLower())) {

                throw "Unknown permission: $permission`nValid permissions: $($permissionTable.Keys)"

            }

            $accessMask += $permissionTable[$permission.ToLower()]

        }

       

        $accessMask

    }

 

The next section takes advantage of Powershell 2.0 splatting so I can store commonly used parameters in a single variable.  If credentials were not specified I don’t want the credential prompt to come up since this script can be used locally or remotely.  Finally, I get the current Security Descriptor so I can add or delete from it.  Always check the ReturnValue returned from calling the WMI method.

    if ($PSBoundParameters.ContainsKey("Credential")) {

        $remoteparams = @{ComputerName=$computer;Credential=$credential}

    } else {

        $remoteparams = @{}

    }

       

    $invokeparams = @{Namespace=$namespace;Path="__systemsecurity=@"} + $remoteParams

 

    $output = Invoke-WmiMethod @invokeparams -Name GetSecurityDescriptor

    if ($output.ReturnValue -ne 0) {

        throw "GetSecurityDescriptor failed: $($output.ReturnValue)"

    }

 

    $acl = $output.Descriptor

 

Next, we need to retrieve the Win32_Account associated with the user or group.  However, there are different ways of specifying the account name.  In a domain environment, you can use domain\account or account@domain.  For a local account, you can use .\account, computername\account, or just account.  This section of the script handles all these cases and normalizes the result so we can get the resulting Win32_Account instance.

    $computerName = (Get-WmiObject @remoteparams Win32_ComputerSystem).Name

   

    if ($account.Contains('\')) {

        $domainaccount = $account.Split('\')

        $domain = $domainaccount[0]

        if (($domain -eq ".") -or ($domain -eq "BUILTIN")) {

            $domain = $computerName

        }

        $accountname = $domainaccount[1]

    } elseif ($account.Contains('@')) {

        $domainaccount = $account.Split('@')

        $domain = $domainaccount[1].Split('.')[0]

        $accountname = $domainaccount[0]

    } else {

        $domain = $computerName

        $accountname = $account

    }

 

    $getparams = @{Class="Win32_Account";Filter="Domain='$domain' and Name='$accountname'"} + $remoteParams

 

    $win32account = Get-WmiObject @getparams

 

    if ($win32account -eq $null) {

        throw "Account was not found: $account"

    }

 

Now we move onto the actual operations.  Here we will handle adding a user and granting/denying specific permissions.  We create a new instance of the Win32_Ace class and set the inheritance flags appropriately.  Next, we create a new Win32_Trustee based on the user’s sid.  This class is an embedded object in the Win32_Ace instance.  With the accessmask and acetype set, we append it to the end of the existing DACL.

    switch ($operation) {

        "add" {

            if ($permissions -eq $null) {

                throw "-Permissions must be specified for an add operation"

            }

            $accessMask = Get-AccessMaskFromPermission($permissions)

   

            $ace = (New-Object System.Management.ManagementClass("win32_Ace")).CreateInstance()

            $ace.AccessMask = $accessMask

            if ($allowInherit) {

                $ace.AceFlags = $OBJECT_INHERIT_ACE_FLAG + $CONTAINER_INHERIT_ACE_FLAG

            } else {

                $ace.AceFlags = 0

            }

                       

            $trustee = (New-Object System.Management.ManagementClass("win32_Trustee")).CreateInstance()

            $trustee.SidString = $win32account.Sid

            $ace.Trustee = $trustee

           

            $ACCESS_ALLOWED_ACE_TYPE = 0x0

            $ACCESS_DENIED_ACE_TYPE = 0x1

 

            if ($deny) {

                $ace.AceType = $ACCESS_DENIED_ACE_TYPE

            } else {

                $ace.AceType = $ACCESS_ALLOWED_ACE_TYPE

            }

 

            $acl.DACL += $ace.psobject.immediateBaseObject

        }

 

For the delete operation, we search through all the Aces for the specific user and remove them from the current DACL.  If you want to modify existing permission, you would need to delete and then add.  You could add a modify operation to the script, but the logic gets a bit complicated if you want to support adding and removing permissions within the same operation.

        "delete" {

            if ($permissions -ne $null) {

                throw "Permissions cannot be specified for a delete operation"

            }

       

            [System.Management.ManagementBaseObject[]]$newDACL = @()

            foreach ($ace in $acl.DACL) {

                if ($ace.Trustee.SidString -ne $win32account.Sid) {

                    $newDACL += $ace.psobject.immediateBaseObject

                }

            }

 

            $acl.DACL = $newDACL.psobject.immediateBaseObject

        }

       

        default {

            throw "Unknown operation: $operation`nAllowed operations: add delete"

        }

    }

 

Now that we have the DACL modified, we just need to invoke the SetSecurityDescriptor method on the namespace and check the ReturnValue to ensure it succeded.

    $setparams = @{Name="SetSecurityDescriptor";ArgumentList=$acl.psobject.immediateBaseObject} + $invokeParams

 

    $output = Invoke-WmiMethod @setparams

    if ($output.ReturnValue -ne 0) {

        throw "SetSecurityDescriptor failed: $($output.ReturnValue)"

    }

 

Steve Lee
Senior Test Manager
Microsoft