SharpHound 从 AD 和 Windows 主机收集对象、权限、组成员及会话数据,输出 JSON 供 BloodHound 导入。理解图中的关系,需要先知道每项数据来自哪里、采集时使用什么权限,以及服务端真正返回了什么。
源码分析涉及客户端和公共库两个项目:
- SharpHound
132eb0d:客户端编排与输出,项目版本为 2.13.0,目标框架为net472。 - SharpHoundCommon
6ff9c69:协议调用、对象处理器、关系解析与缓存。
客户端的 CommonSource 支持 Stable、Dev 和 Local:Stable 使用 4.6.2,当前默认 Dev 会解析 4.6.3-dev*,Local 则引用相邻 Common 仓库编译出的 DLL。下文分别以这两份固定源码说明实现。运行已编译采集器时,可以从启动日志查看实际 SharpHound / Common 版本;与 BloodHound CE 的配套版本可在其 Download Collectors 中取得。
收集方法与数据来源
功能概述
收集方法按数据来源可以分成目录查询和主机查询两部分。一个方法也可能组合 LDAP、共享文件及 RPC,名称本身不代表只访问一种接口:
| 分类 | 收集方法 | 采集的数据 |
|---|---|---|
| 基础 LDAP | Group, ACL, ObjectProps, Container, Trusts | 组成员、访问权限、对象属性、OU 层级、域信任 |
| 会话类 | Session, LoggedOn | 网络会话、交互式登录用户 |
| 本地组 | LocalAdmin, RDP, DCOM, PSRemote | 各本地特权组成员 |
| 证书服务 | CertServices, CARegistry | ADCS 配置、证书模板、CA 注册权限 |
| 域控安全 | DCRegistry, LdapServices | 域控注册表配置、LDAP 签名/通道绑定状态 |
| 其他 | SPNTargets, UserRights, GPOLocalGroup, SmbInfo, WebClientService, NTLMRegistry | SPN 关系、用户权限、GPO 本地组、SMB 签名、WebClient 状态、NTLM 设置 |
预定义组合
SharpHound 提供了几个方便的组合别名:
- Default:
Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container | CertServices | LdapServices | SmbInfo | WebClientService - DCOnly:
ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices(以目录与 SYSVOL 数据为主,CertServices 端点探测另见后文) - ComputerOnly:
LocalGroups | Session | UserRights | CARegistry | DCRegistry | WebClientService | SmbInfo | NTLMRegistry。仍需从目录中确定计算机对象和解析身份,后续再连接目标主机。 - All:Default + LoggedOn + GPOLocalGroup + UserRights + CARegistry + DCRegistry + NTLMRegistry
整体架构概览
可以沿运行阶段、数据流水线和方法标志三部分阅读源码:
执行阶段与共享上下文
入口通过 Links<IContext> 串联初始化、连接测试、域发现、基础采集、可选循环、缓存保存和结束等步骤。接口定义于客户端 src/Client/Links.cs,下面省略部分成员:
public interface Links<T> {
IContext Initialize(IContext context, LdapConfig options);
Task<IContext> TestConnection(T context);
IContext SetSessionUserName(string overrideUserName, T context);
IContext InitCommonLib(T context);
Task<IContext> GetDomainsForEnumeration(T context);
IContext StartBaseCollectionTask(T context);
Task<IContext> AwaitBaseRunCompletion(T context);
IContext StartLoopTimer(T context);
IContext StartLoop(T context);
Task<IContext> AwaitLoopCompletion(T context);
IContext SaveCacheFile(T context);
IContext Finish(T context);
}
每个步骤接收共享的 Context,写入配置、任务、域范围与运行状态,再交给后续步骤。
Producer-Consumer 模型(生产者-消费者流水线)
数据采集的核心使用了 System.Threading.Channels,组织生产者、异步消费者和输出写入器(Runtime/CollectionTask.cs):

