去年在某个授权内网测试环境里,我盯着一台屏幕右下角的时间从凌晨一点跳到三点。BloodHound 跑完采集,普通域账号 user_a 对业务账号 user_b 挂着一条 GenericWrite 边。组里新来的小哥第一反应是“这不就是能改几个属性吗,先放一放”,我没放。当晚按着这条边一步步往下走,拿到了 user_b 的口令;第二天换了一条链,用同一类权限直接拿下一台服务器的机器账户。事后复盘,我们最大的收获不是某个高级漏洞,而是重新认识了 GenericWrite 这个“看起来平平无奇”的 AD 权限。
GenericWrite 的全称是通用写权限,它允许持有者对目标目录对象上的大部分非安全属性执行写操作。很多内网安全测试手册把它放在“低危权限”的分类里,实际在真实域环境里,它往往比 Full Control 更危险:Full Control 太扎眼,GenericWrite 分布广、容易遗留,而且一旦落到用户、计算机或组对象上,能撬动的攻击链并不比完全控制少。这篇文章,我打算把 GenericWrite 的利用思路、两条可落地的完整链路、实测踩坑记录,以及防御侧的检测与收敛方案一次性写清楚。适合正在做域渗透测试的工程师,也适合想给自己 AD 环境做一次深度加固的管理员。
1. 认识 GenericWrite:一枚被大多数人忽略的 AD 权限
1.1 一个你很可能见过的内网场景
某公司 AD 环境里有一个中大型 OU,里面有三四百个用户。某个业务系统上线时,运维为了简化权限管理,把一批“可以维护用户信息”的接口账户授予了部分用户对象的 GenericWrite。几年下来,这类关系越来越多:应用初始化账号、密码重置自助平台、身份同步服务,都可能是 GenericWrite 的来源。
在真实环境里做 ACL 基线检查时,弹出的 GenericWrite 数量往往远多于 GenericAll 或 Full Control。攻击者拿到任意一个普通域账号后,BloodHound 一采样,就会看到大量可利用的边。GenericWrite 的出现频率高,但很多防守方根本不知道这类边怎么被利用,这才是它被低估的原因。我见过有些企业级扫描器只报告“Critical: GenericAll / WriteDACL / WriteOwner”,GenericWrite 就这么被埋没在“低风险”列表里。
1.2 GenericWrite 在 AD 权限模型中的真实含义
先把基础讲清楚。AD 访问控制基于 ACL 与 ACE,每个对象的安全描述符里有一组 ACE,规定某主体能对该对象做什么。GenericWrite 不是“能写任意东西”,它对应的是写属性(WriteProperty)等一组标准权限的集合,能修改目标对象上绝大多数非安全属性,但通常不包括写 DACL、改所有者、删除对象或重置密码这种扩展权限。
这里的关键认知是:AD 里的“非安全属性”本身就是武器库。servicePrincipalName、userAccountControl、scriptPath、委派属性全都是非安全属性,而它们恰恰决定了身份认证与信任关系的走向。
| 目标对象 | 可写属性 | 后续利用 |
|---|---|---|
| 用户 | servicePrincipalName | 定向 Kerberoast |
| 用户 | userAccountControl | AS-REP Roast / 敏感标志修改 |
| 用户 | scriptPath / script | 下次登录执行脚本 |
| 计算机 | msDS-AllowedToActOnBehalfOfOtherIdentity | RBCD 接管 |
| 组 | member | 把自己加进组 |
| GPO 容器 | gPCFileSysPath / 脚本相关属性 | 修改组策略路径 |
很多人以为“写属性”和“完全控制”差得远。但只要想清楚一个前提——AD 里对象之间是相互信任的,属性就是信任的载体——你就会明白,能够修改一个用户账户的 SPN 和 UAC 标志,本质上就是能够操纵对方的身份认证逻辑。这不是概念上的危害,而是可操作的链条。
1.3 为什么实战中它比 Full Control 更值得关注
Full Control 通常在安全基线的重点关注清单里,一旦出现会很快被收掉。GenericWrite 不同,它散布广、表现形式隐蔽,很多第三方应用做权限委托时只勾选“写属性”,这一勾就留下了大量可利用入口。攻击链上,GenericWrite 可以非常“安静”地利用:只是改个 SPN 或属性,不是删除、不是加成员,很多 SIEM 规则根本没有覆盖这些属性名。
换句话说,越不显眼的权限,在真实攻防里活得越久。GenericWrite 的每一条利用链几乎都能转化为凭证或访问权,这才是它的真正价值。
2. 滥用思路梳理:拿到 GenericWrite 之后能撬动哪些关键属性
2.1 用户对象上的四个高价值入口
第一个入口是SPN 定向写入(Kerberoast)。servicePrincipalName标志服务账号,若目标没有 SPN 而你把它写上去,等于把目标变成可请求 TGS 的服务账户。随后用 GetUserSPNs 导出 TGS-REP 离线爆破。为什么要用这种“造 SPN”的玩法?因为域内服务账户的口令往往是固定且复用的,存在多个系统共用口令的常见问题,一旦爆破成功就是凭证落地。这也是所有 GenericWrite 滥用里成本最低、见效最快的方式。
第二个入口是UAC 标志与 AS-REP Roast。userAccountControl里有大量标志位,例如DONT_REQ_PREAUTH(不需要 Kerberos 预认证)。开启后,攻击者可以直接用 GetNPUsers 为这个用户请求 AS-REP,拿回一个用于离线爆破的加密时间戳。实操时尤其注意读取原来的 UAC 值再做加法,不要覆盖其他标志位。
第三个入口是scriptPath / script 登录脚本。把登录脚本属性指向攻击者控制的 UNC 路径,目标用户下次登录会自动执行脚本,得到的执行上下文就是目标用户本身。这个路径适合持久化或配合钓鱼场景,缺点是触发时机不一定可控。
第四个入口是委派相关属性,包括传统的msDS-AllowedToDelegateTo,以及后面重点要讲的msDS-AllowedToActOnBehalfOfOtherIdentity。前者能把一个账户变成“可委派账户”,配合已有凭证进一步伪造票据;后者是 RBCD 的入口,效果最猛。
2.2 计算机对象上的终极大招:RBCD 全链路
RBCD(Resource-Based Constrained Delegation)是 Windows Server 2012 后引入的委派模型,核心思想是“资源自己决定谁可以代表用户访问我”。实际生效属性是计算机对象上的msDS-AllowedToActOnBehalfOfOtherIdentity,里面放的是“被允许代表其他用户访问本机”的安全主体列表。
攻击者拿到某台电脑的 GenericWrite 后,可以往这个属性里塞入一个自己控制的计算机账户。之后这个被控制的计算机账户可以通过 S4U2Self + S4U2Proxy 扩展,以任意用户(包括域管理员)的身份请求访问该电脑对应 SPN 的服务票据。拿到域管身份的 CIFS 票据后,这台电脑基本就等于交到了攻击者手里。
这里插一个生活化类比:可以想象成图书馆的借书权限。正常情况下读者要用自己的卡才能借书,图书馆(资源)列了个“可代办名单”,名单上的人可以用任何身份帮你借书。GenericWrite 就是让你能修改这个名单——你把自己的人写进名单,后面想借哪本、用什么身份借,都由你说了算。
RBCD 之所以是首选,是因为它不需要破解任何口令,也不依赖目标用户是否在线,只要你能在计算机对象上写一个属性。整个过程在 Server 2012+ 域环境里非常稳定,因此成了内网横向移动里最常用的一招。
2.3 组对象与容器对象:从“写属性”到“加成员”
如果 GenericWrite 落在安全组对象上,还能直接操作member属性,把当前受控的账户加进去。比如加进某个“服务器本地管理员”委托组、某个业务权限组,横向价值瞬间扩大。落在容器对象(OU / user container)上时,作用通常是影响该容器下的子对象创建与 ACL 继承,利用复杂度略高,但也值得在测绘时一并关注。
GPO 对象上的相关属性如果可写,还能把组策略的脚本路径指向攻击者的 SYSVOL 目录,实现整批机器下的命令执行。这类变形攻击比较少见于入门教程,却是实战中扩展战果的好思路。所以拿到一条 GenericWrite 边,先别急着固化某一种利用方式,把目标对象的类型和可写属性列出来,往往能找到更大的操作空间。
3. 两条完整利用链路的实操演示
3.1 前置准备:用 BloodHound 完成权限测绘
在进入具体操作前,先把目标环境的权限关系摸清楚。我习惯用 SharpHound 和 bloodhound-python 双管齐下:前者适合在拿到图形界面或移动端上线时跑,后者适合直接用口令从跳板机采样。采集完成后导入 BloodHound,用查询:
MATCH p=(n)-[r:GenericWrite]->(m) RETURN p也可以限定关注节点,比如只看当前控制账户对敏感组的 GenericWrite:
MATCH p=(n)-[r:GenericWrite]->(m) WHERE m.name IN ['DOMAIN ADMINS','ENTERPRISE ADMINS'] RETURN p对每条边,我还会用 PowerView 确认具体 ACE:
Get-DomainObjectACL -Identity target_user -ResolveGUIDs | Where-Object { $_.ActiveDirectoryRights -match "GenericWrite" -and $_.SecurityIdentifier -eq "S-1-5-21-..." }这一步的意义不只是确认权限,而是要拿到对象 SID 和当前环境的域名、DC IP、LDAP 策略等参数,这些在后面的利用命令里全是必填项。BloodHound 会告诉你哪里有路,但具体参数还得自己核对,别偷懒。
3.2 链路 A:GenericWrite 用户 → 定向 Kerberoast → 口令破解
场景设定:当前用户alice对目标用户svc_report拥有 GenericWrite。
先看一下svc_report当前的 SPN,避免误伤已有配置:
Get-DomainUser -Identity svc_report -Properties servicePrincipalName如果为空,写入一个自定义 SPN:
Set-DomainObject -Identity svc_report -Set @{serviceprincipalname="http/ops"}然后用 Impacket 请求对应 TGS:
python3 GetUserSPNs.py -request -dc-ip 10.0.0.10 -outputfile tgshash.txt 'lab.lab/alice:P@ssw0rd'这一步导出的是svc_report的 TGS-REP 哈希。回到本地用 hashcat 爆破,模式-m 13100。爆破成功即拿到svc_report明文口令,还可以继续尝试与其他系统的口令复用。如果目标用户口令强度较弱,这条链路效率极高。完毕之后,把 SPN 清理掉:
Set-DomainObject -Identity svc_report -Clear serviceprincipalname这里给个提示:我不建议在未授权环境修改真实业务系统属性,所有操作都应提前跟目标方确认窗口。这条链路的代价相对可控,但在有心人盯着的环境里,Event 5136 会记录属性修改。
3.3 链路 B:GenericWrite 计算机 → 构建虚假计算机 → RBCD 接管
场景设定:当前用户alice对文件服务器FS01$拥有 GenericWrite。
RBCD 利用需要三样东西:一个自己控制的计算机账户、目标计算机对象、目标计算机的 SPN(例如CIFS/FS01.lab.lab)。
第一步,创建一个虚假计算机账户。域里普通用户默认最多能创建 10 台计算机账户,用 Impacket 直接创建:
python3 addcomputer.py -computer-name 'fake$' -computer-pass 'Password@123' -dc-ip 10.0.0.10 'lab/alice:P@ssw0rd'第二步,把虚假账户写进目标计算机的允许列表:
python3 rbcd.py -delegate-from 'fake$' -delegate-to 'FS01$' -action write -dc-ip 10.0.0.10 'lab/alice:P@ssw0rd'第三步,用虚假账户的密钥请求域管身份的服务票据:
python3 getST.py -spn 'cifs/FS01.lab.lab' -impersonate Administrator 'lab/fake$:Password@123' -dc-ip 10.0.0.10 export KRB5CCNAME=Administrator.ccache python3 secretsdump.py -k -no-pass FS01.lab.lab或者走 Rubeus 的 S4U 路线:
Rubeus.exe hash /password:Password@123 /user:fake$ Rubeus.exe s4u /user:fake$ /rc4:<rc4_hash> /impersonateuser:Administrator /msdsspn:cifs/FS01.lab.lab /ptt dir \\FS01.lab.lab\C$这里解释一下为什么选CIFSSPN:CIFS 服务票据对应 SMB 文件共享访问,拿下后可以直接读取目录、导出哈希,是最通用的入口。如果需要远程执行命令,可以再换HOST或HTTP的 SPN,配合 WinRM 等服务使用。
整个过程的实质是:你把不可能直接访问的文件服务器变成可访问,再把访问身份从普通用户升级到域管理员。期间任何密码都没有被破解,这就是 RBCD 让人又爱又怕的地方。
3.4 两条链路的横向扩展与影响比对
两条链路放在一起对比更直观:
| 对比维度 | 链路 A(用户 SPN/Kerberoast) | 链路 B(计算机 RBCD) |
|---|---|---|
| 目标对象 | 用户账户 | 计算机账户 |
| 是否需要爆破 | 需要离线爆破口令 | 不需要 |
| 触发条件 | 目标用户口令可破解 | S4U2Self 流程通畅即可 |
| 影响范围 | 拿到目标用户身份 | 拿到目标机器系统权限 |
| 利用痕迹 | SPN / UAC 属性修改 | 修改允许委派属性、新建机器账户 |
| 失败常见原因 | 口令强 / 系统策略禁止 RC4 | 虚假账户无 SPN / LDAP 签名限制 |
实战中我会把两条链路组合着用:先用链路 A 拿到目标用户口令,再通过该用户去寻找下一组 GenericWrite 边,或者直接对计算机账户做链路 B。权限像滚雪球一样变大,最后到达域管或关键服务器。
4. 实际踩坑记录:工具细节、报错排查与痕迹处理
4.1 工具组合里的“版本陷阱”
Impacket、PowerView、Rubeus、ADModule 这四个工具是这套打法的标配,但它们之间的版本和参数差异很容易坑人。Impacket 的rbcd.py、getST.py在不同版本里对 LDAP 连接方式、凭据格式要求不一样,老版本可能默认不启用 LDAPS,而新版本在强制 LDAP 签名开启的环境下会直接报错。PowerView 的Set-DomainObject依赖 ADModule 模块的加载顺序,如果只导入 PowerView 而没有先导入 ActiveDirectory 模块,某些命令会静默失败。
我的建议是:固定使用容器化或独立虚拟环境里的最新 Impacket,PowerView 只做查询用,需要写属性时优先考虑 ADModule 官方 cmdlet,避免 PowerShell 版本和模块兼容性带来的二义性。Rubeus 单文件部署,适合在 Windows 主机上快速 PTT,但要注意 .NET Framework 版本影响。
4.2 四个最容易翻车的环节
第一个坑是SPN 冲突。如果目标用户本来就有 SPN,你写入一个与其他账户重复的 SPN,Kerberoast 请求时 KDC 会返回KDC_ERR_S_PRINCIPAL_UNKNOWN。处理办法是尽量选择域内不存在的自定义 SPN 值,并在利用前先全量查询一遍。
第二个坑集中在 RBCD 的S4U2Self 环节。如果你新建的虚假计算机账户没有正确设置 SPN,或者TRUSTED_TO_AUTH_FOR_DELEGATION标志缺失,请求服务票据会失败。这时候用 PowerView 或 ADSI 给虚假账户手动补一个 SPN,或者把 userAccountControl 加上0x00800000,基本能解决大部分问题。但要注意,不同工具创建的机器账户 UAC 标志有差异,提前写好回滚值很重要。
第三个坑是LDAP 签名与 LDAPS。现在不少域环境强制 LDAP 签名,rbcd.py默认走 389 端口时容易被拒绝。用-method LDAPS或-ssl参数改成 636 端口,并确保网络可达。实验环境里我经常因为忘了开 LDAPS 白折腾半小时。
第四个坑是DC 复制延迟。属性写完后,如果你紧接着去另一个 DC 请求服务票据,可能因复制未完成而请求失败。等 15~30 秒,或者指定同一个 DC 进行写和读,是更省心的做法。
4.3 利用完成后的清理与隐蔽性思考
清理动作分三层:一是把改过的属性恢复原值,比如清除 SPN、恢复 UAC、删除msDS-AllowedToActOnBehalfOfOtherIdentity里的 SID;二是删除临时创建的计算机账户;三是处理本地票据缓存(klist purge或删除 ccache 文件)。
python3 rbcd.py -delegate-from 'fake$' -delegate-to 'FS01$' -action remove -dc-ip 10.0.0.10 'lab/alice:P@ssw0rd' python3 addcomputer.py -action delete -computer-name 'fake$' -dc-ip 10.0.0.10 'lab/alice:P@ssw0rd'隐蔽性方面,我通常会把属性修改分散到多个时间段,避免短时间连续出现几条 5136。新建机器账户这类高可见度动作,最好放在业务低峰期。这些都是授权测试里的纪律问题,不是教你对抗检测,而是提醒你在真实项目中留好日志、别影响业务。
5. 防御侧怎么做:检测特征与收敛方案
5.1 优先盯住四类变更事件
对防守方来说,不需要把 AD 里所有日志全量接入 SIEM,只要把几类关键变更事件拉出来重点盯,GenericWrite 滥用就藏不住。第一类是目录对象修改(5136),重点筛选servicePrincipalName、userAccountControl、scriptPath、msDS-AllowedToActOnBehalfOfOtherIdentity这些属性名的变更。第二类是用户/计算机账户变更(4738/4742),尤其是 userAccountControl 值与委派属性变化。第三类是新建计算机账户(4741),短时间大量新增机器账户非常可疑。第四类是 Kerberos 服务票据请求(4769),出现高频向少量 SPN 请求票据、票据加密类型突然从 AES 降级到 RC4 时,需要人工确认。
| 事件 ID | 名称 | 关键关注点 |
|---|---|---|
| 5136 | 目录服务对象修改 | 高危属性白名单匹配 |
| 4738 | 用户账户被修改 | userAccountControl / scriptPath 变化 |
| 4742 | 计算机账户被修改 | msDS-AllowedToActOnBehalfOfOtherIdentity 变化 |
| 4741 | 计算机账户创建 | 新机器账户数量与创建者 |
| 4769 | Kerberos 服务票据请求 | RC4 票据、异常 SPN、高频请求 |
| 4662 | 对象上的访问操作 | 开启 Directory Service Access 审计后可见细节 |
提示:只开默认的账户登录审计是不够的,Directory Service Access 审计默认关闭,需要先在组策略里启用“审计目录服务访问”,才能看到 4662 的详细操作对象。
5.2 从权限收敛到零信任模型
防御动作里最有效的其实是一件事:把“写属性”当成“管理身份”来管理。对用户对象、计算机对象、组对象这三类高危对象,任何业务账号都不应该拥有裸的 GenericWrite。第三方应用需要维护用户信息时,尽量使用受控的服务账号,并通过细化权限委派(只给特定属性写权限)而不是直接给 GenericWrite。
对于真正需要 SPN 的服务,优先使用 gMSA(组托管服务账户),由域自动轮换密码,攻击者即使拿到 GenericWrite 也很难通过 Kerberoast 拿到稳定口令。同时,把域管理员组、Protected Users 组和关键服务器的计算机对象纳入重点监控,任何非授权写操作立即告警。建议定期跑一遍 ACL 基线检查,把所有 GenericWrite 边列成清单,分别确认责任人。
5.3 红队视角下的一次事后复盘体会
那次授权项目结束后,我给客户做复盘时被问到一个问题:“你们花了多久真正打进内网?”我的回答是:“找到第一条 GenericWrite 边只花了半小时,但收敛方案我们讨论了一整天。”原因是 GenericWrite 背后的权限链路太容易被低估了。防守方如果把“写属性能改什么”逐条列出来,就会立刻明白:在一个存身份、存信任、存策略的目录系统里,写权限就是控制权,它不是低危。
从那以后,我在两种角色上都养成了同一个习惯:一是在做渗透测试时,BloodHound 采集结果里永远最先点开 GenericWrite 边;二是在做 AD 加固时,ACL 基线报告里永远先把 GenericWrite 列为高危项。这个小小的认知转变,带来的防守收益远比想象中要大。
如果你也想验证自己的环境,我建议你从今天开始把域内的 GenericWrite 权限完整拉一遍清单,挑出一条最长的从普通用户到域管的链路,站在攻击者视角推演一遍。你会发现,AD 里真正可怕的东西,往往不是那些写着“完全控制”的权限,而是那些看起来人畜无害、实则能递进式接管一切的“写权限”。