SharpHound 数据采集原理

SharpHound 从 AD 和 Windows 主机收集对象、权限、组成员及会话数据,输出 JSON 供 BloodHound 导入。理解图中的关系,需要先知道每项数据来自哪里、采集时使用什么权限,以及服务端真正返回了什么。

源码分析涉及客户端和公共库两个项目:

客户端的 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):

SharpHound 生产、处理与输出流水线

  • 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();               // 颁发策略

SharpHound 的命名上下文查询与分页过程

分页查询(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 列表)。处理流程:

  1. 对每个 DN,通过 LdapUtils.ResolveDistinguishedName() 查询获取其 ObjectSID
  2. 处理 Everyone、Authenticated Users 等 Well-Known SID 的特殊映射;Domain Admins 等域组则使用该域 SID 加相应 RID
  3. 对每个 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);
    }
  4. 使用 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 都可能影响路径,不能只看一条边就省去目标对象上的实际授权分析。

SharpHound 从安全描述符解析权限关系

SID 与对象类型解析

LDAPUtils.ResolveIDAndType() 先处理已知主体和不可解析缓存,再按 SID / GUID 进入不同分支。SID 分支的主要过程为:

  1. 查询 IdToTypeCache。
  2. 尝试由 SID 定位所属域,查询 objectSid。
  3. 用 LDAP://<SID=...> 直接绑定。
  4. 用 PrincipalContext / Principal.FindByIdentity 回退解析。
  5. 失败时加入不可解析集合,避免本轮持续重复查询。

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 上交互登录。

SharpHound 网络会话查询与来源主机

工具对结果过滤空用户名/计算机名、机器账户、采集者自身和匿名登录。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 推导的本地组数据可作为另一来源,真实主机查询则更接近当前成员状态。

源码:LocalGroupProcessor。

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 筛选、策略应用状态和本地手动修改仍可能使主机当前状态不同。

GPO 组配置推导与 SAMR 实际成员查询

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 通信的底层封装

参考来源

版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