- Producer:执行 LDAP 分页查询,将每个搜索结果写入
_ldapChannel(有界通道,容量 1000,写满时阻塞) - Consumers:从
_ldapChannel中读取对象,交给ObjectProcessors进行处理,处理结果写入_outputChannel(无界通道) - OutputWriter:从
_outputChannel中读取结果并写入 JSON 文件
关键代码(CollectionTask.cs):
// 启动 N 个消费者任务(默认 50 个)
for (var i = 0; i < _context.Threads; i++) {
var consumer = ConsumeSearchResults();
_taskPool.Add(consumer);
}
// 启动输出写入器
var outputTask = _outputWriter.StartWriter();
// 启动生产者(LDAP 查询 Default NC)
var producerTask = _producer.Produce();
await producerTask;
// 启动生产者(LDAP 查询 Configuration NC)
var producerTaskNC = _producer.ProduceConfigNC();
await producerTaskNC;
// 关闭 LDAP Channel,等待消费者完成
_ldapChannel.Writer.Complete();
await Task.WhenAll(_taskPool);
// 写入 Well-Known Principal 输出
// 关闭 Output Channel,等待写入器完成
_outputChannel.Writer.Complete();
var zipFile = await outputTask;
收集方法的标志组合
用户通过 -c / --collectionmethods 选择收集方法。SharpHound 内部使用 [Flags] 枚举 CollectionMethod(CommonLib/Enums/CollectionMethod.cs)进行位运算组合:
[Flags]
public enum CollectionMethod {
None = 0,
Group = 1,
LocalAdmin = 1 << 1, // 2
GPOLocalGroup = 1 << 2, // 4
Session = 1 << 3, // 8
LoggedOn = 1 << 4, // 16
Trusts = 1 << 5, // 32
ACL = 1 << 6, // 64
Container = 1 << 7, // 128
RDP = 1 << 8, // 256
ObjectProps = 1 << 9, // 512
DCOM = 1 << 12, // 4096
SPNTargets = 1 << 13, // 8192
PSRemote = 1 << 14, // 16384
UserRights = 1 << 15, // 32768
CARegistry = 1 << 16,
DCRegistry = 1 << 17,
CertServices = 1 << 18,
LdapServices = 1 << 19,
WebClientService = 1 << 21,
SmbInfo = 1 << 22,
NTLMRegistry = 1 << 23,
// 组合别名
LocalGroups = DCOM | RDP | LocalAdmin | PSRemote,
ComputerOnly = LocalGroups | Session | UserRights | CARegistry | DCRegistry | WebClientService | SmbInfo | NTLMRegistry,
DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices,
Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container | CertServices | LdapServices | SmbInfo | WebClientService,
All = Default | LoggedOn | GPOLocalGroup | UserRights | CARegistry | DCRegistry | WebClientService | LdapServices | NTLMRegistry,
}
方法通过按位或组合,(resolved & CollectionMethod.ACL) != 0 即可判断具体方法是否被选中。
核心执行流程
SharpHound 的执行由 SharpLinks.cs 完整编排,各步骤共享 Context;失败标志与取消状态用于阻止后续工作。下面展示关键片段,省略部分日志、异常处理和返回值:
Initialize —— 初始化(SharpLinks.cs)
public IContext Initialize(IContext context, LdapConfig options) {
// 1. 设置 JSON 序列化设置(用于缓存文件的序列化/反序列化)
JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
Converters = new List<JsonConverter> { new KindConvertor() }
};
// 2. 对日志系统进行重新配置
CommonLib.ReconfigureLogging(context.Logger);
// 3. 如果未指定域名,自动探测当前域
if (string.IsNullOrWhiteSpace(context.DomainName)) {
if (!context.LDAPUtils.GetDomain(out var d)) {
context.Flags.IsFaulted = true; // 失败则标记 Faulted,阻止后续步骤
} else {
context.DomainName = d.Name;
}
}
// 4. 如果启用 Loop 模式,验证 LoopDuration(默认 2 小时)和 LoopInterval(默认 30 秒)
// 5. 本方法的 Loop 分支会创建/删除临时文件测试输出目录
}
TestConnection —— 验证 LDAP 连接(SharpLinks.cs)
public async Task<IContext> TestConnection(IContext context) {
if (await context.LDAPUtils.TestLdapConnection(context.DomainName) is (false, var message)) {
context.Flags.IsFaulted = true;
}
}
客户端调用 LDAPUtils.TestLdapConnection(),经连接池取得并释放连接,检查能否建立可用的域 LDAP 连接。它用于提前发现连接/绑定失败,不等于后面的每个属性和对象都已通过授权。
SetSessionUserName(SharpLinks.cs)
获取当前运行用户的 samaccountname(去掉域前缀),用于后续 Session 枚举时过滤掉自身产生的会话:
context.CurrentUserName = overrideUserName ?? WindowsIdentity.GetCurrent().Name.Split('\\')[1];
InitCommonLib —— 初始化公共库和缓存(SharpLinks.cs)
加载或创建缓存文件。缓存中存储了 SID 与类型、域名的映射关系,避免重复 LDAP 查询。缓存读写过程见后面的“缓存系统”。
GetDomainsForEnumeration —— 确定枚举范围(SharpLinks.cs)
支持三种模式:
| 模式 | 参数 | 行为 |
|---|---|---|
| 单域(默认) | –domain 可指定域 | 采集指定域,未指定时尝试取得当前域 |
| SearchForest | --searchforest |
采集当前林中的所有域 |
| RecurseDomains | --recursedomains |
通过域信任关系递归遍历所有可达的域 |
说明:跨域枚举官方会输出警告
"Cross-domain enumeration may result in reduced data quality"。原因是跨域 SID 解析依赖全局编录(Global Catalog)或信任链接,部分对象可能无法被成功解析。
递归域发现的实现(SharpLinks.cs):
private async IAsyncEnumerable<EnumerationDomain> BuildRecursiveDomainList(IContext context) {
var enumerationQueue = new Queue<(string domainSid, string domainName)>();
var trustHelper = new DomainTrustProcessor(utils);
// 从当前域开始 BFS
enumerationQueue.Enqueue((currentDomainSid, currentDomainName));
while (enumerationQueue.Count > 0) {
var (domainSid, domainName) = enumerationQueue.Dequeue();
// 枚举该域的信任关系
await foreach (var trust in trustHelper.EnumerateDomainTrusts(domainName)) {
domainResults.Add(new EnumerationDomain {
Name = trust.TargetDomainName,
DomainSid = trust.TargetDomainSid
});
if (!enumeratedDomains.Contains(trust.TargetDomainSid))
enumerationQueue.Enqueue((trust.TargetDomainSid, trust.TargetDomainName));
}
}
}
启动并等待基础采集完成
创建 CollectionTask 对象,启动整个 Producer-Consumer 流水线。
Loop 相关(可选)
如果开启了 Loop 模式,将在基础采集完成后循环执行仅针对计算机的采集方法。循环方法筛选见后面的“Loop 循环采集”。
SaveCacheFile —— 持久化缓存(SharpLinks.cs)
将运行过程中累积的缓存数据序列化为 JSON 写入磁盘,下次运行时可复用。
Finish —— 结束
输出完成信息,结束运行。
LDAP 数据采集
Producer 的两级 LDAP 查询
LdapProducer 是默认的生产者(非 Stealth 模式下)。它执行两级 LDAP 查询:
第一级:Default Naming Context(默认命名上下文)
查询域的主分区(如 DC=domain,DC=com),获取用户、计算机、组、OU、GPO、域对象:
// LdapProducer.cs
foreach (var domain in Context.Domains) {
foreach (var filter in ldapData.Filter.GetFilterList()) {
foreach (var partitionedFilter in GetPartitionedFilter(filter)) {
await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() {
LDAPFilter = partitionedFilter,
Attributes = ldapData.Attributes,
DomainName = domain.Name,
SearchBase = Context.SearchBase,
IncludeSecurityDescriptor = Context.ResolvedCollectionMethods.HasFlag(CollectionMethod.ACL)
}, cancellationToken)) {
// 过滤掉 cn=domainupdates 和 cn=policies 中的某些对象
// 这里只展示成功结果入队,省略失败处理
var searchResult = result.Value;
await Channel.Writer.WriteAsync(searchResult, cancellationToken);
}
}
}
}
查询的 Filter 和 Attributes 由 LdapProducerQueryGenerator(CommonLib/LdapProducerQueryGenerator.cs)根据用户选择的收集方法动态生成:
public static GeneratedLdapParameters GenerateDefaultPartitionParameters(CollectionMethod methods) {
var filter = new LdapFilter();
var properties = new List<string>();
// 基础属性(始终需要)
properties.AddRange(CommonProperties.BaseQueryProps); // objectSid, distinguishedname 等
properties.AddRange(CommonProperties.TypeResolutionProps); // samaccounttype, objectclass 等
if (methods.HasFlag(CollectionMethod.ObjectProps) || methods.HasFlag(CollectionMethod.ACL) || ...) {
// 需要所有主要对象类型
filter = filter.AddComputers().AddDomains().AddUsers().AddContainers().AddGPOs().AddOUs().AddGroups();
// 按需添加属性
if (methods.HasFlag(CollectionMethod.ACL))
properties.AddRange(CommonProperties.ACLProps); // nTSecurityDescriptor 等
if (methods.HasFlag(CollectionMethod.Group))
properties.AddRange(CommonProperties.GroupResolutionProps); // member 等
if (methods.HasFlag(CollectionMethod.SPNTargets))
properties.AddRange(CommonProperties.SPNTargetProps); // servicePrincipalName 等
if (methods.IsComputerCollectionSet())
properties.AddRange(CommonProperties.ComputerMethodProps); // dnshostname 等
// ...
}
// 如果只有计算机类方法,只查询计算机对象(缩小对象范围的 Filter)
else if (methods.IsComputerCollectionSet()) {
filter = filter.AddComputers();
}
// ...
}
第二级:Configuration Naming Context(配置命名上下文)
查询 Active Directory 的配置分区(CN=Configuration,DC=domain,DC=com),获取 证书服务相关对象:
// LdapProducer.cs
public override async Task ProduceConfigNC() {
foreach (var domain in Context.Domains) {
// 获取 Configuration NC 的实际路径
if (await Context.LDAPUtils.GetNamingContextPath(domain.Name, NamingContext.Configuration) is (true, var path)) {
foreach (var filter in configNcData.Filter.GetFilterList()) {
await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() {
LDAPFilter = filter,
Attributes = configNcData.Attributes,
DomainName = domain.Name,
SearchBase = path,
NamingContext = NamingContext.Configuration
}, cancellationToken)) {
if (result.IsSuccess)
await Channel.Writer.WriteAsync(result.Value, cancellationToken);
}
}
}
}
}
Configuration NC 中的查询目标(LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters()):
filter = filter.AddContainers().AddConfiguration()
.AddCertificateTemplates() // 证书模板
.AddCertificateAuthorities() // 根 CA
.AddEnterpriseCertificationAuthorities() // 企业 CA
.AddIssuancePolicies(); // 颁发策略

