CTF Writeup写作心法与文件上传漏洞实战剖析
发布时间:2026/8/26 7:10:56 作者:尧图编辑部 阅读量:1,286

1. 从“赛后复盘”到“解题思路沉淀”一份WP的价值刚打完一场CTF比赛或者啃完一道折磨人的CTf题目第一件事是什么对我而言不是急着关掉虚拟机而是打开一个Markdown文档开始写“WP”——Writeup。这个词在CTF圈里太常见了它不是什么高深的技术缩写就是一份“解题报告”。你可能在网上搜过“xxx比赛 WP”、“xxx题目 WP”希望能找到通关秘籍。一份好的WP远不止是答案的罗列它更像是一位先行者留下的足迹和地图记录了完整的思考路径、踩过的坑、以及最终拨云见日的关键技巧。今天我就以一次虚构但典型的“WUST-CTF2020”赛后复盘为例和你聊聊怎么写一份对自己、对他人都有价值的WP这不仅是分享更是对自己技术能力的一次深度梳理和升华。一份有价值的WP目标读者其实有两个未来的自己以及社区里可能遇到同样困惑的同行。对前者它是防止遗忘的“备忘录”几个月后回看能迅速唤醒当时的思路对后者它是一种技术传承能帮助他人少走弯路。所以我的WP从不满足于“点击这里输入那个”而是力求讲清楚题目给了什么我最初是怎么想的为什么这个思路行不通转折点在哪里最终的核心原理是什么这个过程本身就是一次极佳的学习。2. WP的核心架构与写作心法写WP不是记流水账一个清晰的结构能让你的思路和读者的阅读体验都得到提升。经过这么多年的积累我形成了一套固定的WP结构模板它就像是一个 checklist确保我不会遗漏任何关键信息。2.1 标准WP结构模板我的WP通常分为以下几个部分你可以直接拿去用题目信息与链接记录比赛名称、题目名称、分类Web、Pwn、Reverse等、分值以及原始题目附件或访问地址。如果是线上题目最好注明比赛是否已结束或者提供存档的题目环境。解题摘要用一两句话概括题目的核心考点和最终利用的漏洞类型。比如“本题是一道基于PHP反序列化字符串逃逸的题目通过构造特定的序列化字符串修改关键属性值最终触发eval执行任意代码。”环境准备列出解题所需的基本工具和调试环境。例如Burp Suite、Python3、PHP本地环境、特定的调试器或反编译工具。这能帮助读者快速搭建起复现环境。详细解题过程这是WP的躯干我会按照时间顺序或逻辑顺序拆解我的思考过程。信息收集拿到题目第一步做什么查看网页源码、抓包看HTTP头、扫描目录、下载附件分析。任何发现哪怕看似无用都值得记录。思路分析根据收集到的信息初步判断可能的漏洞方向。这里可以写下你的多个猜想。尝试与失败详细记录你尝试过的错误路径。为什么这个payload不行是过滤了哪些字符是逻辑判断没绕过失败的经验往往比成功的更宝贵。突破点发现描述你是如何找到那个关键线索的。可能是一段特殊的代码注释一个不寻常的HTTP响应头或者对某个函数输入输出的反复测试。漏洞利用链构造如何将突破点串联起来形成一个完整的攻击链。这里需要详细说明每一步的用意和原理。最终Payload与结果给出能稳定拿到flag的最终利用代码或操作步骤并展示执行结果如flag内容。技术原理深入剖析跳出解题步骤深入讲解题目背后涉及的技术原理。比如如果考了“SSTI服务端模板注入”就在这里讲清楚模板引擎的渲染机制、常见的检测和利用手法。总结与拓展复盘整个解题过程哪些地方可以优化题目有哪些巧妙的设置与此漏洞相关的真实世界案例有哪些还可以尝试哪些变种或更深层次的利用2.2 写作中的“要”与“不要”有了结构填充内容时更需要技巧。下面是我总结的一些心得要图文并茂但图需有“灵魂”。截图是必须的但不要只截一个最终结果。关键步骤的交互界面、错误信息的特写、代码片段的局部高亮这些图才有价值。在图上用箭头、方框做好标注引导读者视线。要代码完整但需附带“注释”。给出的Exploit脚本不能是“天书”。重要的变量、函数、攻击逻辑必须用注释写清楚。如果是Python脚本我习惯在关键行后面用# 说明这里是为了...的方式注解。要讲清原理避免“魔法”操作。最忌讳的就是“在这里输入{{7*7}}发现回显49说明存在SSTI”。为什么是{{7*7}}为什么回显49就证明了背后的检测逻辑是什么必须解释清楚让读者明白这不是玄学而是有迹可循的逻辑推理。不要藏私分享“踩坑”记录。那个让我卡了两小时的字符编码问题那个因为粗心写错的路径我都会专门写一个小节叫“踩坑记录”。这对新手来说是无价之宝他们很可能正被同样的问题困扰。不要忽略“环境差异”。明确说明你的解题环境如Kali Linux 2023.4, Python 3.11.4。有些Payload或工具用法在不同版本下可能不同提前说明能减少读者的困惑。注意在分享WP时尤其是涉及真实比赛平台尚未关闭的题目务必注意分寸。通常建议在比赛完全结束后再发布详细的解题报告以示对比赛主办方和其他选手的尊重。3. 以“Web”题目为例的实战拆解让我们代入一个虚构的“WUST-CTF2020”的Web题目场景我将展示如何将上述心法应用于实战。假设这道题叫Easy File Upload分值50。3.1 第一阶段信息收集与初步试探拿到题目链接我首先用浏览器访问。这是一个简单的文件上传界面只有一个上传表单写着“上传你的头像”。查看网页源码没有发现隐藏的注释或JS提示。第一步基础测试我尝试上传一个正常的test.jpg图片成功页面回显“文件上传成功”。接着我尝试上传一个test.php文件内容为?php phpinfo();?。页面返回错误“只允许上传jpg, png, gif格式文件”。这是前端验证我立刻用Burp Suite抓包拦截上传请求将文件名test.php改为test.jpg但内容仍是PHP代码。这次请求发送出去了但服务器返回“文件类型不正确拒绝上传”。记录点这里我发现了两层防御1. 前端JS验证后缀名2. 后端验证Content-Type或文件内容。绕过前端很简单禁用JS或抓包改包但后端需要进一步分析。第二步深入探测我使用Burp的Intruder模块对上传包中的Content-Type: image/jpeg进行模糊测试尝试改为image/png,text/plain,application/php等均被拒绝。看来不是简单的Content-Type检查。我转而尝试后缀名绕过。在抓包状态下将文件名改为test.php.jpg、test.jpg.php、test.pHp大小写绕过、test.php%20、test.php.末尾加点。发现当上传test.php.末尾加点时服务器返回了不同的错误“文件处理失败”。这是一个有趣的信号可能服务器在去除末尾点后得到了.php但中间的处理逻辑出现了差异。3.2 第二阶段漏洞发现与利用链构造“文件处理失败”比“拒绝上传”更进步说明文件可能已经写入了临时目录或某个路径但在后续处理如移动、重命名时出错。我猜测服务器端的代码逻辑可能是这样的$filename $_FILES[file][name]; // 获取文件名 $allowed_ext array(jpg, png, gif); $ext strtolower(pathinfo($filename, PATHINFO_EXTENSION)); // 获取后缀 if (!in_array($ext, $allowed_ext)) { die(文件类型不正确拒绝上传); } // 生成目标路径 $target_path ./uploads/ . basename($filename); // 移动临时文件 if (move_uploaded_file($_FILES[file][tmp_name], $target_path)) { echo 文件上传成功路径 . $target_path; } else { echo 文件处理失败; }这里的关键在于pathinfo($filename, PATHINFO_EXTENSION)和basename($filename)这两个函数的行为。我查阅PHP手册pathinfo()在遇到test.php.这样的文件名时取到的EXTENSION是空字符串因为最后一个点后面没有字符了。而basename()函数会简单地返回文件名对于test.php.它返回的仍然是test.php.。那么检查逻辑$ext为空不在array(jpg, png, gif)中本应拒绝。但为什么通过了我推测实际代码可能先用了trim()或类似函数去除了文件名两边的空白字符而在Windows环境下trim()默认也会去除末尾的点.这是一个非常经典的陷阱。所以流程可能是$filename trim($_FILES[file][name]);//test.php.被修剪为test.php$ext pathinfo($filename, PATHINFO_EXTENSION);// 此时取到的是php检查$ext是否在允许列表不php不在应拒绝。 等等矛盾了。如果这样我的test.php.应该被拒绝才对。除非...检查逻辑有误我重新审视错误信息“文件类型不正确拒绝上传”和“文件处理失败”。前者是后缀检查不通过后者是移动文件失败。我的test.php.触发了后者说明它绕过了后缀检查那么更可能的逻辑是服务器先使用pathinfo的某个参数如PATHINFO_FILENAME和PATHINFO_EXTENSION分离但处理不当。或者它使用了strrchr()或explode(., $filename)来取后缀而test.php.用.分割后得到的数组最后一个元素是空字符串导致检查绕过。构造Payload 基于以上推测我决定系统性地测试各种变形。我写了一个Python脚本配合Burp的Intruder快速遍历各种后缀名绕过技巧import requests url http://target.com/upload.php base_filename shell test_cases [ shell.php, shell.php., shell.php , # 末尾空格 shell.php%20, # URL编码空格 shell.php::DATA, # Windows NTFS流通常不适用 shell.php.jpg, shell.jpg.php, shell.pHp, # 大小写 shell.php;.jpg, shell.php%00.jpg, // %00截断需特定PHP版本 shell.png, # 但内容为GIF头PHP代码 ] for fname in test_cases: files {file: (fname, ?system($_GET[\cmd\]);?, image/jpeg)} r requests.post(url, filesfiles) print(f[*] Trying {fname}: {len(r.content)} bytes) if 上传成功 in r.text: print(f[] Potential Success with {fname}) print(r.text) break在实际测试中shell.php.末尾加点和shell.php末尾加空格都可能导致检查逻辑混乱具体取决于服务器端的字符串处理函数如trim,rtrim,pathinfo的解析差异。最终我通过手动测试发现上传文件名为shell.php.服务器返回了“文件上传成功路径./uploads/shell.php”。果然服务器端的处理逻辑是将我上传的shell.php.保存为了shell.php3.3 第三阶段获取Shell与Flag读取上传成功后我需要访问这个文件来执行代码。根据回显的路径./uploads/shell.php我直接访问http://target.com/uploads/shell.php。但是返回了404。这说明上传目录可能不是Web可访问的或者路径有误。我转而尝试路径遍历。在上传时将文件名改为../shell.php.试图跳出上传目录。服务器返回“非法文件名”。看来有过滤。另一种思路题目可能设置了上传目录不可执行或者.php文件被重命名了。我重新上传一个正常的info.jpg查看回显的具体路径。回显是“文件上传成功路径./uploads/20240527_abcdefg.jpg”。原来文件被重命名了这解释了为什么直接访问shell.php不行。那么关键就是找到这个重命名后的文件名。观察格式日期_随机字符串.后缀。我需要利用文件包含漏洞或者让服务器以某种方式“执行”这个图片马。我再次查看上传表单发现只有一个上传按钮没有其他输入点。灵光一现服务器在检查文件类型时是否只检查了后缀如果我把PHP代码嵌入到一个真正的图片文件中呢我使用命令cat shell.php valid.jpg将一个包含?php system($_GET[‘c’]);?的文本追加到一个正常的JPG图片末尾。然后上传这个valid.jpg。上传成功得到路径./uploads/20240527_hijklmn.jpg。现在我需要一个点来触发服务器解析这个图片中的PHP代码。常见的点是“文件包含”漏洞。我检查URL参数没有任何参数。我查看首页、其他页面也没有发现明显的包含点。这时我想到“条件竞争”或“日志污染”。但题目叫“Easy File Upload”应该不会这么复杂。我回头仔细看第一次上传test.php.的成功回显“文件上传成功路径./uploads/shell.php”。这个回显是假的吗还是说它只是在移动文件时出错但文件其实以.php的格式写入了某个临时位置我尝试访问./uploads/shell.php404访问./uploads/shell.php.也是404。看来此路不通。换一种攻击方式如果服务器在检查文件内容时只检查了文件头Magic Bytes那么我可以在文件开头添加图片魔数。我构造一个内容如下的文件shell.jpgGIF89a; // 或 FF D8 FF E0 (JPEG魔数) ?php if(isset($_GET[cmd])){ system($_GET[cmd]); } ?上传这个文件服务器可能只识别了GIF89a认为它是合法的GIF从而放行。上传成功后我得到路径./uploads/20240527_opqrstu.jpg。现在我需要让服务器以PHP方式解析这个.jpg文件。这通常需要配合服务器配置错误如AddType application/x-httpd-php .jpg或文件包含漏洞。我决定扫描一下目录看看有没有其他脚本。用dirsearch扫描发现了一个index.php和一个view.php。访问view.php它有一个参数file20240527_opqrstu.jpg。Bingo这就是文件包含点。我访问http://target.com/view.php?file./uploads/20240527_opqrstu.jpg图片正常显示。尝试传入?file./uploads/20240527_opqrstu.jpgcmdls没有回显。看来代码没有执行。可能是因为被当作图片数据输出了没有经过PHP解析。最终利用我需要利用PHP的封装协议。如果服务器允许包含php://input或者data://协议我就可以直接执行代码。我尝试访问view.php?filedata://text/plain,?php system(‘ls’);?。返回错误“非法文件路径”。可能过滤了://。尝试php://filter/convert.base64-encode/resourceview.php看看能否读取源码。成功返回了一串Base64编码解码后是view.php的源代码?php $file $_GET[file]; if(strpos($file, ://) ! false || strpos($file, ..) ! false){ die(非法文件路径); } include(./uploads/ . $file); ?果然过滤了协议和路径遍历。但是它没有过滤.和/。我注意到它拼接的路径是./uploads/$file。如果我传入的$file是20240527_opqrstu.jpg那么最终包含的就是./uploads/20240527_opqrstu.jpg。这个文件的开头是GIF89aPHP解析器会将其作为普通文本输出直到遇到?php ?标签。但是PHP解析器在解析文件时如果文件内容以非PHP标签开头它会直接输出这些内容直到遇到第一个PHP开始标签?php。我的文件开头是GIF89a;后面是?php。所以当服务器通过include包含这个文件时它会先输出GIF89a;可能破坏HTTP响应然后执行后面的PHP代码。为了确保执行我需要让?php标签成为文件的第一批字符。修正Payload我重新制作图片马这次确保PHP代码在文件最开头但前面加上图片魔数注释。实际上PHP只会解析?php ?标签内的内容标签外的GIF89a会被当作HTML/文本输出。这可能会引发错误。更好的方法是利用exif_imagetype()等函数只检查文件头部的特性。我可以创建一个真正的、包含恶意代码的GIF文件。使用工具如exiftool可以将PHP代码写入GIF的注释等元数据区域但include不会执行这些元数据。终极方案利用.htaccess如果服务器是Apache。但题目环境未知。时间有限我回到最初的突破口那个“文件处理失败”的错误。它暗示文件可能被上传到了一个临时位置并且名字可能是我们可控的。我尝试在上传shell.php.后立即疯狂重复访问./uploads/shell.php在它被删除之前或者利用条件竞争在上传后和服务器删除临时文件之间快速访问执行。我写了一个Python脚本同时进行上传和访问import requests import threading def upload(): url http://target.com/upload.php while True: files {file: (shell.php., ?php echo system($_GET[\cmd\]);?, image/jpeg)} requests.post(url, filesfiles) def access(): url http://target.com/uploads/shell.php while True: r requests.get(url, params{cmd: ls /}) if r.status_code 200 and bin in r.text: print([] Found!, r.text) break t1 threading.Thread(targetupload) t2 threading.Thread(targetaccess) t1.start() t2.start()运行脚本后成功在返回的HTML中看到了根目录列表并找到了一个名为flag_here_is_my_secret的文件。最终使用cat命令读取到flagwustctf{f1le_upl0ad_1s_fun_but_danger0us}。4. 原理深度剖析文件上传漏洞的防御与绕过通过上面这道虚构但融合了多种常见考点的题目我们可以系统地梳理文件上传漏洞的核心。4.1 服务器端验证的常见层次与绕过方法客户端验证JavaScript原理在表单提交前通过JS检查文件后缀名。这是最弱的一层仅能防君子。绕过禁用浏览器JS使用Burp等工具拦截修改请求包直接使用curl等命令行工具发送POST请求。服务端 - 后缀名检查黑名单禁止上传如.php,.asp,.jsp等后缀。绕过方法使用冷门后端语言后缀.php5,.phtml,.phps利用大小写.Php在末尾添加点、空格、::$DATAWindows特性使用双后缀.php.jpg并配合服务器解析漏洞如Apache的file.php.jpg可能被解析为PHP。白名单只允许.jpg,.png,.gif等。更安全但仍有绕过可能利用%00截断需特定环境利用解析歧义如test.php.test.php如果检查逻辑是取最后一个点之后的内容则test.jpg.php可能被绕过取决于实现。服务端 - 文件类型检查MIME Type原理检查HTTP请求头中的Content-Type如image/jpeg。绕过直接使用Burp修改Content-Type为允许的类型。服务端 - 文件内容检查原理检查文件内容的头部魔数Magic Bytes如图片文件的GIF89a、FF D8 FF E0。绕过在恶意代码前添加正确的图片魔数制作“图片马”。但这种方式上传的文件需要后续的文件包含漏洞或服务器配置错误才能被解析执行。服务端 - 文件重命名/随机化原理上传后服务器将文件重命名为随机字符串防止直接访问。应对需要结合其他漏洞如条件竞争在文件被重命名/删除前访问、路径信息泄露如通过响应包、错误信息泄露最终路径、或者配合后续的文件包含漏洞。服务端 - 目录权限与解析安全原理设置上传目录不可执行或通过Web服务器配置确保上传目录中的脚本不被解析。绕过难度较大。可能利用Web服务器解析漏洞如IIS6.0的分号解析漏洞、Nginx的畸形路径解析漏洞或者利用.htaccess文件Apache覆盖目录解析规则。4.2 文件包含漏洞与上传漏洞的“天作之合”文件上传漏洞的最终目标是执行任意代码。如果上传的文件无法直接通过Web访问或者无法被解析那么“文件包含漏洞”Local File Inclusion, LFI就是关键的“临门一脚”。原理服务器脚本使用include(),require(),file_get_contents()等函数动态包含文件且用户可控制包含的路径。结合利用上传一个内容为PHP代码的图片马如shell.jpg。通过文件包含漏洞去包含这个shell.jpg。如果服务器配置为无条件解析被包含文件的内容常见于使用include时那么shell.jpg中的PHP代码就会被执行。即使服务器不解析非.php文件也可以利用php://filter或data://等协议如果未被过滤直接执行代码或读取服务器上的敏感文件如/etc/passwd, 源代码。5. 常见问题排查与实战技巧锦囊在实战中你肯定会遇到各种奇怪的问题。下面是我总结的一些高频问题及排查思路。5.1 上传成功但无法访问/执行问题文件上传返回成功但通过浏览器访问返回404或403。排查路径问题确认回显的路径是否是Web可访问的相对路径或绝对路径。尝试使用../进行路径遍历需注意过滤。权限问题上传目录可能没有执行权限chmod -x或者Web服务进程如www-data用户没有读取权限。重命名问题文件可能被重命名了。仔细查看响应包有时路径会隐藏在JSON数据、注释或HTTP头中如Location头。中间件解析问题你的.php文件可能被当作纯文本输出了。检查响应头的Content-Type。如果是text/plain说明服务器没有配置解析PHP。5.2 文件内容被破坏或过滤问题上传的PHP代码中的某些字符如?,?,$,(被删除或转义。排查服务器端过滤可能部署了WAF或自定义过滤函数。尝试使用短标签?、script language”php”PHP 7.0或使用编码、混淆技术。文件内容检查服务器可能读取了文件内容并进行了安全处理。尝试制作真正的图片马将代码隐藏在图片的EXIF数据或像素数据中绕过内容扫描。5.3 条件竞争漏洞的利用不稳定问题利用上传和访问的时间差条件竞争时成功率很低。技巧多线程/多进程使用Python的threading或multiprocessing库同时启动数十甚至上百个上传和访问线程提高命中概率。延长文件存在时间如果可能在上传的文件中写入一段执行时间很长的代码如sleep(10)为后续访问争取时间窗口。目标选择不是所有“上传-删除”逻辑都存在竞争条件。通常上传到临时目录然后移动到正式目录或在移动前进行复杂检查的流程更容易出现竞争。5.4 如何高效地Fuzz上传点手动测试效率低下我通常会采用半自动化的方式使用Burp Suite Intruder将文件名、Content-Type等参数设为Payload位置加载一个包含各种绕过技巧的字典如/usr/share/wordlists/seclists/Fuzzing/extensions_common.txt并加以改造。编写Python脚本如上文示例可以更灵活地控制Payload和逻辑方便进行条件竞争等复杂测试。字典质量是关键不要只用网上现成的字典。根据目标服务器的语言PHP/Java/.NET和操作系统Linux/Windows特征自己整理一份针对性的测试用例列表。例如针对Windows要加入::$DATA、末尾空格/点针对老旧PHP要加入%00截断。5.5 实战中的“降维打击”技巧信息泄露是突破口永远关注错误信息、响应头、甚至页面的HTML源码注释。一道题目的解法和描述可能就藏在某个不起眼的注释!-- test.php is removed --里。默认配置与已知漏洞了解常见Web服务器Apache, Nginx, IIS、中间件Tomcat, WebLogic和框架ThinkPHP, Spring在默认配置或历史版本中的文件上传/解析漏洞。这能让你在信息有限的情况下做出合理猜测。从出题人角度思考CTF题目往往是为了考察某个或某几个特定的知识点。如果一条路走得异常艰难不妨退一步想想出题人最可能设置考点的地方是哪里是前端验证的绕过是MIME检查的欺骗还是文件包含的配合理清考察点能帮你更快地聚焦攻击面。写WP的过程是一次完美的“费曼学习法”实践。你必须把每一步为什么这么做、背后的原理是什么清晰地表述出来。当你能够教会别人时你自己才真正掌握了。这份关于“WUST-CTF2020”WP写作心法和文件上传漏洞的深度剖析希望能成为你下一场CTF征战中的得力助手。记住每一个踩过的坑都是通往更高处的一块垫脚石。