11

Windows Management Instrumentation

Windows Management Instrumentation (WMI) was introduced as a downloadable component with Windows 95 and NT. Windows 2000 had WMI pre-installed, and it has since become a core part of the operating system.

You can use WMI to access a huge amount of information about the computer system. This includes printers, device drivers, user accounts, ODBC, and so on; there are hundreds of classes to explore.

In this chapter, we will be covering the following topics:

  • Working with WMI
  • CIM cmdlets
  • The WMI Query Language
  • WMI type accelerators
  • Permissions

Working with WMI

The scope of WMI is vast, which makes it a fantastic resource for automating processes. WMI classes are not limited to the core operating system; it is not uncommon to find classes created after software or device drivers have been installed.

Given the scope of WMI, finding an appropriate class can be difficult. PowerShell itself is well equipped to explore the available classes.

WMI classes

PowerShell, as a shell for working with objects, presents WMI classes in a similar manner to .NET classes or any other object. There are several parallels between WMI classes and .NET classes.

A WMI class is used as the recipe to create an instance of a WMI object. The WMI class defines properties and methods. The WMI class Win32_Process is used to gather information about running processes in a similar manner to the Get-Process command.

The Win32_Process class has properties such as ProcessId, Name, and CommandLine. It has a terminate method that can be used to kill a process, as well as a create static method that can be used to spawn a new process.

WMI classes reside within a WMI namespace. The default namespace is rootcimv2; classes such as Win32_OperatingSystem and Win32_LogicalDisk reside in this namespace.

WMI commands

PowerShell has two different sets of commands dedicated to working with WMI.

Get-WmiObject is included with PowerShell 1.0 to 5.1 but is not present in PowerShell 6 and above.

The CIM cmdlets were introduced with PowerShell 3.0. They are compatible with the Distributed Management Task Force (DMTF) standard DSP0004. A move toward compliance with open standards is critical as the Microsoft world becomes more diverse.

WMI itself is a proprietary implementation of the CIM server, using the Distributed Component Object Model (DCOM) API to communicate between the client and server.

Standards compliance and differences in approach aside, there are solid, practical reasons to consider when choosing which one to use.

Some properties of CIM cmdlets are as follows:

  • They are available in both Windows PowerShell and PowerShell Core.
  • They handle date conversion natively.
  • They have a flexible approach to networking. They use WSMAN for remote connections by default but can be configured to use DCOM over RPC.
  • They can be used for all WMI operations.

Some properties of WMI cmdlets are as follows:

  • They are only available in Windows PowerShell and are not in PowerShell Core
  • They do not automatically convert dates
  • They use DCOM over RPC exclusively
  • They can be used for all WMI operations
  • They have been superseded by the CIM cmdlets

CIM cmdlets

As mentioned in the previous section, the Common Information Model (CIM) cmdlets were introduced with PowerShell 3 and are the only commands available to access WMI in PowerShell 6 and above.

The CIM commands are as follows:

  • Get-CimAssociatedInstance
  • Get-CimClass
  • Get-CimInstance
  • Get-CimSession
  • Invoke-CimMethod
  • New-CimInstance
  • New-CimSession
  • New-CimSessionOption
  • Register-CimIndicationEvent
  • Remove-CimInstance
  • Remove-CimSession
  • Set-CimInstance

Each of these CIM cmdlets uses either the ComputerName or CimSession parameter to target the operation at another computer.

Getting instances

You can use the Get-CimInstance command to execute queries for instances of WMI objects, as the following code shows:

Get-CimInstance -ClassName Win32_OperatingSystem 
Get-CimInstance -ClassName Win32_Service 
Get-CimInstance -ClassName Win32_Share 

Several different parameters are available when using Get-CimInstance. The command can be used with a filter, as follows:

Get-CimInstance Win32_Directory -Filter "Name='C:\Windows'" 
Get-CimInstance Win32_Service -Filter "State='Running'" 

Each command will return instances matching the filter. In the case of Win32_Directory, a single object is returned, as the following output shows:

Name        Hidden  Archive  Writeable  LastModified
----        ------  -------  ---------  ------------
C:Windows  False   False    True       20/02/2021 09:25:20

The Filter format is WQL and will be explored later in this chapter.

When returning large amounts of information, the Property parameter can be used to reduce the number of fields returned by a query:

Get-CimInstance Win32_UserAccount -Property Name, SID 

You can also use the Query parameter, although it is rare to find a use for this that cannot be served by the individual parameters:

Get-CimInstance -Query "SELECT * FROM Win32_Process" 
Get-CimInstance -Query "SELECT Name, SID FROM Win32_UserAccount" 

Getting classes

The Get-CimClass command is used to return the details of a WMI class:

PS> Get-CimClass Win32_Process
NameSpace: ROOT/cimv2
CimClassName     CimClassMethods                CimClassProperties 
------------     ---------------                ------------------ 
Win32_Process    {Create, Terminate, Get...}    {Caption, Description...} 

The Class object describes the capabilities of that class. By default, Get-CimClass lists classes from the rootcimv2 namespace.

The Namespace parameter will fill using tab completion, meaning if the following partial command is entered, pressing Tab repeatedly will cycle through the possible root namespaces:

Get-CimClass -Namespace <tab, tab, tab> 

The child namespaces of a given namespace are listed in a __Namespace class instance. For example, the following command returns the namespaces under root:

Get-CimInstance __Namespace -Namespace root 

Extending this technique, it is possible to recursively query __Namespace to find all the possible namespace values. Certain WMI namespaces are only available to administrative users (run as administrator); the following function may display errors for some namespaces:

function Get-CimNamespace { 
    param ( 
        $Namespace = 'root' 
    ) 
       
    Get-CimInstance __Namespace -Namespace $Namespace | ForEach-Object { 
        $childNamespace = Join-Path -Path $Namespace -ChildPath $_.Name 
        $childNamespace 
 
        Get-CimNamespace -Namespace $childNamespace 
    } 
} 
Get-CimNamespace 

CIM class objects describe the methods available for a class. These methods are used to change objects.

Calling methods

You can use the Invoke-CimMethod command to call a method. The CIM class can be used to find details of the methods that a class supports:

PS> (Get-CimClass Win32_Process).CimClassMethods 
Name        ReturnType   Parameters        Qualifiers 
----        ----------   ----------        ---------- 
Create      UInt32       {CommandLine...}  {Constructor...}
Terminate   UInt32       {Reason}          {Destructor...} 
GetOwner    UInt32       {Domain...}       {Implemented...}
GetOwnerSid UInt32       {Sid}             {Implemented...}

The method with the Constructor qualifier can be used to create a new instance of Win32_Process.

The Parameters property of a specific WMI method can be explored to find out how to use a method:

PS> (Get-CimClass Win32_Process).CimClassMethods['Create'].Parameters
Name                          CimType    Qualifiers
----                          -------    ----------
CommandLine                    String    {ID, In, MappingStrings}
CurrentDirectory               String    {ID, In, MappingStrings}
ProcessStartupInformation    Instance    {EmbeddedInstance, ID, In, MappingStrings}
ProcessId                      UInt32    {ID, MappingStrings, Out}

If an argument has the In qualifier, it can be passed in when you're creating an object. If an argument has the Out qualifier, it will be returned once the instance has been created. Arguments are passed in using a Hashtable.

When creating a process, the CommandLine argument is required; the rest can be ignored until later:

$params = @{
    ClassName  = 'Win32_Process'
    MethodName = 'Create'
    Arguments  = @{
        CommandLine = 'notepad.exe' 
    } 
}
$return = Invoke-CimMethod @params

The return object holds three properties in the case of the Create method of Win32_Process. This includes ProcessId and ReturnValue. PowerShell adds a PSComputerName property to these:

PS> $return
ProcessId    ReturnValue     PSComputerName
---------    -----------     --------------
    15172              0 

PSComputerName is blank when a request is local. ProcessId is the Out property listed under the Create method parameters. ReturnValue indicates whether the operation succeeded, and 0 indicates that it was successful.

A nonzero return value indicates that something went wrong, but the values are not translated in PowerShell. The return values are documented in Microsoft Docs: https://docs.microsoft.com/windows/win32/cimwin32prov/create-method-in-class-win32-process.

The Create method used here creates a new instance. The other methods for Win32_Process act against an existing instance (an existing process).

Extending the preceding example, a process can be created and then terminated:

$params = @{
    ClassName  = 'Win32_Process'
    MethodName = 'Create'
    Arguments  = @{ 
        CommandLine = 'notepad.exe' 
    }
}
$return = Invoke-CimMethod @params
pause
Get-CimInstance Win32_Process -Filter "ProcessID=$($return.ProcessId)" | 
    Invoke-CimMethod -MethodName Terminate 

The pause command will wait for return to be pressed before continuing; this allows us to show that Notepad was opened before it was terminated.

The Terminate method has an optional argument that is used as the exit code for the terminate process. This argument may be added using a Hashtable; in this case, a (made up) value of 5 is set as the exit code:

$invokeParams = @{
    ClassName  = 'Win32_Process'
    MethodName = 'Create'
    Arguments  = @{
        CommandLine = 'notepad.exe' 
    }
}
$return = Invoke-CimMethod @invokeParams
$getParams = @{
    ClassName = 'Win32_Process'
    Filter    = 'ProcessId={0}' -f $return.ProcessId
}
Get-CimInstance @getParams |
    Invoke-CimMethod -MethodName Terminate -Arguments @{Reason = 5}

Invoke-CimMethod returns an object with a ReturnValue. A return value of 0 indicates that the command succeeded. A nonzero value indicates an error condition. The meaning of the value will depend on the WMI class.

The return values associated with the Terminate method of Win32_Process are documented in Microsoft Docs: https://docs.microsoft.com/windows/win32/cimwin32prov/terminate-method-in-class-win32-process.

Some methods require or can use instances of CIM classes as arguments. In some cases, these instances must be created.

Creating instances

The arguments for Win32_Process include a ProcessStartupInformation parameter. ProcessStartupInformation is described by a WMI class, Win32_ProcessStartup.

There are no existing instances of Win32_ProcessStartup; running Get-CimInstance Win32_ProcessStartup will not find anything. The Win32_ProcessStartup class doesn't have a Create method (or any other constructor) that can be used to create an instance either.

You can use New-CimInstance to create an instance of the class:

$class = Get-CimClass Win32_ProcessStartup 
$startupInfo = New-CimInstance -CimClass $class -ClientOnly 

You can also use New-Object:

$class = Get-CimClass Win32_ProcessStartup 
$startupInfo = New-Object CimInstance $class 

Finally, the new method may be used:

$class = Get-CimClass Win32_ProcessStartup 
$startupInfo = [CimInstance]::new($class)

You can set properties on the created instance; the effect of each property is documented in Microsoft Docs: https://docs.microsoft.com/windows/win32/cimwin32prov/win32-processstartup.

In the following example, properties are set to dictate the position and title of a pwsh.exe window:

$class = Get-CimClass Win32_ProcessStartup 
$startupInfo = New-CimInstance -CimClass $class -ClientOnly 
$startupInfo.X = 50 
$startupInfo.Y = 50 
$startupInfo.Title = 'This is the window title' 
$params = @{
    ClassName  = 'Win32_Process'
    MethodName = 'Create'
    Arguments  = @{ 
        CommandLine               = 'pwsh.exe' 
        ProcessStartupInformation = $startupInfo 
    }
} 
$returnObject = Invoke-CimMethod @params 

If the process starts successfully, $returnObject will have a ReturnValue of 0.

Working with CIM sessions

As we mentioned earlier in this chapter, a key feature of the CIM cmdlets is their ability to change how connections are formed and used.

The Get-CimInstance command has a ComputerName parameter, and when you use this, the command automatically creates a session to a remote system using WSMAN. The connection is destroyed as soon as the command completes.

The Get-CimSession, New-CimSession, New-CimSessionOption, and Remove-CimSession commands are optional commands that you can use to define the behavior of remote connections.

The New-CimSession command creates a connection to a remote server. An example is as follows:

PS> $cimSession = New-CimSession -ComputerName Remote1
PS> $cimSession
Id           : 1
Name         : CimSession1
InstanceId   : 1cc2a889-b649-418c-94a2-f24e033883b4
ComputerName : Remote1
Protocol     : WSMAN 

Alongside the other parameters, New-CimSession has a Credential parameter that can be used in conjunction with Get-Credential to authenticate a connection.

If the remote system does not, for any reason, present access to WSMAN, it is possible to switch the protocol down to DCOM by using the New-CimSessionOption command:

PS> $option = New-CimSessionOption -Protocol DCOM
PS> $cimSession = New-CimSession -ComputerName Remote1 –SessionOption $option
PS> $cimSession
Id           : 2
Name         : CimSession2
InstanceId   : 62b2cb56-ec84-472c-a992-4bee59ee0618
ComputerName : Remote1
Protocol     : DCOM 

The New-CimSessionOption command is not limited to protocol switching; it can affect many of the other properties of the connection, as shown in the help and the examples for the command.

Once a session has been created, it exists in memory until it is removed. The Get-CimSession command shows a list of connections that have been formed, and the Remove-CimSession command permanently removes connections.

Associated classes

The Get-CimAssociatedClass command replaces the use of the ASSOCIATORS OF query type when using the CIM cmdlets.

The following command gets the class instances associated with Win32_NetworkAdapterConfiguration. As the arguments for the Get-CimInstance command are long strings, splatting is used to pass the parameters into the command:

$params = @{ 
    ClassName = 'Win32_NetworkAdapterConfiguration' 
    Filter    = 'IPEnabled=TRUE AND DHCPEnabled=TRUE' 
} 
Get-CimInstance @params | Get-CimAssociatedInstance  

The following example uses Get-CimAssociatedClass to get the physical interface associated with the IP configuration:

$params = @{ 
    ClassName = 'Win32_NetworkAdapterConfiguration' 
    Filter    = 'IPEnabled=TRUE AND DHCPEnabled=TRUE' 
} 
Get-CimInstance @params | ForEach-Object { 
    $adapter = $_ | Get-CimAssociatedInstance -ResultClassName Win32_NetworkAdapter 
 
    [PSCustomObject]@{ 
        NetConnectionID = $adapter.NetConnectionID 
        Speed           = [Math]::Round($adapter.Speed / 1MB, 2) 
        IPAddress       = $_.IPAddress 
        IPSubnet        = $_.IPSubnet 
        Index           = $_.Index 
        Gateway         = $_.DefaultIPGateway 
    } 
} 

The preceding command returns details of every IP- and DHCP-enabled network adapter, merging the results of two different CIM classes into a single object.

The WMI Query Language

WMI Query Language, or WQL, is used to query WMI in a similar style to SQL.

WQL implements a subset of Structured Query Language (SQL). The keywords are traditionally written in uppercase; however, WQL is not case-sensitive.

Both the CIM and the older WMI cmdlets support the Filter and Query parameters, which accept WQL queries.

Understanding SELECT, WHERE, and FROM

The SELECT, WHERE, and FROM keywords are used with the Query parameter.

The generalized syntax for the Query parameter is as follows:

SELECT <Properties> FROM <WMI Class> 
SELECT <Properties> FROM <WMI Class> WHERE <Condition> 

You can use the wildcard * to request all available properties or a list of known properties:

Get-CimInstance -Query "SELECT * FROM Win32_Process" 
Get-CimInstance -Query "SELECT ProcessID, CommandLine FROM Win32_Process" 

The WHERE keyword is used to filter results returned by SELECT; for example, see the following:

Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessID=$PID" 

WQL and arrays

WQL cannot filter array-based properties; for example, the capabilities property of Win32_DiskDrive.

Escape sequences and wildcards

The backslash character, , is used to escape the meaning of characters in a WMI query. You can use it to escape a wildcard character, quotes, or itself. For example, the following WMI query uses a path; each instance of in the path must be escaped:

Get-CimInstance Win32_Process -Filter "ExecutablePath='C:\Windows\Explorer.exe'" 

The preceding command returns any instances of the explorer.exe process, as shown here:

ProcessId Name         HandleCount WorkingSetSize VirtualSize
--------- ----         ----------- -------------- -----------
8320      explorer.exe 3412        198606848      2204322111488

The properties shown will vary from one computer to another.

About Win32_Process and the Path property