分页查询(Paged Query)
主对象枚举使用 PagedQuery,通过 LDAP 分页结果控制(RFC 2696)分批读取并继续查询,避免受到单次返回条目数上限的截断。单对象解析等路径也使用普通 Query;分页不会消除总查询量和属性读取成本。
可选的分区查询优化
当 --partitionldapqueries 参数启用时,SharpHound 会将主 LDAP 查询拆分为 256 个子查询(LdapProducer.cs):
private IEnumerable<string> GetPartitionedFilter(string originalFilter) {
if (Context.Flags.ParititonLdapQueries) {
for (var i = 0; i < 256; i++) {
// (objectguid=\00*), (objectguid=\01*), ..., (objectguid=\ff*)
yield return $"(&{originalFilter}(objectguid=\\{i.ToString("x2")}*))";
}
} else {
yield return originalFilter;
}
}
这段代码按 objectGUID 的首字节将原过滤器拆成 256 个桶,每个桶仍独立分页。桶的大小与实际对象分布有关;总共需要更多过滤查询,实际耗时和 DC 成本还涉及索引与目录规模。这个选项改变的是查询组织方式。
AdminSDHolder 特殊处理
如果 ACL 收集被启用,Producer 会在主查询前单独获取 AdminSDHolder 的安全描述符(LdapProducer.cs):
// 查询 cn=adminsdholder,cn=system,<domainDN>
var adminSdHolderParameters = new LdapQueryParameters() {
LDAPFilter = "(objectClass=*)",
Attributes = new[] { "nTSecurityDescriptor", "distinguishedName", "objectClass", "objectSid" },
DomainName = domain.Name,
SearchBase = $"cn=adminsdholder,cn=system,{domainDN}",
IncludeSecurityDescriptor = true
};
// 获取 AdminSDHolder 的 nTSecurityDescriptor 字节
if (searchResult.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var sd)) {
var aclProcessor = new ACLProcessor(Context.LDAPUtils);
// 规范化显式 ACE 并计算哈希(authoritative SD hash)
var authoritativeSd = aclProcessor.CalculateImplicitACLHash(sd);
// 存储到 Context 中,后续处理每个安全主体时用于比较
Context.AdminSDHolderHash.AddOrUpdate(domain.Name, _ => authoritativeSd, (_, _) => authoritativeSd);
}
SDProp 根据 AdminSDHolder 模板维护受保护主体的 DACL。采集器提取显式 ACE,规范化后计算哈希,与目标对象的对应哈希比较,输出 adminsdholderprotected。它表示两份 ACL 在该比较规则下是否匹配;当前实际保护范围、残留 adminCount 和 DACL 继承状态还需分别分析。
Consumer 的处理流程
每个 Consumer 任务(CollectionTask.cs)从 _ldapChannel 中读取 LDAP 结果:
internal async Task ConsumeSearchResults() {
var processor = new ObjectProcessors(_context, log, _compStatusChannel);
await foreach (var item in _ldapChannel.Reader.ReadAllAsync()) {
// 1. 解析搜索结果:确定对象类型、域、DisplayName
if (await LdapUtils.ResolveSearchResult(item, _context.LDAPUtils) is not (true, var res))
continue;
// 2. 分派给 ObjectProcessors 处理
var processed = await processor.ProcessObject(item, res);
// 3. 处理完成,写入 Output Channel
if (processed is Domain d && _context.CollectedDomainSids.Contains(d.ObjectIdentifier)) {
d.Properties.Add("collected", true); // 标记已采集的域
}
await _outputChannel.Writer.WriteAsync(processed);
}
}
对象类型分派(ObjectProcessors.cs):
internal async Task<OutputBase> ProcessObject(IDirectoryObject entry, ResolvedSearchResult res) {
switch (res.ObjectType) {
case Label.User: return await ProcessUserObject(entry, res);
case Label.Computer: return await ProcessComputerObject(entry, res);
case Label.Group: return await ProcessGroupObject(entry, res);
case Label.GPO: return await ProcessGPOObject(entry, res);
case Label.Domain: return await ProcessDomainObject(entry, res);
case Label.OU: return await ProcessOUObject(entry, res);
case Label.Container:
case Label.Configuration: return await ProcessContainerObject(entry, res);
case Label.RootCA: return await ProcessRootCA(entry, res);
case Label.AIACA: return await ProcessAIACA(entry, res);
case Label.EnterpriseCA: return await ProcessEnterpriseCA(entry, res);
case Label.NTAuthStore: return await ProcessNTAuthStore(entry, res);
case Label.CertTemplate: return await ProcessCertTemplate(entry, res);
case Label.IssuancePolicy: return await ProcessIssuancePolicy(entry, res);
}
}
Well-Known Principal 输出
在消费者全部完成后,SharpHound 会额外输出所有 Well-Known Principal(如 Everyone、Authenticated Users、Enterprise Domain Controllers 等)(CollectionTask.cs):
await foreach (var wkp in _context.LDAPUtils.GetWellKnownPrincipalOutput()) {
if (!wkp.ObjectIdentifier.EndsWith("S-1-5-9")) { // Enterprise DCs
wkp.Properties["reconcile"] = false;
}
await _outputChannel.Writer.WriteAsync(wkp);
}
这确保 BloodHound 中能正确显示这些特殊主体节点。
收集方法详解
Group —— 组成员关系
采集的 BloodHound 边:MemberOf
实现原理(GroupProcessor):对每个 Group 对象,读取 LDAP 的 member 属性(包含该组所有直接成员的 DN 列表)。处理流程:
- 对每个 DN,通过
LdapUtils.ResolveDistinguishedName()查询获取其 ObjectSID - 处理 Everyone、Authenticated Users 等 Well-Known SID 的特殊映射;Domain Admins 等域组则使用该域 SID 加相应 RID
- 对每个 User/Computer 对象,额外处理
primaryGroupID属性(用户常为 Domain Users 的 RID 513,成员计算机常为 Domain Computers 的 RID 515)
csharp
// ObjectProcessors.cs
if (_methods.HasFlag(CollectionMethod.Group)) {
var pg = entry.GetProperty(LDAPProperties.PrimaryGroupID);
ret.PrimaryGroupSID = GroupProcessor.GetPrimaryGroupInfo(pg, resolvedSearchResult.ObjectId);
} - 使用 Ranged Retrieval(范围检索)技术处理超大组(成员数超过 1500),通过
member;range=0-1499这种属性名分段获取
默认目录权限下,普通域用户通常可读取组成员属性。
ACL:安全描述符与权限关系
ACL 方法解析 nTSecurityDescriptor 中的 Owner 和 DACL,输出 GenericAll、WriteDacl、WriteOwner、ForceChangePassword、AddMember、复制权限及 LAPS 读取权限等关系。委派目标 AllowedToDelegate 则来自对象委派属性的处理。
安全描述符读取
LdapConnectionPool.CreateSearchRequest() 在需要安全描述符时附加 SecurityDescriptorFlagControl,请求 Owner | Dacl。SACL 的读取有另外的特权要求,这条 ACL 采集路径没有请求它。
searchRequest.Controls.Add(new SecurityDescriptorFlagControl {
SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner
});
普通域用户通常可读取许多目录对象的 Owner / DACL;指定对象仍要通过其目录读取权限检查。
属性 GUID 与 ACE
ACLProcessor.BuildGuidCache() 查询 Schema 中的 schemaIDGUID 与名称,并缓存传统 / Windows LAPS 密码属性的映射,解决不同环境属性 GUID 的识别问题。其他已知扩展权限与属性 GUID 由代码常量提供。
LDAPFilter = "(schemaIDGUID=*)",
NamingContext = NamingContext.Schema,
Attributes = new[] { LDAPProperties.SchemaIDGUID, LDAPProperties.Name }
ACE 的解释需要同时看几项:
- Trustee SID 指向授权主体,先解析为用户、组或计算机。
- 权限掩码说明读、写、控制访问、修改 DACL/Owner 等操作。
ObjectType限制到特定属性、属性集、扩展权限或对象类型。InheritedObjectType和继承标志限制继承范围,IsInherited标记当前 ACE 是否来自继承。- 目标对象类型决定某项权限能形成什么关系。
例如 bf967aba-0de6-11d0-a285-00aa003049e2 是 User 类的 Schema GUID;GenericAll 则是权限掩码,判断还要结合 ACE 的作用范围。给组的 member 属性写权限可形成添加成员关系,给域根的复制扩展权限则用于目录复制分析。
关系提取与有效访问
ProcessACL() 跳过 Deny ACE,并筛选适用于目标类型的允许 ACE,然后映射关系。这是面向图分析的权限提取,没有重演 Windows 针对某个完整访问令牌的全部 AccessCheck。嵌套组、拒绝 ACE、Owner Rights、继承适用范围和操作自身的检查仍需要在分析具体路径时核对。
Owner Rights 在本版本有专门字段标记。Owner 的隐式 DACL 控制能力与显式 ACE 都可能影响路径,不能只看一条边就省去目标对象上的实际授权分析。

