Enumerate Windows user group members on remote system using c# - c#

Within c#, I need to be able to
Connect to a remote system, specifying username/password as appropriate
List the members of a localgroup on that system
Fetch the results back to the executing computer
So for example I would connect to \SOMESYSTEM with appropriate creds, and fetch back a list of local administrators including SOMESYSTEM\Administrator, SOMESYSTEM\Bob, DOMAIN\AlanH, "DOMAIN\Domain Administrators".
I've tried this with system.directoryservices.accountmanagement but am running into problems with authentication. Sometimes I get:
Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. Disconnect all previous connections to the server or shared resource and try again. (Exception from HRESULT: 0x800704C3)
The above is trying because there will be situations where I simply cannot unmap existing drives or UNC connections.
Other times my program gets UNKNOWN ERROR and the security log on the remote system reports an error 675, code 0x19 which is KDC_ERR_PREAUTH_REQUIRED.
I need a simpler and less error prone way to do this!

davidg was on the right track, and I am crediting him with the answer.
But the WMI query necessary was a little less than straightfoward, since I needed not just a list of users for the whole machine, but the subset of users and groups, whether local or domain, that were members of the local Administrators group. For the record, that WMI query was:
SELECT PartComponent FROM Win32_GroupUser WHERE GroupComponent = "Win32_Group.Domain='thehostname',Name='thegroupname'"
Here's the full code snippet:
public string GroupMembers(string targethost, string groupname, string targetusername, string targetpassword)
{
StringBuilder result = new StringBuilder();
try
{
ConnectionOptions Conn = new ConnectionOptions();
if (targethost != Environment.MachineName) //WMI errors if creds given for localhost
{
Conn.Username = targetusername; //can be null
Conn.Password = targetpassword; //can be null
}
Conn.Timeout = TimeSpan.FromSeconds(2);
ManagementScope scope = new ManagementScope("\\\\" + targethost + "\\root\\cimv2", Conn);
scope.Connect();
StringBuilder qs = new StringBuilder();
qs.Append("SELECT PartComponent FROM Win32_GroupUser WHERE GroupComponent = \"Win32_Group.Domain='");
qs.Append(targethost);
qs.Append("',Name='");
qs.Append(groupname);
qs.AppendLine("'\"");
ObjectQuery query = new ObjectQuery(qs.ToString());
ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
ManagementObjectCollection queryCollection = searcher.Get();
foreach (ManagementObject m in queryCollection)
{
ManagementPath path = new ManagementPath(m["PartComponent"].ToString());
{
String[] names = path.RelativePath.Split(',');
result.Append(names[0].Substring(names[0].IndexOf("=") + 1).Replace("\"", " ").Trim() + "\\");
result.AppendLine(names[1].Substring(names[1].IndexOf("=") + 1).Replace("\"", " ").Trim());
}
}
return result.ToString();
}
catch (Exception e)
{
Console.WriteLine("Error. Message: " + e.Message);
return "fail";
}
}
So, if I invoke Groupmembers("Server1", "Administrators", "myusername", "mypassword"); I get a single string returned with:
SERVER1\Administrator
MYDOMAIN\Domain Admins
The actual WMI return is more like this:
\\SERVER1\root\cimv2:Win32_UserAccount.Domain="SERVER1",Name="Administrator"
... so as you can see, I had to do a little string manipulation to pretty it up.

This should be easy to do using WMI. Here you have a pointer to some docs:
WMI Documentation for Win32_UserAccount
Even if you have no previous experience with WMI, it should be quite easy to turn that VB Script code at the bottom of the page into some .NET code.
Hope this helped!

I would recommend using the Win32 API function NetLocalGroupGetMembers. It is much more straight forward than trying to figure out the crazy LDAP syntax, which is necessary for some of the other solutions recommended here. As long as you impersonate the user you want to run the check as by calling "LoginUser", you should not run into any security issues.
You can find sample code for doing the impersonation here.
If you need help figuring out how to call "NetLocalGroupGetMembers" from C#, I reccomend that you checkout Jared Parson's PInvoke assistant, which you can download from codeplex.
If you are running the code in an ASP.NET app running in IIS, and want to impersonate the user accessing the website in order to make the call, then you may need to grant "Trusted for Delegation" permission to the production web server.
If you are running on the desktop, then using the active user's security credentials should not be a problem.
It is possible that you network admin could have revoked access to the "Securable Object" for the particular machine you are trying to access. Unfortunately that access is necessary for all of the network management api functions to work. If that is the case, then you will need to grant access to the "Securable Object" for whatever users you want to execute as. With the default windows security settings all authenticated users should have access, however.
I hope this helps.
-Scott