The Path script property is added to the output from the Win32_Process class by PowerShell. While it appears in the output, the property cannot be used to define a filter, nor can Path be selected using the Property parameter of either Get-CimInstance or Get-WmiObject.

Get-Member shows that it is a ScriptProperty, as follows:

Get-CimInstance Win32_Process -Filter "ProcessId=$pid" |
    Get-Member -Name Path
Get-WmiObject Win32_Process -Filter "ProcessId=$pid" |
    Get-Member -Name Path

WQL defines two wildcard characters that can be used with string queries:

  • The % (percentage) character matches any number of characters and is equivalent to using * in a filesystem path or with the -like operator.
  • The _ (underscore) character matches a single character and is equivalent to using ? in a filesystem path or with the -like operator.

The following query filters the results of Win32_Service, including services with paths starting with a single drive letter and ending with .exe:

Get-CimInstance Win32_Service -Filter 'PathName LIKE "_:\%.exe"' 

Logic operators

Logic operators can be used with the Filter and Query parameters.

The examples in the following table are based on the following command:

Get-CimInstance Win32_Process -Filter "<Filter>"

Description

Operator

Syntax

Example

Logical and

AND

<Condition1> AND <Condition2>

ProcessID=$pid AND Name='powershell.exe'

Logical or

OR

<Condition1> OR <Condition2>

ProcessID=$pid OR ProcessID=0

Logical not

NOT

NOT <Condition>

NOT ProcessID=$pid

Table 11.1: WQL logical operators

Comparison operators

Comparison operators may be used with the Filter and Query parameters.

The examples in the following table are based on the following command:

Get-CimInstance Win32_Process -Filter "<Filter>"

Description

Operator

Example

Equal to

=

Name='powershell.exe' AND ProcessId=0

Not equal to

<>

Name<>'powershell.exe'

Greater than

>

WorkingSetSize>$(100MB)

Greater than or equal to

>=

WorkingSetSize>=$(100MB)

Less than

<

WorkingSetSize<$(100MB)

Less than or equal to

<=

WorkingSetSize<=$(100MB)

Is

IS

CommandLine IS NULL

CommandLine IS NOT NULL

Like

LIKE

CommandLine LIKE '%.exe'

Table 11.2: WQL comparison operators

Quoting values

When building a WQL query, string values must be quoted; numeric and Boolean values do not need quotes.

As the filter is also a string, this often means nesting quotes within one another. The following techniques may be used to avoid needing to use PowerShell's escape character.

For filters or queries containing fixed string values, use either of the following styles. Use single quotes outside and double quotes inside:

Get-CimInstance Win32_Process -Filter 'Name="pwsh.exe"' 

Alternatively, use double quotes outside and single quotes inside:

Get-CimInstance Win32_Process -Filter "Name='pwsh.exe'" 

For filters or queries containing PowerShell variables or sub-expressions, use double quotes around the filter. Variables within single-quoted strings will not expand:

Get-CimInstance Win32_Process -Filter "ProcessId=$PID" 
Get-CimInstance Win32_Process -Filter "ExecutablePath LIKE '$($pshome -replace '', '')%'" 

Regex recap

The regular expression '' represents a single literal '', as the backslash is normally the escape character. Each '' in the pshome path is replaced with '' to account for WQL using '' as an escape character as well.

In the previous example, the filter extends over a single line. If a filter is long, or contains several conditions, consider using the format operator to compose the filter string:

$params = @{
    ClassName = 'Win32_Process'
    Filter    = "ExecutablePath LIKE '{0}%'" -f @(
        $PSHOME -replace '', ''
    ) 
}
Get-CimInstance @params

The format operator is used to add the escaped version of the path to the string.

The preceding command will show any instances of PowerShell that are running; for example:

ProcessId Name     HandleCount WorkingSetSize VirtualSize
--------- ----     ----------- -------------- -----------
14868     pwsh.exe 1114        243924992      2204239736832
10952     pwsh.exe 1513        180326400      2204260892672

WQL filters are especially useful when working with CIM classes that contain many instances.

Associated classes

WMI classes often have several different associated or related classes; for example, each instance of Win32_Process has an associated class, CIM_DataFile.

Associations between two classes are expressed by a third class. In the case of Win32_Process and CIM_DataFile, the relationship is expressed by the CIM_ProcessExecutable class.

The relationship is defined by using the antecedent and dependent properties, as shown in the following example:

PS> Get-CimInstance CIM_ProcessExecutable |
>>     Where-Object Dependent -match $PID |
>>     Select-Object -First 1
Antecedent         : CIM_DataFile (Name = "C:WINDOWSSystem32Windo...)
Dependent          : Win32_Process (Handle = "11672")
BaseAddress        : 2340462460928
GlobalProcessCount : 
ModuleInstance     : 4000251904
ProcessCount       : 0
PSComputerName     : 

This CIM_ProcessExecutable class does not need to be used directly; it is only used to express the relationship between two other classes.

WMI object paths

A WMI path is required to find classes associated with an instance. The WMI object path uniquely identifies a specific instance of a WMI class.

The object path is made up of several components:

<Namespace>:<ClassName>.<KeyName>=<Value> 

The namespace can be omitted if the class is under the default namespace, rootcimv2.

You can discover the KeyName for a given WMI class in several ways. In the case of Win32_Process, the key name might be discovered by using any of the following methods.

It can be discovered by using the CIM cmdlets:

(Get-CimClass Win32_Process).CimClassProperties | 
    Where-Object { $_.Flags -band 'Key' } 

It can be discovered by using the Microsoft Docs website, which provides descriptions of each property (and method) exposed by the class: https://docs.microsoft.com/windows/win32/cimwin32prov/win32-process.

Having identified a key, only the value remains to be found. In the case of Win32_Process, key (handle) has the same value as the process ID. The object path for the Win32_Process instance associated with a running PowerShell console is, therefore, as follows:

rootcimv2:Win32_Process.Handle=$PID 

The namespace does not need to be included if it uses the default, rootcimv2; the object path can be shortened to the following:

Win32_Process.Handle=$PID 

Get-CimInstance and Get-WmiObject do not retrieve an instance from an object path, but the Wmi type accelerator can:

PS> [Wmi]"Win32_Process.Handle=$PID" | Select-Object Name, Handle
Name     Handle
----     ------
pwsh.exe 11672 

The preceding object is somewhat equivalent to using a filter for Handle when querying Win32_Process.

Using ASSOCIATORS OF

Queries using ASSOCIATORS OF are used to find instances of classes that are related to a specific object.

The ASSOCIATORS OF query may be used for any given object path; for example, using the preceding object path results in the following command:

Get-CimInstance -Query "ASSOCIATORS OF {Win32_Process.Handle=$PID}" 

This query will return objects from three different classes: Win32_LogonSession, Win32_ComputerSystem, and CIM_DataFile. The classes that are returned are shown in the following example:

PS> $params = @{
>>     Query = "ASSOCIATORS OF {Win32_Process.Handle=$PID}" 
>> }
PS> Get-CimInstance @params | Select-Object CimClass -Unique
CimClass
--------
root/cimv2:Win32_ComputerSystem
root/cimv2:Win32_LogonSession
root/cimv2:CIM_DataFile

The query can be refined to filter a specific resulting class; an example is as follows:

Get-CimInstance -Query "ASSOCIATORS OF {Win32_Process.Handle=$PID} WHERE ResultClass = CIM_DATAFILE" 

ResultClass values must not be quoted

The value in the ResultClass condition is deliberately not quoted.

The result of this operation is a long list of files that are used by the PowerShell process. A snippet of this is shown as follows:

PS> Get-CimInstance -Query "ASSOCIATORS OF {Win32_Process.Handle=$PID} WHERE ResultClass = CIM_DATAFILE" | 
>>     Select-Object Name
Name 
---- 
C:Program FilesPowerShell7pwsh.exe
C:WINDOWSSYSTEM32
tdll.dll
C:WINDOWSSystem32KERNEL32.DLL
C:WINDOWSSystem32KERNELBASE.dll
C:WINDOWSSystem32USER32.dll
C:WINDOWSSystem32win32u.dll 

While the older Get-WmiObject command has not been continued into PowerShell 6, the WMI type accelerators remain.

WMI Type Accelerators

The WMI cmdlets were removed in PowerShell 6 and are not going to be reinstated.

The following type accelerators remain and can still be used:

  • Wmi: System.Management.ManagementObject
  • WmiClass: System.Management.ManagementClass
  • WmiSearcher: System.Management.ManagementObjectSearcher

When necessary, these accelerators may be used to simulate the functionality provided by the older WMI cmdlets.

Both the Wmi and WmiClass type accelerators can be written to use a remote computer by including the computer name. An example is as follows:

[Wmi]"\RemoteComputer
ootcimv2:Win32_Process.Handle=$PID" 
[WmiClass]"\RemoteComputer
ootcimv2:Win32_Process" 

You can use these classes in PowerShell 6 and higher.

Getting instances

You can use the type accelerator WmiSearcher to execute queries and retrieve results:

([WmiSearcher]"SELECT * FROM Win32_Process").Get() 

The returned object is identical to the object that would have been returned by the Get-WmiObject command.

Working with dates

WMI instances retrieved using type accelerators do not convert date-time properties into the DateTime type. Querying the Win32_Process class for the creation date of a process returns the date-time property as a long string:

PS> $query = '
>> SELECT Name, CreationDate
>> FROM Win32_Process
>> WHERE ProcessId={0}
>> ' -f $PID
PS> ([WmiSearcher]$query).Get() | Select-Object Name, CreationDate
Name     CreationDate
----     ------------
pwsh.exe 20200510090416.263973+060

The .NET namespace, System.Management, includes a class called ManagementDateTimeConverter, dedicated to converting date and time formats found in WMI. This method is added to WMI objects in PowerShell as a ConvertToDateTime script method.

The string in the preceding example may be converted as follows:

$query = '
SELECT Name, CreationDate
FROM Win32_Process
WHERE ProcessId={0}
' -f $PID
([WmiSearcher]$query).Get() | Select-Object @(
    'Name'
    @{
        Name = 'CreationDate'
        Expression = {
            $_.ConvertToDateTime($_.CreationDate)
        }
    }
)

WMI classes may be used via the WmiClass accelerator.

Getting classes

An instance of a class may be created using the WmiClass accelerator, as the following code shows:

[WmiClass]'Win32_Process'

The class describes the methods and properties that can be used.

Calling methods

Calling a method on an existing instance of an object found using WmiSearcher is like using any other .NET method call.

The following example gets and restarts the "Print Spooler" service. The following operation requires administrative access:

$query = '
  SELECT *
  FROM Win32_SERVICE
  WHERE DisplayName="Print Spooler"
'
$service = ([WmiSearcher]$query).Get()
$service.StopService()     # Call the StopService method
$service.StartService()    # Call the StartService method

The WMI class can be used to find the details of a method; for example, the Create method of Win32_Share, as follows:

PS> ([WmiClass]'Win32_Share').Methods['Create']
Name          : Create
InParameters  : System.Management.ManagementBaseObject
OutParameters : System.Management.ManagementBaseObject
Origin        : Win32_Share
Qualifiers    : {Constructor, Implemented, MappingStrings, Static}

When the Invoke-CimMethod command accepts a Hashtable, methods invoked on a WMI object expect arguments to be passed in a specific order. The order is described in the documentation for the class, such as https://docs.microsoft.com/windows/win32/cimwin32prov/create-method-in-class-win32-share.

The documentation shows which arguments are mandatory (not optional) in the syntax element at the top.

Alternatively, the order arguments must be written like so:

PS> ([WmiClass]'Win32_Share').Create.OverloadDefinitions -split '(?<=,)'
System.Management.ManagementBaseObject Create(System.String Path,
 System.String Name,
 System.UInt32 Type,
 System.UInt32 MaximumAllowed,
 System.String Description,
 System.String Password,
 System.Management.ManagementObject#Win32_SecurityDescriptor Access)

To create a share, the argument list must contain an argument for Access, then Description, then MaximumAllowed, and so on. If the argument is optional, it can be ignored, or a null value may be provided:

([WmiClass]'Win32_Share').Create(
    'C:TempShare1', # Path 
    'Share2',         # Name 
    0                 # Type (Disk Drive) 
)

The Description argument follows the MaximumAllowed argument. If Description were to be set (but not MaximumAllowed), a null value can be added for that argument:

([WmiClass]'Win32_Share').Create(
    'C:TempShare1', # Path 
    'Share3',         # Name 
    0,                # Type (Disk Drive),
    $null,            # MaximumAllowed
    'Description'     # Description
)

ReturnValue describes the result of the operation; a ReturnValue of 0 indicates success. As this operation requires administrator privileges (run as administrator), a ReturnValue of 2 is used to indicate that it was run without sufficient rights.

If the folder used in the previous example does not exist, ReturnValue will be set to 24.

A less well-known alternative is available compared to passing arguments in an array. You can pass arguments by setting the values of an object. The object is retrieved using the GetMethodParameters method on a WMI class:

$class = [WmiClass]'Win32_Share'
$params = $class.GetMethodParameters('Create')
$params.Name = 'Share1'
$params.Path = 'C:TempShare1'
$params.Type = 0
$class.InvokeMethod('Create', $params)

Creating an object to represent the parameters has a clear advantage in that each property has a clear name, rather than being reliant on discovering and using positional arguments.

Creating instances

An instance of a WMI class can be created using the CreateInstance method of the class. The following example creates an instance of Win32_Trustee:

([WmiClass]'Win32_Trustee').CreateInstance() 

Associated classes

Objects returned by WmiSearcher have a GetRelated method that can be used to find associated instances.

The GetRelated method accepts arguments that can be used to filter the results. The first argument, relatedClass, is used to limit the instances that are returned to specific classes, as shown here:

([WmiSearcher]'SELECT * FROM Win32_LogonSession').Get() | ForEach-Object {
    [PSCustomObject]@{
        LogonName      = $_.GetRelated('Win32_Account').Caption
        SessionStarted = [System.Management.ManagementDateTimeConverter]::ToDateTime(
            $_.StartTime
        )
    }
}

Permissions

Working with permissions in WMI is more difficult than in .NET as the values in use are not given friendly names. However, the .NET classes can still be used, even if not quite as intended.

The following working examples demonstrate configuring the permissions.

Sharing permissions

Get-Acl and Set-Acl are fantastic tools for working with filesystem permissions, or permissions under other providers. However, these commands cannot be used to affect SMB share permissions.

The SmbShare module

The SmbShare module has commands that affect share permissions. This example uses the older WMI classes to modify permissions. It might be used if the SmbShare module cannot be.

The Get-SmbShareAccess command might be used to verify the outcome of this example.

The following operations require administrative privileges; use PowerShell as an administrator if you're attempting to use these examples.

Creating a shared directory

The following snippet creates a directory and shares that directory:

$path = 'C:TempWmiPermissions' 
New-Item $path -ItemType Directory
$params = @{
    ClassName = 'Win32_Share'
    MethodName = 'Create'
    Arguments = @{
        Name = 'WmiPerms'
        Path = $path
        Type = [UInt32]0
    }
}
Invoke-CimMethod @params 

The Create method used here will fail if the argument for Type is not correctly defined as a UInt32 value. PowerShell will set the type to Int32 if the value of 0 is used without the cast.

The requirement for UInt32, in this case, may be viewed by exploring the parameters required for the method:

PS> (Get-CimClass Win32_Share).CimClassMethods['Create'].Parameters |
>>     Where-Object Name -eq Type
Name     CimType     Qualifiers               ReferenceClassName
----     -------     ----------               ------------------
Type     UInt32     {ID, In, MappingStrings}

Now that the share exists, you can retrieve the security descriptor assigned to the share.

Getting a security descriptor

When Get-Acl is used, the object that it gets is a security descriptor. The security descriptor includes a set of control information (ownership and so on), along with the discretionary and system access control lists.

The WMI class Win32_LogicalShareSecuritySetting is used to represent the security for each of the shares on a computer:

$params = @{
    ClassName = 'Win32_LogicalShareSecuritySetting'
    Filter    = "Name='WmiPerms'"
}
$security = Get-CimInstance @params

The returned object has a limited number of properties:

Caption        : Security settings of WmiPerms
Description    : Security settings of WmiPerms
SettingID      :
ControlFlags   : 32772
Name           : WmiPerms
PSComputerName :

This instance is important as it is required to use the GetSecurityDescriptor method:

$return = $security | Invoke-CimMethod -MethodName GetSecurityDescriptor 
$aclObject = $return.Descriptor 

The security descriptor held in the aclObject variable is different from the result returned by Get-Acl:

PS> $aclObject
ControlFlags   : 32772
DACL           : {Win32_ACE}
Group          : 
Owner          : 
SACL           : 
TIME_CREATED   : 
PSComputerName : 

The ControlFlags value states that a DACL is present, and that the security descriptor information is stored in a contiguous block of memory. These values are described in Microsoft Docs: https://docs.microsoft.com/windows/win32/secauthz/security-descriptor-control.

The DACL, or discretionary access control list, is used to describe the permission levels for each security principal (a user, group, or computer account). Each entry in this list is an instance of Win32_ACE:

PS> $aclObject.DACL
AccessMask              : 1179817
AceFlags                : 0
AceType                 : 0
GuidInheritedObjectType : 
GuidObjectType          : 
TIME_CREATED            : 
Trustee                 : Win32_Trustee
PSComputerName          : 

The Win32_ACE object has a Trustee property that holds the Name, Domain, and SID properties of the security principal (in this case, the Everyone principal):

PS> $aclObject.DACL.Trustee
Domain         : 
Name           : Everyone
SID            : {1, 1, 0, 0...}
SidLength      : 12
SIDString      : S-1-1-0
TIME_CREATED   : 
PSComputerName : 

AceFlags describes how an ACE is to be inherited. As this is a share, the AceFlags property will always be 0. Nothing can, or will, inherit this entry; .NET can be used to confirm this:

PS> [System.Security.AccessControl.AceFlags]0
None

AceType is either AccessAllowed (0) or AccessDenied (1). Again, .NET can be used to confirm this:

PS> [System.Security.AccessControl.AceType]0
AccessAllowed 

Finally, the AccessMask property can be converted into a meaningful value with .NET as well. The access rights that can be granted on a share are a subset of those that might be assigned to a file or directory:

PS> [System.Security.AccessControl.FileSystemRights]1179817
ReadAndExecute, Synchronize 

Putting this together, the entries in a shared DACL can be made much easier to understand:

using namespace System.Security.AccessControl 
 
$aclObject.DACL | ForEach-Object { 
    [PSCustomObject]@{ 
        Rights   = [FileSystemRights]$_.AccessMask 
        Type     = [AceType]$_.AceType 
        Flags    = [AceFlags]$_.AceFlags 
        Identity = $_.Trustee.Name 
    } 
} 

In the preceding example, the domain of the trustee is ignored. If the trustee is something other than Everyone, the domain should be included.

Adding an access control entry

To add an Access Control Entry (ACE) to an existing list, you must create a Win32_ACE. Creating an ACE requires a Win32_Trustee. The following trustee is created from the current user:

$trustee = New-CimInstance (Get-CimClass Win32_Trustee) -ClientOnly 
$trustee.Domain = $env:USERDOMAIN 
$trustee.Name = $env:USERNAME 

SID does not need to be set on the trustee object, but if the security principal is invalid, attempting to apply the change to security will fail.

Then, you can create Win32_ACE. The following ACE grants full control of the share to trustee:

using namespace System.Security.AccessControl 
$ace = New-CimInstance (Get-CimClass Win32_ACE) -ClientOnly 
$ace.AccessMask = [UInt32][FileSystemRights]'FullControl' 
$ace.AceType = [UInt32][AceType]'AccessAllowed' 
$ace.AceFlags = [UInt32]0 
$ace.Trustee = $trustee 

The ACE can be added to the DACL using the += operator:

$aclObject.DACL += $ace 

Setting the security descriptor

Once the ACL has been changed, the modified security descriptor must be set. The instance returned by Win32_LogicalShareSecuritySetting contains a SetSecurityDescriptor method:

$params = @{
    MethodName = 'SetSecurityDescriptor'
    Arguments  = @{
        Descriptor = $aclObject
    }
}
$security | Invoke-CimMethod @params 

A return value of 0 indicates that the change to the ACL has been successfully applied. You can view this change by looking at the properties of the share in File Explorer, as shown in the following example:

Figure 11.1: Modified share permissions

In early versions of PowerShell (and earlier versions of Windows), WMI was the only option for changing configurations like share permissions. The SmbShare module is an easier-to-use set of commands than the convoluted process used here. It is also able to show the assigned share permissions:

PS> Get-SmbShareAccess -Name WmiPerms
Name      ScopeName  AccountName  AccessControlType  AccessRight
----      ---------  -----------  -----------------  -----------
WmiPerms  *          Everyone     Allow              Read
WmiPerms  *          TITANChris  Allow              Full

While share permissions have better commands available, the preceding process provides a good basis for modifying any security presented via WMI. One of these is permissions defined on namespaces in WMI.

WMI permissions

Getting and setting WMI security in PowerShell uses the same approach as share security. WMI permissions might be set using wmimgmt.msc if the GUI is used. The content of the DACL differs slightly.

The __SystemSecurity class is used to access the security descriptor. Each WMI namespace has its own instance of the __SystemSecurity class; an example is as follows:

Get-CimClass __SystemSecurity -Namespace root 
Get-CimClass __SystemSecurity -Namespace rootcimv2 

Getting a security descriptor

The security descriptor for a given namespace can be retrieved from the __SystemSecurity class. By default, administrator privileges are required to get the security descriptor:

$security = Get-CimInstance __SystemSecurity -Namespace rootcimv2 
$return = $security | Invoke-CimMethod -MethodName GetSecurityDescriptor 
$aclObject = $return.Descriptor 

The access mask

The access mask defines which rights are assigned by the ACE. The values of an access mask in the DACL are documented as access right constants in Microsoft Docs: https://docs.microsoft.com/windows/win32/wmisdk/namespace-access-rights-constants.

The standard access rights, ReadSecurity and WriteSecurity, are also relevant. The access mask is a composite of the values listed here:

  • EnableAccount: 1
  • ExecuteMethods: 2
  • FullWrite: 4
  • PartialWrite: 8
  • WriteProvider: 16
  • RemoteEnable: 32
  • ReadSecurity: 131072
  • WriteSecurity: 262144

You can use these values to add a new ACE and set the security descriptor in the same way, just like when setting permissions on a share in the previous section.

The WMI methods used in this subsection also allow a security descriptor to be exported to and imported from a Security Descriptor Definition Language (SDDL) string.

WMI and SDDL

SDDL is used to describe the content of a security descriptor as a string. The string format is described in Microsoft Docs: https://docs.microsoft.com/windows/win32/secauthz/security-descriptor-string-format.

A security descriptor returned by Get-Acl has a method that can convert the entire security descriptor into a string, as follows:

PS> (Get-Acl C:).GetSecurityDescriptorSddlForm('All')
O:S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464G:S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464D:PAI(A;;LC;;;AU)(A;OICIIO;SDGXGWGR;;;AU)(A;;FA;
;;SY)(A;OICIIO;GA;;;SY)(A;OICIIO;GA;;;BA)(A;;FA;;;BA)(A;OICI;0x1200a9;;;BU) 

A security descriptor defined using SDDL can also be imported. If the sddlString variable is assumed to hold a valid security descriptor, then you can use the following command:

$acl = Get-Acl C: 
$acl.SetSecurityDescriptorSddlForm($sddlString) 

The imported security descriptor will not apply to the directory until Set-Acl is used.

WMI security descriptors can be converted to and from different formats, including SDDL. WMI has a specialized class for this: Win32_SecurityDescriptorHelper. The methods for this class are shown here:

PS> (Get-CimClass Win32_SecurityDescriptorHelper).CimClassMethods
Name              ReturnType  Parameters              Qualifiers
----              ----------  ----------              ----------
Win32SDToSDDL         UInt32  {Descriptor, SDDL}      {implemented, static}
Win32SDToBinarySD     UInt32  {Descriptor, BinarySD}  {implemented, static}
SDDLToWin32SD         UInt32  {SDDL, Descriptor}      {implemented, static}
SDDLToBinarySD        UInt32  {SDDL, BinarySD}        {implemented, static}
BinarySDToWin32SD     UInt32  {BinarySD, Descriptor}  {implemented, static}
BinarySDToSDDL        UInt32  {BinarySD, SDDL}        {implemented, static}

You might want to convert a WMI security descriptor into SDDL to create a backup before it makes a change, as follows:

$security = Get-CimInstance __SystemSecurity -Namespace rootcimv2 
$return = $security | Invoke-CimMethod -MethodName GetSecurityDescriptor 
$aclObject = $return.Descriptor 
$params = @{
    ClassName = 'Win32_SecurityDescriptorHelper'
    MethodName = 'Win32SDToSDDL'
    Arguments  = @{
        Descriptor = $aclObject
    }
}
$return = Invoke-CimMethod @params

If the operation succeeds (that is, if ReturnValue is 0), the security descriptor in SDDL form will be available:

PS> $return.SDDL
O:BAG:BAD:AR(A;CI;CCDCWP;;;S-1-5-21-2114566378-1333126016-908539190-1001)(A;CI;CCDCLCSWRPWPRCWD;;;BA)(A;CI;CCDCRP;;;NS)(A;CI;CCDCRP;;;LS)(A;CI;CCDCRP;;;AU) 

You can import a security descriptor expressed as an SDDL string:

$params = @{
    ClassName = 'Win32_SecurityDescriptorHelper'
    MethodName = 'SDDLToWin32SD'
    Arguments  = @{
        SDDL = 'O:BAG:BAD:AR(A;CI;CCDCWP;;;S-1-5-21-2114566378-1333126016-908539190-1001)(A;CI;CCDCLCSWRPWPRCWD;;;BA)(A;CI;CCDCRP;;;NS)(A;CI;CCDCRP;;;LS)(A;CI;CCDCRP;;;AU)'
    }
}
$return = Invoke-CimMethod @params
$aclObject = $return.Descriptor

If ReturnValue is 0, the aclObject variable will contain the imported security descriptor:

PS> $aclObject
ControlFlags   : 33028
DACL           : {Win32_ACE, Win32_ACE, Win32_ACE, Win32_ACE...}
Group          : Win32_Trustee
Owner          : Win32_Trustee
SACL           : 
TIME_CREATED   : 
PSComputerName : 

The ACL object, like much of WMI, is not particularly descriptive. The raw information is there, and the descriptor can be set. Reading the descriptor requires work, as you must translate all the values into human-readable equivalents.

Summary

This chapter explored working with WMI classes, the different available commands, and the WMI Query Language. You used CIM cmdlets as a means of working with WMI.

Since Get-WmiObject has been removed from PowerShell, the WMI type accelerators were explored as an alternative means of working with WMI. This may be useful for the few rare classes that do not work using the CIM cmdlets. You used getting and setting permissions with WMI while using shared security and WMI security as examples.

Chapter 12, Working with HTML, XML, and JSON, will explore working with and generating and consuming data from a variety of different text-based formats.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.175.180