SID 与对象类型解析
LDAPUtils.ResolveIDAndType() 先处理已知主体和不可解析缓存,再按 SID / GUID 进入不同分支。SID 分支的主要过程为:
- 查询
IdToTypeCache。 - 尝试由 SID 定位所属域,查询
objectSid。 - 用
LDAP://<SID=...>直接绑定。 - 用
PrincipalContext/Principal.FindByIdentity回退解析。 - 失败时加入不可解析集合,避免本轮持续重复查询。
GUID 分支对应使用 objectGUID。可被解析的主体受域可达性、目录可见性及对象是否存在影响。缓存减少的是重复解析,不会补回缺少权限而读不到的数据。
AdminSDHolder 比较
Producer 先查询 AdminSDHolder 的安全描述符。CalculateImplicitACLHash() 实际规范化的是显式 ACE 集合,包含 Allow 和 Deny、跳过继承 ACE,再排序并计算哈希。对象处理时调用 IsAdminSDHolderProtected() 比较哈希。
var isAdminSdHolderProtected =
_aclProcessor.IsAdminSDHolderProtected(entry, adminSdHolderHash);
if (isAdminSdHolderProtected != null) {
ret.Properties.Add("adminsdholderprotected", isAdminSdHolderProtected);
}
这个结果适合解释模板匹配情况。判断某账户当前是否由 SDProp 保护,还需要结合其受保护组成员关系和目录状态。
源码:ACLProcessor。
Session:网络连接中的身份与来源主机
Session 方法调用 NetSessionEnum 查询某台服务器的网络会话,再把用户名和来源客户端解析为 AD 对象。ComputerSessionProcessor.ReadUserSessions() 使用的是响应中的客户端名称,最后写入 ComputerSID 与 UserSID。
例如用户 Alice 在 WS01 上访问 FS01 的共享,采集器向 FS01 查询得到 Alice / WS01,解析后描述的是 Alice 与来源 WS01 的会话关联。它没有据此证明 Alice 在 FS01 上交互登录。