You should be able to do this with System.DirectoryServices.DirectoryEntry. If you are having trouble running it remotely, maybe you could install something on the remote machines to give you your data via some sort of RPC, like remoting or a web service. But I think what you're trying should be possible remotely without getting too fancy.

If Windows won't let you connect through it's login mechanism, I think your only option is to run something on the remote machine with an open port (either directly or through remoting or a web service, as mentioned).

Related

Convert SessionId to User Account SID _without_ WTSQuerySessionInformation?

I'm working on a service (C#) that receives session-change notifications (specifically SessionLogon). The only piece of information I get with that notification is SessionId.
My ultimate goal is to check the logon user's profile (local/roaming AppData/MyCorp/MyApp folder) for a particular setting, and perform a task if it's there.
I need to go from SessionId to something I can map to a local user profile, either directly to a User Account SID or to something that can be mapped to a SID, (e.g. "<domain>\<username>", etc).
The solutions I've found on SO depend upon Windows Terminal Services (WTS) APIs (e.g. WTSQuerySessionInformation), but Remote Desktop Services isn't available on Windows 10 Home edition, so that's a non-starter.
Does anyone know how to map from SessionId to a local user account that doesn't involve WTS APIs?
(EDIT #1) CLARIFICATION:
In .NET, the ServiceBase class has an OnSessionChange override that gets called for login/logout/unlock/lock events. I was originally thinking this was for all such events (from physical machine or Terminal Server).
It looks like this only applies to Terminal Server sessions(?) So, apparently, the sessionId that I get back is a TerminalServer-specific thing. As #RbMm points out below, this override probably wouldn't get called in the first place on Windows Home edition. It's a moot point, though, because it was the local (physical) logon events I was interested in, and that's completely different from Terminal Service sessions.
It seems odd to me that the service base class would have a useful event like this, but have it tied to Terminal Services, rather than work for all cases. Maybe someone has some insight into this?
(EDIT #2) REALIZATION:
#RbMm's comments have cleared up some misconceptions that I started with. Here's a update:
The OnSessionChange event is only for Terminal Services, and has nothing to do with local (physical) logon sessions (I was conflating the two).
I'm only interested in the local logon sessions, so I'll be looking for a way to get notified about them inside my service. If no such notification is available, I'll have to set up a timer and poll.
I'll need to derive a user account SID from whatever piece of information I receive along with such a notification (or periodic call to LsaGetLogonSessionData)
I think you can retrieve this information via WMI
Win32_LogonSession
class Win32_LogonSession : Win32_Session
{
string Caption;
string Description;
datetime InstallDate;
string Name;
string Status;
datetime StartTime;
string AuthenticationPackage;
string LogonId;
uint32 LogonType;
};
Furthermore:
LogonId
Data type: string
Access type: Read-only
Qualifiers: key
ID assigned to the logon session.
Example
var scope = new ManagementScope(ManagementPath.DefaultPath);
var query = new SelectQuery($"Select * from Win32_LogonSession where LogonId = {SessionId}");
var searcher = new ManagementObjectSearcher(scope, query);
var results = searcher.Get();
foreach (ManagementObject mo in results)
{
}
Note this is fully untested
You can resolve this info via processes instead of tokens.
I dont have a c# sample here, but here is PS-snippet to demo the concept:
# https://learn.microsoft.com/en-us/windows/win32/devnotes/getting-the-active-console-session-id
$sessionId = [System.Runtime.InteropServices.Marshal]::ReadByte(0x7ffe02d8)
$pList = (Get-CimInstance Win32_Process -Filter "name = 'explorer.exe' and SessionId=$sessionId")
$winEx = $pList | where {$_.Path -eq 'C:\WINDOWS\explorer.exe'} | sort CreationDate | select -First 1
$sid = (Invoke-CimMethod -InputObject $winEx -MethodName GetOwnerSid).sid

LDAP Change password: Exception from HRESULT: 0x80070547

I am trying to run my password change application from a non domain joined machine. The code works fine when run from domain joined machine. So now, I am connecting to the AD with direct LDAP connection via SSL. After changepassword method is invoked, I am getting an error:
Configuration information could not be read from the domain controller, either because the machine is unavailable, or access has been denied. (Exception from HRESULT: 0x80070547).
I am making the connection and running the application using a service account with permission to change user passwords.
string adminUser = Domain + #"\" + AdminUserName;
string adminPass = AdminUserPassword;
string ldapString = LDAPString;
DirectoryEntry de = new DirectoryEntry(ldapString, adminUser, adminPass, AuthenticationTypes.Secure);
DirectorySearcher deSearch = new DirectorySearcher(de) { SearchRoot = de, Filter = "(&(objectCategory=user)(cn=" + userName + "))" };
SearchResult result = deSearch.FindOne();
if (result != null)
{
var adContext = new PrincipalContext(ContextType.Domain);
currentdc = adContext.ConnectedServer;
DirectoryEntry userEntry = result.GetDirectoryEntry();
if (userEntry != null)
{
userEntry.Invoke("ChangePassword", new object[] { OldPassword, NewPassword });
}
}
Invoking ChangePassword, calls IADsUser::ChangePassword. That documentation says it works much the same as IADsUser::SetPassword. That documentation has more information. Really, only the first method would work when you're running this from outside the domain:
First, the LDAP provider attempts to use LDAP over a 128-bit SSL connection. For LDAP SSL to operate successfully, the LDAP server must have the appropriate server authentication certificate installed and the clients running the ADSI code must trust the authority that issued those certificates. Both the server and the client must support 128-bit encryption.
I assume your LDAPString is in the format LDAP://example.com:636 (the :636 being the important part). If you can read data like that, then the SSL certificate is trusted. So that's good.
The only maybe missing piece could be 128-bit encryption? Check the certificate and see if it's maybe using less than 128-bit. Although I'd be surprised if it did.
This answer has a short snippet of code that you can use to download a certificate from any site: https://stackoverflow.com/a/22251597/1202807
Just use "https://example.com:636" as the "website".
There is also this:
In Active Directory, the caller must have the Change Password extended control access right to change the password with this method.
You should make sure that the user account you are authenticating to LDAP with does have the Change Password permission on the account you are trying to update. In our environment, Everyone has the Change Password permission (since you still need to provide the old password to do it). I think that's the default, but it's worth checking.

C#: Check If User Has Access To Server Without Attempting Connection

Good afternoon,
I am currently facing an issue where I have a list of server names, and I need to verify that a windows authenticated user has permissions to the specified server names without attempting to establish a connection to the specified server, before I present the list of servers to them. So for example:
Servers: A, B, C, D
Joe has permissions to A and D but not B and C so Joe should only see A and D in his server list.
How should I be tackling this issue? Should I be pulling from Active Directory? I know how to pull my user's identity, but where to pull the server information and find out if a user has permissions is a completely different story. Any documentation, code samples, articles, etc. are helpful.
Notes About The Work Environment
All of the servers in the list are database servers running SQL Server 2008 R2 and higher.
This is a government environment where there are heavy restrictions.
I am unable to modify Active Directory in any way.
I can query Active Directory and SQL Server all day, provided the user has permissions.
Using third party libraries and tools are not authorized.
Side Note About The Server List
This list is stored in a database, however I don't think this really helps with the permissions side. I've already solved the issue of checking permissions against the individual databases with the following SQL query:
SELECT name
FROM sys.databases
WHERE HAS_DBACCESS(name) = 1
ORDER BY name
Thank you for your help!
-Jamie
Conclusion
Essentially, I have found no possible way to query the AD groups on the remote server without attempting to establish a connection, thus the execution of all samples found will flag the users account with IA. For others with a similar issue that don't have to worry about IA dropping a hammer on users, I have included a few solutions below you can try to meet your needs (all of the following will work when implemented correctly).
Solutions
Query the server in active directory and retrieve the groups from the server, if this fails 9 times out of ten the exception message will be 'Access is denied.'. If it succeeds, proceed to pull the current user's groups and compare to the groups pulled from the server. The selected answer to this post combined with this post and/or this post will get you where you need to be with this solution.
If you have access to the server already through impersonation or other means (SQL Server Auth) then you can use the following to see if the member has any server roles assigned:
SELECT IS_SRVROLEMEMBER('public')
You could also use the Login class available in the Microsoft.SqlServer.Smo assembly using the Microsoft.SqlServer.Management.Smo namespace. However, you may or may not have issues moving the Microsoft.SqlServer.SqlClrProvider namespace from the GAC to the BIN. More information about this can be found at this StackOverflow post, and at this Microsoft Connect thread which states the following:
Client applications should not be using the assemblies from the Program Files folders unless they are from the specific SDK folders (such as "C:\Program Files (x86)\Microsoft SQL Server\130\SDK")
You could even do a basic connection test wrapped in a try catch to see if the connection will work.
using (SqlConnection conn = new SqlConnection(connString)) {
try {
conn.Open();
conn.Close();
} catch (Exception e) { Console.Write($"Connection test failed: {e.Message}"); }
}
There are a small variety of ways to achieve the overall goal, it just depends on your particular situation and how you want to approach it. For me, none of the solutions will work since in each scenario the test will attempt a connection to the server in question which will flag the user due to lack of permissions.
If you have Active Directory implemented then you should be giving users access rights to things like servers via AD groups anyways or else that creates a management nightmare. Imagine if John Smith joins your company as a sys admin, are you going to go to every server and explicitly assign him rights? Much easier to just create a server admin AD group then assign it to the server (or dictate what AD groups exists on servers and permission levels by group policy.
Why this also helps you is that when you develop applications, you can use the built in AD role provider to serve up things like this. Here is a simple example of grabbing a users groups by AD user Name
using System.DirectoryServices.AccountManagement;
public List<string> GetGroupNames(string userName)
{
List<string> result = new List<string>();
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YOURDOMAINHERE"))
{
using (PrincipalSearchResult<Principal> src = UserPrincipal.FindByIdentity(pc, userName).GetGroups(pc))
{
src.ToList().ForEach(sr => result.Add(sr.SamAccountName));
}
}
return result;
}
EDIT: So if you absolutely refuse to use active directory groups to manage permissions on servers and buying a tool is out of the question, here is a class that will iterate through all of your local machine groups and give you a list of users within those groups. You could do something like have it run as a scheduled task on the server (or win service) and save it's results back to a DB so you can query or build a UI to pull and monitor this info at any time. This doesn't reach out and grab sql server permissions as you said you already have that.
public class MachinePermissions
{
string machineName { get; set; }
public List<LocalGroup> localGroups { get; set; }
public List<string> GetGroupMembers(string sGroupName)
{
List<String> myItems = new List<String>();
GroupPrincipal oGroupPrincipal = GetGroup(sGroupName);
PrincipalSearchResult<Principal> oPrincipalSearchResult = oGroupPrincipal.GetMembers();
foreach (Principal oResult in oPrincipalSearchResult)
{
myItems.Add(oResult.Name);
}
return myItems;
}
private GroupPrincipal GetGroup(string sGroupName)
{
PrincipalContext oPrincipalContext = GetPrincipalContext();
GroupPrincipal oGroupPrincipal = GroupPrincipal.FindByIdentity(oPrincipalContext, sGroupName);
return oGroupPrincipal;
}
private PrincipalContext GetPrincipalContext()
{
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Machine);
return oPrincipalContext;
}
public MachinePermissions()
{
machineName = Environment.MachineName;
PrincipalContext ctx = new PrincipalContext(ContextType.Machine, Environment.MachineName);
GroupPrincipal gp = new GroupPrincipal(ctx);
gp.Name = "*";
PrincipalSearcher ps = new PrincipalSearcher();
ps.QueryFilter = gp;
PrincipalSearchResult<Principal> result = ps.FindAll();
if(result.Count() > 0)
{
localGroups = new List<LocalGroup>();
foreach (Principal p in result)
{
LocalGroup g = new LocalGroup();
g.groupName = p.Name;
g.users = GetGroupMembers(g.groupName);
localGroups.Add(g);
}
}
}
}
public class LocalGroup
{
public string groupName { get; set; }
public List<String> users { get; set; }
}
You can create AD group for accessing each database, then add users to them.
In your app you can add list of groups and check if user Is member of them.
It's common practice and allow to create secure scenarios for different access right for different users. You only set permissions for group once and all members can benefit from access rights.

Connect to Active Directory using LdapConnection class on remote server

I have a problem: I need to connect from a remote server to Active Directory, but the code has to be using the LdapConnection class. I need this because that way I can only test change notifiers when some event happen (such as user is deactivated or he changed group, data etc). OS on the remote server is Windows Server 2012.
I managed to do this from local using DirectoryServices with the following code:
String ldapPath = "LDAP://XRMSERVER02.a24xrmdomain.info";
directoryEntry = new DirectoryEntry(ldapPath, #"A24XRMDOMAIN\username", "pass");
//// Search AD to see if the user already exists.
DirectorySearcher search = new DirectorySearcher(directoryEntry);
search.Filter = "(&(objectClass=user))";
SearchResult result = search.FindOne();
This is okay and connection works but now I need to connect using the LdapConnection class.
I tried something like this on many ways but none of that helped me:
LdapConnection connection = new LdapConnection(XRMSERVER02.a24xrmdomain.info);
var credentials = new NetworkCredential(#"A24XRMDOMAIN\username", "pass");
connection.Credential = credentials;
connection.Bind();
It says that credentials are invalid but that is not true.
Explanations:
XRMSERVER02 - Domain controller
a24xrmdomain.info - Domain
A24XRMDOMAIN - Domain used for logging
Thanks for your help.
Even though I solved my problem I want to share with other developers what I achieved so far. Problem that I encountered was that I had remote server with OS Windows server 2012 and Active directory on it. I needed to connect on him via my local machine(Windows 10).
As I stated in my question it is possible to do that via DirectoryServices with the following code:
String ldapPath = "LDAP://(DomainController).a24xrmdomain.info";
directoryEntry = new DirectoryEntry(ldapPath, #"DOMAIN\username","pass");
//// Test search on AD to see if connection works.
DirectorySearcher search = new DirectorySearcher(directoryEntry);
search.Filter = "(&(objectClass=user))";
SearchResult result = search.FindOne();
This is one of the solutions, but since my task was to get notification and to identify when ever some object has changed in Active Directory, I needed connection to Active Directory on Remote server via LDAP class. Code for getting notifiers is taken from:
- Registering change notification with Active Directory using C#
I succeeded to connect with LDAP class via next code:
String ldapPath2 = "(DomainController).a24xrmdomain.info";
LdapConnection connection = new LdapConnection(ldapPath2);
var credentials = new NetworkCredential(#"username", "pass");
connection.Credential = credentials;
connection.Bind();
Want to mention that no IP address of remote server is needed, just Domain Controller that is used on him, and that Domain used for logging is unnecessary.
Happy coding
Try using NetworkCredential constructor with 3 parameters: username, password and domain. Specify domain separately from user name

C# access data from COMAdmin.COMAdminCatalog using WMI

Earlier I was using code as below to grab COM+ applications and verify that my app is running
COMAdmin.COMAdminCatalog catalog = new COMAdmin.COMAdminCatalogClass();
catalog.Connect(servername);
catalog.GetCollection("Applications")
Now I need to perform the same actions but from other domain. So when I try to run the code above I receive authentication error.
I have tried to connect via WMI and grab list of COM+ applications from win32 wmi providers, but it seems that it's either not possible or I am doing smth wrong.
I would be pleased if someone could help me to get the list of applications from COMAdminCatalog using credentials.
You will have to impersonate a different user on the current thread.
using (ImpersonatedUser user = new ImpersonatedUser("USER_NAME", "DOMAIN_NAME", "USER PASSWORD"))
{
COMAdmin.COMAdminCatalog objCatalog = new COMAdmin.COMAdminCatalog();
objCatalog.Connect("SERVER_NAME");
COMAdmin.COMAdminCatalogCollection objAppCollection =
(COMAdmin.COMAdminCatalogCollection) objCatalog.GetCollection("Applications");
objAppCollection.Populate();
}
For more details:
ImpersonatedUser class: https://blogs.msdn.microsoft.com/joncole/2009/09/21/impersonation-code-in-c/
How to impersonate: https://blogs.msdn.microsoft.com/shawnfa/2005/03/21/how-to-impersonate/

Categories

Resources