WMI part 3 - Remote Queries
One of the principles of WMI is that everything that can be queried or set locally should be able to be done remotely.
WMI makes no distinction between local and remote access ... The difference between a local and a remote connection is that users can specify a user name and password in a remote connection, replacing the current user name and password. With a local connection, users cannot override the current name and password."
Connecting to WMI on a remote computer
With
the odd exception this is true: any WMI query can be run remotely, including event notification queries.
To initiate a remote WMI connection (assuming all required services are correctly configured) you need to have administrative rights on the local computer. Also, you have to be using (or be able to specify the credentials of) an account with the rights to connect to the target computer and which has have the Remote Enable access permission on the WMI namespace you want to connect to. (For computers not in a domain, the account should have the same username and password on each computer). This access right is given by default to the Administrators group only, so effectively administrative rights are needed on the target computer. This makes the security model for remote access very simple - if you can initiate a connection, you get access to everything you would normally have access to, and as you're an admin that's everything anyway.
This sample class illustrates local and remote connections. It should work in VFP6 or later. Leave username, domain and password blank to connect with the current user context: from a security point of view, this is preferable as ConnectServer sends the password across the network as plain text unless your network uses Kerberos authentication.
WMI connection class
** Example usage: query a remote PC for the startup commands buried in the registry
oWMIConnection = createobject("WMIConnect")
oWMIConnection.cDomain = "domain"
oWMIConnection.cUsername = "remoteadmin"
* Computer can be specified by IP address or DNS name.
oWMIConnection.cComputer = "target"
oWMIConnection.cPassword = "itsasecret"
oWMI = oWMIConnection.GetConnection()
if vartype(oWMI) = "O"
oCommands = oWMI.ExecQuery("Select * from WIN32_StartupCommand")
? transform(oCommands.Count) + " startup commands found"
endif
** WMI connection class
define class WMIConnect as relation
** User/domain settings
cDomain = ""
cUserName = ""
cPassword = ""
** Target computer.
cComputer = ""
** WMI settings
cNameSpace = "root/cimv2"
* Without this flag, a connection attempt would wait indefinitely.
* nFlags = 128 && XP or better only..
nFlags = 0
* Find your Locale ID here:
* http://www.microsoft.com/globaldev/reference/win2k/setup/lcid.mspx
cLocale = "MS_409" && EN-US
** Security settings.
nImpersonationLevel = 3 && wbemImpersonationLevelImpersonate
nAuthenticationLevel = 0 && wbemAuthenticationLevelDefault
* If .T., load all privileges before connection.
lLoadPrivileges = .f.
procedure GetConnection
local cUserName, cPassword, lRemote, oWMILocator, ;
oPrivileges, oWMIServices, nPrivilegeID
if empty(this.cComputer)
* Use "." for a local connection
this.cComputer = "."
endif
if this.cComputer = "."
* Cannot use credentials to connect locally
* even if they are the ones currently in use.
cUserName = ""
cPassword = ""
lRemote = .f.
else
cUserName = iif(not empty(this.cDomain), this.cDomain + "\", "") + this.cUserName
cPassword = this.cPassword
lRemote = .t.
endif
oWMILocator = createobject("WbemScripting.SWbemLocator")
if lRemote
* Apply settings for DCOM:
oWMILocator.Security_.ImpersonationLevel = this.nImpersonationLevel
oWMILocator.Security_.AuthenticationLevel = this.nAuthenticationLevel
else
* Privileges only need to be applied for on local system
* but must be requested *before* connection on Windows 9x/NT
if this.lLoadPrivileges
for nPrivilegeID = 1 to 27
oWMILocator.Security_.Privileges.add(nPrivilegeID)
next
endif
endif
cAuthority = "" && for kerberos, see PSDK
* Connect as specified...
oWMIServices = oWMILocator.ConnectServer ;
(this.cComputer, this.cNameSpace, cUserName, cPassword, ;
this.cLocale, cAuthority, this.nFlags)
* ... and return the WMI service object.
return oWMIServices
endproc
procedure error(nError, cMethod, nLine)
* Sample error code
* This needs to be tested across versions to make sure
* same errors are raised.
local arrErr[1]
aerror(arrErr)
display memory like arrErr
if nError = 1429 or nError = 1427
nError = arrErr(7)
else
messagebox("Error " + transform(nError) + " has occurred")
* Oops..
endif
do case
case nError = 4110
? "Invalid namespace"
case nError = 4099
? "Valid credentials, but not allowed to connect"
case nError = 5
? "Invalid credentials"
case nError = 1722
? "Remote computer not found"
endcase
endproc
enddefine
Impersonation and Authentication
The
Impersonation Level specifies whether your security credentials are available to WMI. Of the four options, the documentation says of two that "calls to WMI may fail with this impersonation level" and the fourth "may constitute an unnecessary security risk." This leaves the value "impersonate", and this is the default in Windows 2000 or later - or, to be completely accurate, the value in the registry key 'HKLM\Software\Microsoft\wbem\Scripting\Default Impersonation Level' is the default. This is why impersonationLevel is set to impersonate in all WMI scripts designed for potential remote use.
The
Authentication Level determines how secure the connection is. This is constrained by the target operating system: Windows XP won't accept a connection less than 'packet', Windows 9x can't accept connections greater than 'connect'. Fortunately, using the default setting (wbemAuthenticationLevelDefault) allows DCOM to negotiate the appropriate security level.
There are a number of complications when connecting between different versions of Windows, and sometimes specific impersonation and authentication levels need to be set - see
this document for the details. Generally, going downstream - from newer to older - is easier than upstream; crossing two or more generations of Windows either way can be problematic and is sometimes impossible.
Optimising the performance of remote queries
Add the WBEM_FLAG_FORWARD_ONLY flag to get one-time forward-enumeration objects: the returned collection object does not populate it's Count property (or support the Clone_ method) and is released as soon as it has been enumerated.
If practical, specify a specific field list rather than * in the SELECT statement.
Don't use moniker syntax even if you are connecting with the current user account.
Use as specific a WHERE clause as possible.
Setting WMI security with the Windows WMI Control
The WMI access permissions are set via the WMI control, which is hidden in the Services and Applications section of Windows 2000/XP Computer Management [or via start/run/wmimgmt.msc]. To access it, right-click on "WMI Control" and choose Properties. Here you can check the default WMI namespace, activate some very verbose logging, and back up the WMI repository. On the security tab is a familiar Windows Access Control Entries (ACE) dialog which enables allowing or denying specific permissions to each namespace on your computer. Here you can disable remote access to WMI namespaces, if you're paranoid but don't want to stop the WMI service altogether.
Alternatively, in some circumstances you could here give an account permission to make a remote connection (and perform some actions) without giving it administrative rights as such, on the general principle it's best to initiate connections with as few rights as possible in order to accomplish the task at hand. (The account will also need DCOM remote connect rights, in Windows 2000/XP these are set with the other DCOM settings in the Component Services manager which can be started from the Administrative Tools menu: right click on the "My Computer" entry in the "Computers" tree and choose "Properties" to access them. Use dcomcnfg.exe for
NT and
Windows 9x.
Troubleshooting
Links
Notes / command snippets
netsh firewall set service type=remoteadmin mode=enable scope=all profile=all