工具对结果过滤空用户名/计算机名、机器账户、采集者自身和匿名登录。127.0.0.1、[::1] 等本机来源映射为被查询主机,其余来源交给 ResolveHostToSid()。
主机与用户解析
ResolveHostToSid() 先去除 SPN 前缀等格式并查主机缓存,然后依次尝试:
NetWkstaGetInfo取得计算机名和域,再查相应计算机账户。- UDP 137 的 NetBIOS 名称查询,再解析账户。
- 对 FQDN / 短名构造
sAMAccountName=<host>$查询,必要时切换域。 - DNS 名称解析后再次查询计算机账户。
- 失败后缓存不可解析结果。
用户名通过全局编录查询 sAMAccountName,可能在不同域得到多个 SID;必要时再尝试当前域账户解析。这里按账户名匹配,结果不等于已经知道唯一的用户登录令牌。
接口权限与采集状态
SharpHound 的调用使用 NetAPI,服务端按查询级别和 SRVSVC 会话信息权限检查。现代 Windows 上可能因策略返回 Access Denied;查询成功时的可见范围也受权限约束。失败、成功但没有结果,以及过滤后为空,需要分别看待。
输出的 SessionAPIResult 包含 Collected、FailureReason 和 Results。图上没有边时,先检查采集是否成功,再判断是否真的未观察到会话。
源码:ComputerSessionProcessor;权限语义见 NetSessionEnum 文档。
LoggedOn:活动登录与已加载用户配置
LoggedOn 使用 NetWkstaUserEnum 和可选的远程注册表读取,分别写入 PrivilegedSessions、RegistrySessions。
NetWkstaUserEnum
该 API 可返回交互式、服务和批处理登录用户。采集器过滤空域、本地计算机域、机器账户等结果,再将用户解析为域主体,关联到当前被查询的计算机。
枚举所需权限与服务端的接口访问检查有关,常见远程配置需要管理员级别权限。它返回的“已登录”范围比可见的 RDP 桌面更广。
HKEY_USERS
ReadUserSessionsRegistry() 连接远程 HKEY_USERS,枚举 SID 格式的子键,然后解析身份。Windows 加载用户配置文件时会挂载相应用户 hive,因此这是已加载配置的线索。
using (var key = await _registryAccessor.Connect(RegistryHive.Users, computerName)) {
foreach (var subkey in key.GetSubKeyNames()) {
// 匹配 SID 并解析为可识别的域主体
}
}
S-1-5-21-... 同时可用于本地账户和域账户,正则匹配只是第一步,后续还要看身份解析结果。Hive 存在也不代表此刻存在可交互或可提取凭据的会话。此路径需要 Remote Registry 可用和相应键读取权限,--skipregistryloggedon 可跳过它。
指定本地管理员身份
--doLocalAdminSessionEnum 配合 --LocalAdminUsername、--LocalAdminPassword 时,网络会话/登录用户 API 先在指定身份的 LOGON_NEW_CREDENTIALS 模拟上下文中调用。该尝试失败后,代码再回退默认身份。
LDAP 显式凭据用于目录连接,与计算机 NetAPI / 注册表操作使用的 Windows 身份不应混为一谈。分别配置目录查询身份与主机查询身份,才能解释哪些方法成功、哪些返回访问拒绝。
相关 API:NetWkstaUserEnum。
Trusts —— 域信任关系
采集的 BloodHound 边:TrustedBy
实现原理(DomainTrustProcessor):通过 LDAP 查询 trustedDomain 对象的方向、属性和目标 SID;另外尝试用 .NET Domain API 补充信任类型。SidFilteringEnabled、TGTDelegationEnabled 等由读取到的信任属性推导。
输出数据的结构:
– TargetDomainName:目标域名
– TargetDomainSid:目标域 SID
– TrustDirection:信任方向(Inbound / Outbound / Bidirectional)
– TrustType:信任类型(ParentChild / TreeRoot / External / Forest / …)
– TrustAttributes:信任属性(Forest Transit、Quarantined Domain 等)
– SidFilteringEnabled:SID 过滤状态
默认目录权限下,普通域用户通常可读取相关 trustedDomain 属性。
LocalGroups:本地组与远程访问关系
LocalGroups 是 DCOM | RDP | LocalAdmin | PSRemote 的组合。客户端只要选中其中的本地组方法,就会调用 LocalGroupProcessor.GetLocalGroups(),通过 SAMR 枚举域和别名,再读取成员。本版本处理器返回的是 LocalGroups 数据集合,并非为每个标志各调用一套独立 API。
过程是打开目标 SAM server,枚举 Builtin / 本地 SAM 域,打开别名并查询成员,再将成员 SID 解析为目录对象或目标本地身份。每个结果也带采集状态与失败原因。
常见内置组及分析含义:
| 组 | SID | 提供的线索 |
|---|---|---|
| Administrators | S-1-5-32-544 |
本地管理组成员 |
| Remote Desktop Users | S-1-5-32-555 |
RDP 登录授权分析的一部分 |
| Distributed COM Users | S-1-5-32-562 |
DCOM 访问/激活授权分析的一部分 |
| Remote Management Users | S-1-5-32-580 |
WinRM / PowerShell 远程管理授权分析的一部分 |
组成员信息和实际连接授权有联系,但服务是否开启、网络可达性、登录拒绝策略、COM 类 ACL、WinRM endpoint 权限还会检查各自条件。例如加入 Remote Desktop Users 后,拒绝远程交互登录策略仍可阻止 RDP。
SAMR 远程访问受 RestrictRemoteSAM 等策略限制。GPO 推导的本地组数据可作为另一来源,真实主机查询则更接近当前成员状态。
SPNTargets:MSSQL 服务账户与主机
本版本 SPNProcessors.ReadSPNTargets() 读取用户的 servicePrincipalName,筛选包含 mssqlsvc 的 SPN,解析主机和端口,然后输出 SPNPrivilege:
yield return new SPNPrivilege {
ComputerSID = host,
Port = port,
Service = EdgeNames.SQLAdmin
};
未指定可解析端口时使用 1433。主机部分交给 ResolveHostToSid(),成功解析到计算机 SID 后才输出。HTTP/web.domain.com 等其他服务 SPN 不进入这条 SQL 分支。
这项数据从目录中的服务归属推导账户与 SQL 主机关系,没有登录 SQL Server 枚举实际数据库角色。Kerberoasting 则关注服务票据的加密材料与账户密码强度,读取所有 SPN 属性和这里的 SQL 目标解析承担不同用途。
源码:SPNProcessors。
ObjectProps:目录属性与委派数据
LdapPropertyProcessor 将原始 LDAP 属性转为输出字段。这里既有时间、系统版本和账户标志,也有需要进一步解析的委派目标、安全描述符和 SID History。
| 对象 | 原始数据 | 处理结果 |
|---|---|---|
| User / Computer | userAccountControl、密码/登录时间、sIDHistory |
账户状态、委派标志、时间及历史 SID |
| User / Computer | msDS-AllowedToDelegateTo |
保留委派目标 SPN,并将可解析主机映射为目录主体 |
| Computer | msDS-AllowedToActOnBehalfOfOtherIdentity |
解析其中的安全描述符及主体,输出 AllowedToAct |
| gMSA | msDS-GroupMSAMembership |
可读取托管密码的授权关系线索 |
| Computer | LAPS 过期时间属性、operatingSystem |
haslaps 及系统信息 |
| Domain | 功能级别、口令策略、配额等属性 | 域配置 |
| GPO | displayName、gPCFileSysPath |
GPO 名称与文件路径 |
haslaps 由 LAPS 相关过期时间等属性判断,常规采集并不把 LAPS 明文密码写入图数据。密码读取/解密是另一个操作,仍需相应授权。
对于 RBCD,当前处理器遍历属性内的访问规则并解析 SID,没有完整模拟 KDC 对该描述符执行的访问检查。分析 AllowedToAct 时,还需核对 ACE 类型、权限掩码及可用前端身份,具体授权过程见《委派攻击》。
adminCount 反映目录中的标志,可能在主体离开受保护组后残留;账户当前是否仍受保护,不能只凭这个字段判断。
Container —— 容器与 OU
采集内容:对象的容器/OU 层级关系(ContainedBy 边)和 GPO 链接。
实现原理(ContainerProcessor):
– 从对象的 distinguishedName 中提取父容器 DN
– 解析父容器的对象标识及类型,填写 ContainedBy;OU、GPO、普通 Container 等通常使用 objectGUID,安全主体多使用 SID
– 对 Domain 和 OU 对象,读取 gPLink 属性解析链接的 GPO 列表
– 读取 gpoptions 判断是否启用”阻止 GPO 继承”
利用条件:域用户默认可读。
GPOLocalGroup —— GPO 本地组
采集内容:从组策略文件推导本地组变更,包括 Restricted Groups 和 Group Policy Preferences 的组配置。
实现原理(GPOLocalGroupProcessor):
– 读取 Domain/OU 的 gPLink 属性
– 定位到关联的 GPO
– 通过 GPO 的 gPCFileSysPath 定位 SYSVOL 中的 XML 文件
– 解析 Machine\Microsoft\Windows NT\SecEdit\GptTmpl.inf 中的 Restricted Groups
– 解析 Machine\Preferences\Groups\Groups.xml 中的添加、删除、替换等动作,按处理器的顺序合并
安全意义:当远程 SAMR 查询受限时(如在 Stealth 模式下),GPO 本地组提供了一种替代数据来源。因为 GPO 配置的本地组成员信息存储在 SYSVOL 中(域控的 SMB 共享),可以通过 DC 的 LDAP / SYSVOL 读取,不必为此向每台成员机查询 SAMR。GPO 数据描述策略意图,安全筛选、WMI 筛选、策略应用状态和本地手动修改仍可能使主机当前状态不同。

