I like to take on bites a little larger than I can chew to stretch myself. Well, this one was quite a step up from my simple WMI Query post. It started out with this article pertaining specifically to WMI remote connectivity itself:
http://msdn.microsoft.com/en-us/library/aa389286(VS.85).aspx
After trying that for a few days, I decided to search around some more and realized that WMI, in the context of VBScript or C++, was not really what I needed to be looking at. Nonetheless, it brought up the most common error folks seemed to run into: the inability to access the remote machine with ASP.NET.
Preparing your machines for Remote WMI Communication
COM security posed the biggest issue in all of the articles I read at first as well as in my own testing. When I modified my ASP.NET page to point to the remote machine I was getting this error.
After I actually slowed down long enough to read the article listed above, getting past the connection issue boiled down to two CLI entries on 1 machine and a single CLI entry on another. As was noted in the article I needed to enable the RemoteAdmin service machine B (see the graphic in the article). I then needed to permit 1) a single TCP port on my client machine and 2) a WMI application to handle the return data.
To enable the RemoteAdmin on Machine B I did what they said:
- Logon on to the machine
- Start the cmd.exe
- Switch to the netshell using netsh
C:\>netsh [Enter]
- Open the firewall context
Netsh>firewall [Enter]
- Within the firewall context, type this command:
Netsh firewall>
netsh firewall set service RemoteAdmin enable [Enter]Ok.
- To verify the entry I checked within the netsh context again by typing:
Netsh firewall>show service
Service configuration for Domain profile:
Mode Customized Name
Enable No Remote Desktop
Service configuration for Standard profile:
Mode Customized Name
Enable No Remote Administration
To verify this was actually enabled I then looked in the Group Policy editor.
- Start | Run | gpedit.msc
- Open the following node:
- Within that property panel double click the:
On a side note, Windows Home versions (and other low-end OS flavors) will not have this .msc snap-in. You can accomplish the same thing shown below for the GUI with CLI commands shown above. It just takes a good bit more knowledge to address it via the CLI. If you've never looked at the Explanation for RemoteAdmin in the gpedit.msc panel, it might help clear a few things up.
Allows remote administration of this computer using administrative tools such as the Microsoft Management Console (MMC) and Windows Management Instrumentation (WMI). To do this, Windows Firewall opens TCP ports 135 and 445. Services typically use these ports to communicate using remote procedure calls (RPC) and Distributed Component Object Model (DCOM). This policy setting also allows SVCHOST.EXE and LSASS.EXE to receive unsolicited incoming messages and allows hosted services to open additional dynamically-assigned ports, typically in the range of 1024 to 1034.
If you enable this policy setting, Windows Firewall allows the computer to receive the unsolicited incoming messages associated with remote administration. You must specify the IP addresses or subnets from which these incoming messages are allowed.
If you disable or do not configure this policy setting, Windows Firewall does not open TCP port 135 or 445. Also, Windows Firewall prevents SVCHOST.EXE and LSASS.EXE from receiving unsolicited incoming messages, and prevents hosted services from opening additional dynamically-assigned ports. Because disabling this policy setting does not block TCP port 445, it does not conflict with the "Windows Firewall: Allow file and printer sharing exception" policy setting.
Note: Malicious users often attempt to attack networks and computers using RPC and DCOM. We recommend that you contact the manufacturers of your critical programs to determine if they are hosted by SVCHOST.exe or LSASS.exe or if they require RPC and DCOM communication. If they do not, then do not enable this policy setting.
Note: If any policy setting opens TCP port 445, Windows Firewall allows inbound ICMP echo request messages (the message sent by the Ping utility), even if the "Windows Firewall: Allow ICMP exceptions" policy setting would block them. Policy settings that can open TCP port 445 include "Windows Firewall: Allow file and printer sharing exception," "Windows Firewall: Allow remote administration exception," and "Windows Firewall: Define port exceptions."
So, machine B was good to go. Now, it's time to set up out ASP.NET/WMI-client machine (Machine A). These were essentially similar commands, but, we have to open up a port and specify an "allowedprogram" entry to handle the WMI request/response data so that the asp page can handle the information.
On machine A (the one I've got ASP.NET pages on this box), I have to perform these steps:
- Logon on to the machine
- Start the cmd.exe
- Switch to the netshell using netsh
C:\>netsh [Enter]
- Open the firewall context
Netsh>firewall [Enter]
- Within the firewall context, type this command:
netsh firewall>add portopening protocol = TCP port = 135 name = dcom_tcp_135 [Enter]
Ok.
- To verify the entry I checked within the netsh context again by typing:
netsh firewall>show portopening
Port configuration for Domain profile:
Port Protocol Mode Name
-------------------------------------------------------------------
135 TCP Enable dcom_tcp_135
139 TCP Enable NetBIOS Session Service
445 TCP Enable SMB over TCP
137 UDP Enable NetBIOS Name Service
138 UDP Enable NetBIOS Datagram Service
3389 TCP Enable Remote Desktop
Port configuration for Standard profile:
Port Protocol Mode Name
-------------------------------------------------------------------
139 TCP Enable NetBIOS Session Service
445 TCP Enable SMB over TCP
137 UDP Enable NetBIOS Name Service
138 UDP Enable NetBIOS Datagram Service
3389 TCP Enable Remote Desktop
Now that the TCP port 135 is open, I need to permit remote access to the unsecapp.exe in the wbem directory for handling incoming calls from Machine B. To accomplish this I perform the following steps. (Assume that wmic is still open from the previous steps.)
- Netsh firewall>add allowedprogram name = UNSECAPP program = %windir%\system32\wbem\unsecapp.exe [Enter]
Ok.
After this has been added you can verify the entry:
netsh firewall>show allowedprogram [Enter]
Allowed programs configuration for Domain profile:
Mode Name / Program
-------------------------------------------------------------------
Enable Network Diagnostics for Windows XP / C:\WINDOWS\Network Diagnostic\xpnetdiag.exe
Enable Remote Assistance / C:\WINDOWS\system32\sessmgr.exe
Enable UNSECAPP / C:\WINDOWS\system32\wbem\unsecapp.exe
Enable Microsoft Management Console / C:\WINDOWS\system32\mmc.exe
Allowed programs configuration for Standard profile:
Mode Name / Program
-------------------------------------------------------------------
Enable Network Diagnostics for Windows XP / C:\WINDOWS\Network Diagnostic\xpnetdiag.exe
Enable Remote Assistance / C:\WINDOWS\system32\sessmgr.exe
We now have one machine (Machine A) that sends a sync request over TCP port 135 to another (Machine B) which then responds back with some information (handled by the unsecapp.exe). To ensure we've really got a working setup, let's test. First, a local machine info poll:
Next, the same poll against the remote machine:
Disabling Remote Access for WMI
As always, security was not far from my thought process. if you're testing, like I was, you may want to disable the changes just made via netsh. This can easily be done, provided these changes will not cause other problems. To reverse these edits on:
Machine A: switch to the netsh firewall context and perform the following steps:
- Netsh firewall> delete portopening protocol = TCP port = 135 [Enter]
- Netsh firewall>delete allowedprogram %windir%\system32\wbem\unsecapp.exe [Enter]
To verify these guys have been removed you can use the show command. Notice port 135 is not displayed in the domain or local machine (Standard) profiles.
netsh firewall>show portopening [Enter]
This will display the following list of results
Port configuration for Domain profile:
Port Protocol Mode Name
-------------------------------------------------------------------
139 TCP Enable NetBIOS Session Service
445 TCP Enable SMB over TCP
137 UDP Enable NetBIOS Name Service
138 UDP Enable NetBIOS Datagram Service
3389 TCP Enable Remote Desktop
Port configuration for Standard profile:
Port Protocol Mode Name
-------------------------------------------------------------------
139 TCP Enable NetBIOS Session Service
445 TCP Enable SMB over TCP
137 UDP Enable NetBIOS Name Service
138 UDP Enable NetBIOS Datagram Service
3389 TCP Enable Remote Desktop
Next, check the alloweprograms by typing this command:
netsh firewall>show allowedprogram [Enter]
This will display the following list of results. Again, note the absence of the UNSECAPP entry.
netsh firewall>show allowedprogram
Allowed programs configuration for Domain profile:
Mode Name / Program
-------------------------------------------------------------------
Enable Network Diagnostics for Windows XP / C:\WINDOWS\Network Diagnostic\xpnetdiag.exe
Enable Remote Assistance / C:\WINDOWS\system32\sessmgr.exe
Allowed programs configuration for Standard profile:
Mode Name / Program
-------------------------------------------------------------------
Enable Network Diagnostics for Windows XP / C:\WINDOWS\Network Diagnostic\xpnetdiag.exe
Enable Remote Assistance / C:\WINDOWS\system32\sessmgr.exe
Last, switch to machine B and remove the entry by typing this command
netsh firewall>set service remoteconfig disable [Enter]
Ok.
At this point we are ready to do some actual WMI calls.
Coding for Remote WMI on ASP.NET
Now that all that is out of the way, let's look at the actual code needed for an ASP.NET page to show remote WMI info. This turned out to be quite a search for information. First, I had to figure out how to get access to my remote box. Using WMIC I was able to reach my remote box with this command to verify I could connect and get baseline data:
Wmic\root>/node:remotemachine /user:admin /password:adminPassword os
This simple command would get me the info I needed perfectly. So, I decided, I'd start there. To get information from a remote machine, I would have to point to it. I added the machine name, the NetBIOS name works fine, to the code with this line:
string sServerPath = @"\\remoteMachine\root\cimv2";
Alright, we were now pointing to the .mof file over there. Next, I needed to be able to authenticate against the remote machine using a valid ID. Here's where I started having a hard time. At first, I thought simply adding a few parameters to the ConnectionOptions object would get me there. I was confused because I was able to run my script and apparently authenticate against the remote machine without any errors, but, the code would show me my local running processes. "Why in the world," I wondered, "am I seeing my local processes when I am clearly pointing to another machine?" The page worked fine, which told me I was on the right track. Only, it would not show me what I wanted, remote machine data.
Off to the MSDN and ASP.NET forums I went. If you want to cut the story short, you can read these and get to the end of the chance if you like, but, for the gory details, keep reading.
- http://forums.asp.net/t/1464037.aspx
- http://social.msdn.microsoft.com/Forums/en-US/ITCG/thread/d06ccb35-7b8d-4e86-9c80-2c8718734c67
I discovered there were no clear answers here and, in fact, the Microsoft documentation has some errors. More about that in a moment. I dug around Google and the MSDN documentation long enough to realize I needed to include an Authority against which the login could perform validation. When I looked into the MSDN notes I found this link:
- http://msdn.microsoft.com/en-us/library/ms257337.aspx - How To: Connect to a Remote Computer
- http://msdn.microsoft.com/en-us/library/system.management.connectionoptions.authority.aspx- ConnectionOptions..::.Authority Property
While digging around the documentation I found this note:
If the property [ConnectionOptions.Authority] is null, NTLM authentication will be used and the NTLM domain of the current user will be used."
In my app I was not specifying the property, so, it was in fact null, therefore, it pointed to my local machine. So, my original question about why I was seeing my own processes was answered. But, it raised a new question: which protocol should I use and what's the syntax? I tried both suggestions without success:
- Kerberos:<principal name>, example "Kerberos:myTestBox"
- NTLMDOMAIN: <domain_name>, example "NTLMDOMAIN:myTestBox"
So, the question shifted: which authorization schema would I use to connect FROM a domain-based XP box TO a workgroup-based Vista box? Kerberos or NTLMDOMAIN? Reading a little further and taking one of the comments that made sense, I pretty quickly concluded that the NTLMDOMAIN made the most sense. But, it got more interesting as I started digging into it. Here's essentially how it went:
- I realized there was a discrepancy in the link about the connectionoptions.Authority syntax. The C# sample code says to use ntDlmdomain. I searched some more for this to clarify, but, it was all over Google. In the end, it ended up being NTLMDOMAIN.
// Build an options object for the remote connection
// if you plan to connect to the remote
// computer with a different user name
// and password than the one you are currently using.
// This example uses the default values.
ConnectionOptions options =
new ConnectionOptions();options.Authority = "ntdlmdomain:DOMAIN";
// Make a connection to a remote computer.
// Replace the "FullComputerName" section of the
// string "\\\\FullComputerName\\root\\cimv2" with
// the full computer name or IP address of the
// remote computer.
ManagementScope scope =
new ManagementScope(
"\\\\FullComputerName\\root\\cimv2", options);scope.Connect();
Putting this info to use now made my connectionOptions follow this pattern:
conOptions.Authority = "NTLMDOMAIN:DOMAIN";
- Next, I had to pin down what the DOMAIN value was. Since the second machine was a test box, it was not on any domain. At first, I tried the machine name. Then, I tried the UNC path for the machine name. Finally, I realized, since it's a workgroup, the workgroup name would probably stand for a DOMAIN value. It does. Consequently, I needed to put the following to work:
conOptions.Authority = "NTLMDOMAIN:WORKGROUP";
- Alright, I was making progress. But, when I ran my code, I started getting weird errors. So, I undid and redid all my WMI settings with netsh firewall. Oddly, the WMIC was failing with authentication errors. I got frustrated and started searching pretty intensely and found some of the links referenced in my WMI reference post a few days ago. Armed with this information, I ran WMIDiagnostics and found a few errors, to no avail. Next, I looked at this the Let's troubleshoot WMI (Part 1: Remoting and Security) link and realized there were some good error code references. A little more digging found a much more comprehensive reference well worth bookmarking if you are doing anything in depth with WMI.
- By now, I had the following code in my ASP page and kept getting the same error over and over: invalid parameter.
ASP.NET Code
- using System;
- using System.Collections;
- using System.Configuration;
- using System.Data;
- using System.IO;
- using System.Linq;
- using System.Management;
- using System.Web;
- using System.Web.Security;
- using System.Web.UI;
- using System.Web.UI.HtmlControls;
- using System.Web.UI.WebControls;
- using System.Web.UI.WebControls.WebParts;
- using System.Xml.Linq;
- namespace WMIQueryTest
- {
- public partial class _Default : System.Web.UI.Page
- {
- protected void Page_Load(object sender, EventArgs e)
- {
- //Set your machine path
- //string sServerPath = @"\\" + Environment.MachineName + @"\root\cimv2";
- string sServerPath = @"\\RemoteMachine\root\cimv2";
- //Set the impersonation options and add your username/password - for testing only
- ConnectionOptions conOptions = new ConnectionOptions(null, "Administrator", "AdminPassword", @"ntdlmdomain:WORKGROUP", ImpersonationLevel.Impersonate, AuthenticationLevel.Call, true, null, new TimeSpan());
- //Create a ManagementScope object
- ManagementScope Scope = new ManagementScope(sServerPath, conOptions);
- //Connect to the COM interface
- Response.Write(Scope.Options.Authentication + "</br>");
- Response.Write(Scope.Options.Authority + "</br>");
- Response.Write(Scope.Options.Context + "</br>");
- Response.Write(Scope.Options.EnablePrivileges + "</br>");
- Response.Write(Scope.Options.Impersonation + "</br>");
- Response.Write(Scope.Options.Locale + "</br>");
- //Response.Write(Scope.Options.Password + "</br>");
- //Response.Write(Scope.Options.SecurePassword + "</br>");
- Response.Write(Scope.Options.Timeout + "</br>");
- Response.Write(Scope.Options.Username + "</br>");
- Response.Write("</br>");
- Response.Write(Scope.Path.ClassName + "</br>");
- Response.Write(Scope.Path.IsClass + "</br>");
- Response.Write(Scope.Path.IsInstance + "</br>");
- Response.Write(Scope.Path.IsSingleton + "</br>");
- Response.Write(Scope.Path.NamespacePath + "</br>");
- Response.Write(Scope.Path.Path + "</br>");
- Response.Write(Scope.Path.RelativePath + "</br>");
- Response.Write(Scope.Path.Server + "</br>");
- Response.Write("</br>");
- Response.Write(Scope.IsConnected.ToString() + "</br>");
- Response.Write("</br>");
- Scope.Connect();
- Response.Write("</br>");
- Response.Write(Scope.IsConnected.ToString() + "</br>");
- Response.Write("</br>");
- //Create a new ObjectQuery
- //ObjectQuery objectQuery = new ObjectQuery("select * from Win32_Process");
- //ManagementObjectSearcher searcher = new ManagementObjectSearcher(objectQuery);
- //foreach (ManagementObject BiosEntry in searcher.Get())
- //{
- //Response.Write(BiosEntry["Name"] + "</br>");
- //}
- }
- }
}
Aspx error
- Server Error in '/' Application.
- --------------------------------------------------------------------------------
- Invalid parameter
- Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
- Exception Details: System.Management.ManagementException: Invalid parameter
- Source Error:
- Line 54: Response.Write("</br>");
- Line 55:
- Line 56: Scope.Connect();
- Line 57:
- Line 58: Response.Write("</br>");
- Source File: C:\Data\My Documents\Visual Studio 2008\Projects\WMIQueryTest\WMIQueryTest\Default.aspx.cs Line: 56
- Stack Trace:
- [ManagementException: Invalid parameter ]
- System.Management.ManagementException.ThrowWithExtendedInfo(ManagementStatus errorCode) +377984
- System.Management.ManagementScope.InitializeGuts(Object o) +654
- System.Management.ManagementScope.Initialize() +162
- System.Management.ManagementScope.Connect() +4
- WMIQueryTest._Default.Page_Load(Object sender, EventArgs e) in C:\Data\My Documents\Visual Studio 2008\Projects\WMIQueryTest\WMIQueryTest\Default.aspx.cs:56
- System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e) +14
- System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e) +35
- System.Web.UI.Control.OnLoad(EventArgs e) +99
- System.Web.UI.Control.LoadRecursive() +50
- System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +627
- --------------------------------------------------------------------------------
- Version Information: Microsoft .NET Framework Version:2.0.50727.3082; ASP.NET Version:2.0.50727.3082
Realizing I needed more information with my failing code, I started thinking closer to home. My code works locally, so, the ASP code is fine. My WMIC call works, so, it's not connection. Without anything else to point towards I decided to modify the WMIC logging to verbose. You can do this by left clicking the WMIC node in the computer management console under Services and Application. You then right click and select Properties. If you are using Window XP Pro, the machine connects to the WMIC Control Panel. You can set the logging level to Verbose. By default, on XP, the logs will be located in the C:\windows\system32\wbem\logs directory. Within this directory I was able to see two files that had problems. I would run the aspx page, see which files updated, then check those out. Well, this led me to realize that I was still entering something incorrectly. When I spotted that I still accidentally entered NTDLMDOMAIN I switched and it worked fine. Error was gone. But, I was still not getting the remote machine.
- Back in the Scripting Guys forum one fellow suggested I try using a different overload of the ManagementObjectSearcher that included the Scope object. When I added that to the instantiation I was able to finally get the page up and running.
So, when it was all said and done, I had learned a good deal about WMI, WMIC, the .NET Management classes and the integration of WMI into ASP.NET. Below is the working code.
- using System;
- using System.Collections;
- using System.Configuration;
- using System.Data;
- using System.IO;
- using System.Linq;
- using System.Management;
- using System.Web;
- using System.Web.Security;
- using System.Web.UI;
- using System.Web.UI.HtmlControls;
- using System.Web.UI.WebControls;
- using System.Web.UI.WebControls.WebParts;
- using System.Xml.Linq;
-
- namespace WMIQueryTest
- {
- public partial class _Default : System.Web.UI.Page
- {
- protected void Page_Load(object sender, EventArgs e)
- {
- //Set your machine path
- //string sServerPath = @"\\" + Environment.MachineName + @"\root\cimv2";
- string sServerPath = @"\\RemoteMachine\root\cimv2";
-
- //Set the impersonation options and add your username/password - for testing only
- ConnectionOptions conOptions = new ConnectionOptions(null, "Administrator", "AdminPassword", "ntlmdomain:WORKGROUP", ImpersonationLevel.Impersonate, AuthenticationLevel.Call, true, null, new TimeSpan());
-
- //Create a ManagementScope object
- ManagementScope Scope = new ManagementScope(sServerPath, conOptions);
-
- //Connect to the COM interface
- Response.Write(Scope.Options.Authentication + "</br>");
- Response.Write(Scope.Options.Authority + "</br>");
- Response.Write(Scope.Options.Context + "</br>");
- Response.Write(Scope.Options.EnablePrivileges + "</br>");
- Response.Write(Scope.Options.Impersonation + "</br>");
- Response.Write(Scope.Options.Locale + "</br>");
- //Response.Write(Scope.Options.Password + "</br>");
- //Response.Write(Scope.Options.SecurePassword + "</br>");
- Response.Write(Scope.Options.Timeout + "</br>");
- Response.Write(Scope.Options.Username + "</br>");
- Response.Write("</br>");
- Response.Write(Scope.Path.ClassName + "</br>");
- Response.Write(Scope.Path.IsClass + "</br>");
- Response.Write(Scope.Path.IsInstance + "</br>");
- Response.Write(Scope.Path.IsSingleton + "</br>");
- Response.Write(Scope.Path.NamespacePath + "</br>");
- Response.Write(Scope.Path.Path + "</br>");
- Response.Write(Scope.Path.RelativePath + "</br>");
- Response.Write(Scope.Path.Server + "</br>");
- Response.Write("</br>");
- Response.Write(Scope.IsConnected.ToString() + "</br>");
- Response.Write("</br>");
-
- Scope.Connect();
-
- Response.Write("</br>");
- Response.Write(Scope.IsConnected.ToString() + "</br>");
- Response.Write("</br>");
-
- //Create a new ObjectQuery
- ObjectQuery objectQuery = new ObjectQuery("select * from Win32_Process");
- ManagementObjectSearcher searcher = new ManagementObjectSearcher(<STRONG>Scope</STRONG>, objectQuery);
-
- foreach (ManagementObject BiosEntry in searcher.Get())
- {
- Response.Write(BiosEntry["Name"] + "</br>");
- }
- }
- }
- }
Thanks for the post. Ive been looking for something like this for our internal project.
ReplyDeleteI am having some problems. Are you able to post a link to your actual source files?
Thanks
I am not even sure if I have them any more. I'll check around.
ReplyDelete