UserRights:远程交互登录权限
UserRightsAssignmentProcessor 通过 LSA policy 接口查询指定 User Right 的主体,底层过程为打开策略、取得本地 SID 信息、调用 LsaEnumerateAccountsWithUserRight,再解析 SID。
本版本的默认列表在 LSAPrivileges.DesiredPrivileges 中只有一项:
public static readonly string[] DesiredPrivileges = {
RemoteInteractiveLogon // SeRemoteInteractiveLogonRight
};
所以客户端默认的 UserRights 采集提供“允许通过远程桌面服务登录”的授权数据。处理器接口支持传入其他权利名称,但客户端这里没有展开所有 SeDebug / SeBackup 等特权。
RDP 判断还需结合拒绝远程交互登录、组成员、服务与 endpoint 条件。LSA 读取也要通过目标 policy 的访问检查,常见配置需要管理员级别权限。
源码:UserRightsAssignmentProcessor、默认查询列表。
CertServices / CARegistry —— ADCS 证书服务
采集内容:Active Directory Certificate Services 相关数据。这些数据供 BloodHound 结合模板、CA、信任及账户关系分析证书路径。采集结果与图分析规则分别实现,不能从属性数量推成完整覆盖某一串 ESC 编号。
CertServices 采集
对以下 ADCS 对象读取所选属性并处理 ACL:
| 对象类型 | LDAP 位置 | 关键信息 |
|---|---|---|
| RootCA | Configuration NC, cn=Certification Authorities |
根 CA 证书 |
| AIACA | Configuration NC, cn=AIA |
中间 CA 证书 |
| EnterpriseCA | Configuration NC, cn=Enrollment Services |
注册端点 URL、启用的证书模板 |
| NTAuthStore | Configuration NC, cn=NTAuthCertificates |
NTAuth 证书指纹 |
| CertTemplate | Configuration NC, cn=Certificate Templates |
EKU、注册权限、SAN 标志 |
| IssuancePolicy | Configuration NC, cn=OID |
颁发策略 OID |
证书模板的关键属性:
| 属性 | 含义 |
|---|---|
msPKI-Certificate-Name-Flag |
请求方名称 / SAN 等命名行为 |
msPKI-Enrollment-Flag |
注册处理标志,包括待审批等设置 |
msPKI-RA-Signature |
所需授权签名数量 |
pKIExtendedKeyUsage、msPKI-Certificate-Application-Policy |
证书用途 / 应用策略 |
msPKI-RA-Application-Policies |
注册代理签名的应用策略要求 |
msPKI-Private-Key-Flag |
密钥相关选项 |
| 模板 DACL | 注册及修改等权限 |
模板可由请求者提供 SAN,还要结合认证用途、注册权限、审批/签名要求,以及 CA 和 KDC 的检查,才能分析身份冒用路径。允许导出私钥本身并非 ESC1 的独立判据;ESC6 关注 CA 策略模块的 SAN 处理,ESC11 则关注 RPC 注册接口的保护要求。
EnterpriseCA 采集(ObjectProcessors.cs):
– 通过 DNS 解析 CA 服务器的主机 SID -> 建立 HostingComputer 关系
– HttpEnrollmentEndpoints:CA 的 HTTP 注册端点(通过 CAEnrollmentProcessor 扫描)
– EnabledCertTemplates:该 CA 启用的证书模板列表
CARegistry 额外采集
通过直接连接到 CA 服务器,读取注册表中的权威配置:
// ObjectProcessors.cs
CARegistryData cARegistryData = new() {
IsUserSpecifiesSanEnabled = await _certAbuseProcessor.IsUserSpecifiesSanEnabled(...),
EnrollmentAgentRestrictions = await _certAbuseProcessor.ProcessEAPermissions(...),
RoleSeparationEnabled = await _certAbuseProcessor.IsRoleSeparationEnabled(...),
CASecurity = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(...)
};
代码注释中特别强调了设计决策(ObjectProcessors.cs):
CASecurity同时存在于 AD 对象的 DACL 和 CA 服务器注册表中。我们优先使用注册表中的值,因为它们是权威数据(ground truth)。如果在 CA 服务器上做了更改,注册表和 AD 对象都会更新。但如果直接修改 AD 对象,CA 服务器注册表不会更新。
利用条件:
– Configuration NC 的 LDAP 读取(域用户默认可读)
– CARegistry 需要到 CA 服务器的管理员级网络连接
其他计算机检查方法
DCRegistry(仅对域控制器)
// ObjectProcessors.cs
DCRegistryData dCRegistryData = new() {
CertificateMappingMethods = _dcRegistryProcessor.GetCertificateMappingMethods(apiName),
StrongCertificateBindingEnforcement = _dcRegistryProcessor.GetStrongCertificateBindingEnforcement(apiName),
VulnerableNetlogonSecurityDescriptor = _dcRegistryProcessor.GetVulnerableNetlogonSecurityDescriptor(apiName)
};
CertificateMappingMethods 和 StrongCertificateBindingEnforcement 提供证书到账户映射的配置线索;VulnerableChannelAllowList 则涉及 Netlogon 通道策略。字段缺失时还要结合系统版本及对应默认行为解释。
LdapServices(仅对域控制器)
// ObjectProcessors.cs
var ldapServices = await dcLdapProcessor.Scan(...);
ret.Properties.Add("ldapavailable", ldapServices.HasLdap);
ret.Properties.Add("ldapsavailable", ldapServices.HasLdaps);
ret.Properties.Add("ldapsepa", !ldapServices.IsChannelBindingDisabled.Result); // EPA = !Disabled
ret.Properties.Add("ldapsigning", ldapServices.IsSigningRequired.Result);
扫描结果先保留 HasLdap / HasLdaps 可达性及每项测试的 Collected、FailureReason。签名和 CBT 只是中继保护条件的一部分,字段测试失败时不能按“未启用保护”解释。
SmbInfo
SmbProcessor 执行 TCP 445 的 SMB 协商,读取签名要求。本版本将扫描结果 SigningRequired 写入 SmbInfo.SigningEnabled,所以读取此字段时应按“是否要求签名”解释,并先检查 Collected 是否成功。
WebClientService
尝试打开 \\<host>\pipe\DAV RPC SERVICE,根据结果提供 WebClient 服务运行线索。WebClient 可提供 WebDAV / HTTP 认证入口;EFSRPC 触发普通 SMB 回连并不以它为统一前提。
NTLMRegistry
通过远程注册表读取目标计算机的 NTLM 相关安全设置。
Stealth 与方法范围
--stealth 在 Options.ResolveCollectionMethods() 中修改所选标志,并将变更写入日志。它移除 LoggedOn、本地组四项、CARegistry、DCRegistry 和 NTLMRegistry;移除了本地组方法时,再加入 GPOLocalGroup。
源码先检查某一位已存在才执行异或清除,下面用按位与表示等价的方法筛选:
// 方法筛选关系示意
resolved &= ~(CollectionMethod.LoggedOn
| CollectionMethod.LocalGroups
| CollectionMethod.CARegistry
| CollectionMethod.DCRegistry
| CollectionMethod.NTLMRegistry);
if (localGroupRemoved) {
resolved |= CollectionMethod.GPOLocalGroup;
}
Session、SmbInfo、WebClientService、LdapServices 等未在这组移除项中。CollectionTask 随后选择 StealthProducer,它还尝试从路径属性与 DC 对象构造候选主机集合。
本客户端快照中有两个会影响范围的实现细节:
BuildStealthTargets()填充StealthContext,但主查询仍按_query生产结果,消费者及主机处理路径没有调用IsSidStealthTarget()来按该集合过滤。因此不能将候选集合直接当作最终访问主机列表。ProduceConfigNC()检查了配置分区过滤器,却使用_query/_props发起查询,而非_queryConfigNC/_propsConfigNC。证书等配置分区结果的完整性需要特别核对。
这些行为可直接对照 StealthProducer 和客户端消费者。此快照的 Stealth 主要提供方法筛选,剩余方法仍可能访问成员主机。
DCOnly 也需要沿具体处理器判断。它包含 GPOLocalGroup,需要读取 SYSVOL;还包含 CertServices,ProcessEnterpriseCA() 会对 CA 主机调用 CAEnrollmentProcessor.ScanAsync() 探测 HTTP 注册端点,CA 可能是成员服务器。若要明确限定目录查询方法,可直接列出 Group,ACL,ObjectProps,Container,Trusts,再检查身份解析可能产生的辅助连接。
Loop 循环采集
Loop 模式(-l / --Loop)允许 SharpHound 在基础采集完成后,循环执行仅针对计算机的采集(LoopManager.cs):
// LoopManager.cs
while (!_context.CancellationTokenSource.IsCancellationRequested) {
_loopCount++;
if (time >= _loopEndTime) break; // 超时退出
_context.UpdateLoopTime(); // 更新时间戳,用于输出文件名
var task = new CollectionTask(_context).StartCollection();
var filename = await task;
_filenames.Add(filename);
await Task.Delay(_context.LoopInterval, ...); // 间隔等待
}
// 所有循环结束后,合并 ZIP
var zipName = ZipFiles();
关键参数:
– --LoopDuration:总循环时长(格式 hh:mm:ss,默认值 2 小时)
– --LoopInterval:每轮间隔(格式 hh:mm:ss,默认值 30 秒)
基础采集完成后,SharpLinks.StartLoop() 调用 GetLoopCollectionMethods(),只保留原始选择与以下方法的交集:
// src/Extensions.cs,省略方法外层定义
const CollectionMethod computerCollectionMethods =
CollectionMethod.LocalGroups | CollectionMethod.LoggedOn |
CollectionMethod.Session;
return methods & computerCollectionMethods;
会话和主机组成员会随时间变化,循环用于补充不同时间点的观察。目录 ACL 与组成员同样可能变化,只是本实现没有在 Loop 中重采它们。原始方法若未包含 Session / LoggedOn / LocalGroups,Loop 不会自动添加这些方法。每轮完成后再等待 LoopInterval,因此两轮开始时间的间隔还包含上一轮采集耗时。
关键基础设施:缓存系统
SharpHound 的缓存系统(Cache.cs)是其性能优化的核心,避免了对同一 SID/账户名的重复 LDAP 查询。
缓存结构
| 缓存字典 | 键 | 值 | 生命周期 | 作用 |
|---|---|---|---|---|
IdToTypeCache |
SID 字符串或 GUID | Label 枚举 | 内存+磁盘 | 确定 SID 对应的对象类型(User/Computer/Group 等) |
ValueToIdCache |
{name}|{domain} |
ObjectSID | 内存+磁盘 | 账户名到 SID 的映射 |
MachineSidCache |
AD 计算机对象标识 | 主机本地 SAM 的账户域 SID | 内存+磁盘 | 复用主机本地 SAM SID 以识别本地身份 |
SIDToDomainCache |
SID 或域名 | 域名或 SID | 内存+磁盘 | SID<->域名 双向映射 |
GlobalCatalogCache |
账户名 | SID 数组 | 内存+磁盘 | 全局编录账户名匹配结果 |
_hostResolutionMap |
主机名 | SID | 仅内存 | 主机名到 SID 的映射(不持久化) |
_distinguishedNameCache |
DN 字符串 | TypedPrincipal | 仅内存 | DN 到主体的映射(不持久化) |
_unresolvablePrincipals |
SID/DN | — | 仅内存 | 黑名单,避免重复查询已知不可解析的主体 |
缓存持久化
// SharpLinks.cs
public IContext SaveCacheFile(IContext context) {
if (context.Flags.MemCache) return context; // 结束时跳过缓存保存
var cache = Cache.GetCacheInstance();
var serialized = JsonConvert.SerializeObject(cache, CacheContractResolver.Settings);
using var stream = new StreamWriter(context.GetCachePath()); // 默认路径: <output>/<machine_id>.bin
stream.Write(serialized);
return context;
}
默认名称由 ClientHelpers.GetBase64MachineID() 读取注册表 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid 后编码生成;读取失败时回退机器名。文件扩展名为 .bin,内容是 JSON。
加载缓存
// SharpLinks.cs
if (!File.Exists(path)) {
cache = null; // 缓存文件不存在,创建新缓存
} else try {
var json = File.ReadAllText(path);
cache = JsonConvert.DeserializeObject<Cache>(json, ...);
} catch {
cache = null; // 反序列化失败,丢弃旧缓存
}
CommonLib.InitializeCommonLib(context.Logger, cache);
缓存参数与实际读写
SaveCacheFile() 检查 MemCache,启用时跳过保存。InitCommonLib() 仍会尝试读取现有路径,所以在该快照中 --memcache 控制的是结束时不写缓存,不能据此判断启动时没有读取旧缓存。
--rebuildcache 被解析并写入 Flags.InvalidateCache,但当前 InitCommonLib() 未使用该标志决定是否读取文件。需要新的解析起点时,可以为本次实验指定新的 --cachename,避免将参数帮助中的意图直接当作已执行的缓存清空。
--cachename 控制文件名,输出目录共同决定最终路径。
输出、权限与结果解释
JSON 与采集状态
OutputWriter 按对象类型分发到对应 JsonDataWriter,分别写 Users、Computers、Groups、Domains、OUs、GPOs 及证书对象等文件,再压缩输出。JSON 包含 data 数组与 meta;该客户端 JsonDataWriter.DataVersion 为 6,meta 还记录对象数、方法位掩码和采集器版本。
这里收集的是对象及其关系字段。BloodHound 导入和后续图分析再建立关系、组合路径,某条字段也可能只是更复杂推断的一项输入。
主机查询通常返回:
public class APIResult {
public bool Collected { get; set; }
public string FailureReason { get; set; }
}
会话结果还包含 Results,单值检查包含 Result。解释顺序为:该项是否被选中、目标是否进入处理、Collected 是否成功,再读实际结果。这样才能区分“查询成功但结果为空”和“未采集 / 超时 / 访问拒绝”。
例如一个本地组没有成员记录,可能来自 SAMR 拒绝访问;WebClient 的 Result 为默认值,也可能同时伴随 Collected=false。导出文件中存在一个计算机对象,并不说明该计算机的所有方法都执行成功。
目录身份与主机身份
| 数据来源 | 连接与权限 |
|---|---|
| 域对象 / Schema / Configuration | 到 DC 的 LDAP / LDAPS,使用允许读取相应属性的身份 |
| 跨域与全局编录解析 | 相应域、GC 与 DNS 可访问,且目标对象可见 |
| GPO 文件 | SYSVOL 的 SMB 文件读取 |
| Session / LoggedOn | 被查询主机的 NetAPI/RPC 权限 |
| 本地组 / UserRights | 目标 SAMR / LSA 策略访问权限 |
| 远程注册表 | Remote Registry 和对应 hive/key 的读取权限 |
| CA 端点 | 到 CA 的 HTTP / HTTPS 等对应服务 |
--ldapusername / --ldappassword 配置目录连接,主机原生 API 通常使用进程的 Windows 安全上下文,或指定的本地管理员模拟身份。在未加域的 Windows 上使用时,需要明确域、DC/DNS、目录凭据与主机查询身份;只提供 LDAP 密码不会自动让所有原生 API 使用该账户。
.NET Framework 4.7.2 是此客户端的运行要求。代码依赖 Windows 原生 API,Linux 侧收集应使用对应平台采集器,并分别核对其功能与输出兼容性。
采集参数的作用位置
--threads 设置消费者任务数。--throttle / --jitter 在调用了 DoDelay() 的计算机处理步骤之间加入延迟,影响速度与请求分布;LDAP Producer、身份解析和其他检查还有自己的调用过程。
--skipportcheck 跳过可用性检查中的预探测,后续实际 API 连接仍需成功。--disablesigning 修改 LDAP 连接的签名/加密选项,若目标要求 LDAP 消息保护,会使连接失败;它没有关闭系统审计或认证保护的权限。
--excludedcs 在此快照被用于构造 Stealth 候选目标的分支,没有成为所有主机处理路径的统一过滤器。采集范围要根据解析后的方法与实际调用判断。
观察采集过程
日志中的 resolved methods、主机状态输出、方法失败原因和 JSON meta 可以互相对照。网络侧可观察目录查询、SMB 命名管道、注册表访问和 CA 端点探测,主机侧再结合已有的服务审计与传感器数据。
Windows 安全事件不会仅因调用某个枚举 API 就固定产生某一条日志。目录服务访问和对象访问记录还涉及审计策略与 SACL;例如不能把每次 NetSessionEnum 简单等同于一条 4663。判断采集行为时,应结合接口、时间、来源身份、目标范围及相关记录。
源码文件索引
本文分析所涉及的关键源码文件及其职责:
SharpHound 主项目
| 文件 | 职责 |
|---|---|
src/Sharphound.cs |
程序入口点,CLI 参数解析,驱动整个执行流程 |
src/Options.cs |
所有命令行选项定义,CollectionMethod 解析与 Stealth 转换 |
src/SharpLinks.cs |
责任链实现类,串联初始化、域发现、采集、循环和结束 |
src/BaseContext.cs |
共享上下文,包含延迟/抖动逻辑、文件路径解析 |
src/Client/Links.cs |
责任链接口 Links<T> 定义 |
src/Client/Context.cs |
Context 接口 IContext 定义和工具方法 |
src/Client/Flags.cs |
运行时所有布尔标志位 |
src/Client/Enums.cs |
CollectionMethodOptions 枚举(CLI 字符串到内部枚举的映射) |
src/Producers/BaseProducer.cs |
Producer 抽象基类,定义 Produce() 和 ProduceConfigNC() 接口 |
src/Producers/LdapProducer.cs |
默认 LDAP Producer,执行 Default NC + Configuration NC 两阶段查询 |
src/Producers/StealthProducer.cs |
Stealth 模式 Producer |
src/Runtime/CollectionTask.cs |
采集任务编排器:创建 Channel、启动 Producer/Consumer/Writer |
src/Runtime/ObjectProcessors.cs |
对象处理分发器:根据 Label 将对象路由到对应处理器 |
src/Runtime/LoopManager.cs |
循环模式管理器:循环采集+ZIP 合并 |
src/Runtime/LDAPConsumer.cs |
独立消费者相关类型;当前主采集循环在 CollectionTask.ConsumeSearchResults |
src/Runtime/OutputWriter.cs |
JSON 输出写入器 |
src/Writers/BaseWriter.cs |
Writer 抽象基类 |
src/Writers/JsonDataWriter.cs |
JSON 格式写入器 |
SharpHoundCommon 公共库
| 文件 | 职责 |
|---|---|
src/CommonLib/LDAPUtils.cs |
LDAP 操作核心:SID / GUID / DN 及主机身份解析、Forest/Domain 查询 |
src/CommonLib/Cache.cs |
多级持久化缓存系统(SID<->类型<->域名<->账户名) |
src/CommonLib/ConnectionPoolManager.cs |
多域 LDAP 连接池管理 |
src/CommonLib/LdapConnectionPool.cs |
单域连接池:连接复用、故障检测、自动重连 |
src/CommonLib/LdapProducerQueryGenerator.cs |
根据 CollectionMethod 动态生成 LDAP 查询(Filter + Properties) |
src/CommonLib/Enums/CollectionMethod.cs |
[Flags] 枚举,定义所有收集方法及其组合 |
src/CommonLib/Processors/ACLProcessor.cs |
ACL 解析核心:Schema GUID 缓存、ACE 解析、继承检测、AdminSDHolder 保护检测 |
src/CommonLib/Processors/ComputerSessionProcessor.cs |
会话采集:NetSessionEnum、NetWkstaUserEnum、Registry Session |
src/CommonLib/Processors/GroupProcessor.cs |
组成员关系:member 属性读取、PrimaryGroupID 处理、Ranged Retrieval |
src/CommonLib/Processors/DomainTrustProcessor.cs |
域信任关系枚举 |
src/CommonLib/Processors/CertAbuseProcessor.cs |
ADCS 攻击路径分析:模板解析、CA 安全配置和注册权限 |
src/CommonLib/Processors/LocalGroupProcessor.cs |
SAMR 本地组枚举 |
src/CommonLib/Processors/GPOLocalGroupProcessor.cs |
GPO 本地组首选项解析 |
src/CommonLib/Processors/UserRightsAssignmentProcessor.cs |
LSA 用户权限分配枚举 |
src/CommonLib/Processors/SPNProcessors.cs |
SPN 处理 |
src/CommonLib/Processors/ContainerProcessor.cs |
OU/容器层级关系 |
src/CommonLib/Processors/LdapPropertyProcessor.cs |
LDAP 属性解析(对象属性、账户标志和委派关系) |
src/CommonLib/Processors/DCRegistryProcessor.cs |
域控注册表安全配置读取 |
src/CommonLib/Processors/RegistryProcessor.cs |
通用注册表远程读取(NTLM 设置等) |
src/CommonLib/Processors/SmbProcessor.cs |
SMB 签名状态检测 |
src/CommonLib/Processors/WebClientServiceProcessor.cs |
WebClient 服务状态检测 |
src/CommonLib/Processors/DCLdapProcessor.cs |
DC LDAP 服务扫描(签名/EPA 状态) |
src/CommonLib/Processors/CAEnrollmentProcessor.cs |
CA HTTP 注册端点扫描 |
src/SharpHoundRPC/(多个文件) |
SAMR、LSA、注册表、端口扫描等 RPC 通信的底层封装 |