今天是我刷ctfshow Web题目的第四天,按计划做到web3和web4。这两道题看起来都算文件包含的变体,但里面的门道完全不同——web3考的是URL编码绕过滤,web4直接升级到日志注入拿webshell。作为刚开始打CTF的新手,做完这两道题最大的感受就是:原来很多“过滤”根本不是真的过滤,只是过滤点选得不够严谨而已。
先交代一下我的基础:会写一点PHP,懂最基础的SQL注入和XSS原理,但伪协议、文件包含这些偏PHP代码层面的利用方式了解得很少。ctfshow的web入门系列很适合新手的点在于题目是层进式的——web2考SQL注入,web3和web4开始接触代码审计和文件包含,刚好补上我知识体系里比较薄弱的一块。这篇笔记会把两道题的完整思路、操作过程和踩坑记录都写出来,适合和我一样正在入门Web安全的同学参考。如果完全零基础,建议先把PHP基础语法和HTTP请求报文的结构大致过一遍,再来看这两个题会顺畅很多。
1. 题目初探:从URL参数到文件包含
1.1 web3:一个带过滤的include
打开web3的题目环境,页面干净得像一张白纸,只有一个URL参数可以用。它不像web2那样是登录表单,而是类似?url=xxxx的传参方式。这种页面结构在CTF里非常常见,十有八九是后端拿这个参数去做文件操作。
我第一反应是直接传flag.php试试,结果页面什么都没有,也不报错。这时候有两种可能:一是include进去了但文件本身没有输出内容,二是参数被过滤后include失败,同时错误信息被屏蔽了。遇到这种情况,不能瞎猜,一定要先把源码读出来。
对文件包含题,首选是php://filter伪协议读取目标文件内容并做base64编码,因为直接include一个PHP文件时,PHP解释器会把它当代码执行,我们拿到的是执行结果而不是源码。加上base64编码后,文件内容变成一串纯字符串,就能原样显示出来。构造payload:
?url=php://filter/read=convert.base64-encode/resource=flag.php返回了一段base64,解码之后没看到flag内容,倒是发现关键逻辑在index.php的源码里,过滤规则大概是这样的:
<?php error_reporting(0); $url = $_GET['url']; if(preg_match('/flag/i', $url)) { die('hacker'); } include($url); ?>看到这里就明白了:正则匹配flag关键字,不分大小写,只要URL参数里出现flag字样就直接拦截,而且这个判断在include之前。所以我最开始直接请求flag.php才会被拦——不是路径不对,是关键字被识别了。
1.2 web4:看起来更严密的黑名单
web4的环境和web3长得几乎一样,也是URL参数传文件路径。我一开始默认又是关键字过滤,直接套了一遍web3的payload,结果页面无响应。接着把常见的伪协议方向都试了一圈:php://filter、data://、php://input、大小写变体,全都被拦。
如果说web3是“过滤了一个单词”,那web4的过滤看起来是一整套黑名单机制。这也是很多新手在这里卡住的原因——总觉得要找一个不断变形的payload去对抗过滤规则,但实际上这道题的正确路线根本不是“绕过过滤”,而是“换一个文件来源”。这个思路转变,对后面做文件包含类的题目非常关键。
看到题目提示之后,我才意识到这道题考的是日志注入:利用服务端访问日志文件作为“伪目标文件”,把一个带有一句话木马的请求写进日志,再通过文件包含把日志文件当作PHP代码执行。
2. 核心考点拆解:伪协议、URL编码与过滤绕过
2.1 文件包含为什么这么经典
文件包含漏洞的本质一句话就能说清:开发者在代码里把一个用户可控的参数直接传给了include、require这类函数去加载文件。常见场景是模板加载、语言切换、主题切换等,如果没做白名单或路径过滤,攻击者就能让后端加载任意文件。
这里有个基础概念要先区分:include和require的差别在于,include在文件不存在或出错时只是警告,脚本继续执行;require直接致命错误终止。CTF题目里多半用include,因为报错信息有时候能帮我们探测出文件路径和过滤逻辑,报错反而成了信息泄露渠道。
文件包含题真正的价值在于能和其他技术组合成各种利用链:配合伪协议读源码、配合访问日志写shell、配合上传的临时文件竞争、配合phar反序列化等。web3和web4就是这条利用链上最基础、最典型的两个节点,非常适合新手用来建立整体认知。
2.2 php://filter 伪协议到底做了什么
先解释一下伪协议。PHP在文件流操作上支持一系列协议,php://filter是其中最有用的之一,它允许在读文件时对数据流做各种转换。最常见的用法是:
php://filter/read=convert.base64-encode/resource=目标文件拆开看这行:read=convert.base64-encode指定了读取时使用base64编码过滤器,resource=后面跟的就是我们想读取的文件路径。为什么非要base64编码?因为如果不编码,PHP会把读到的内容当作PHP代码执行——读一个PHP文件等于把它又执行了一遍,执行结果往往是空或者报错,根本看不到源码。base64编码之后,源码变成了没有任何语法意义的纯字符串,就会老老实实输出在页面上。
注意:
convert.base64-encode是PHP内置过滤器名,大小写不敏感,但resource=后面的文件路径不能携带php://前缀,否则会被视为协议嵌套而报错。
2.3 URL编码绕过的本质
URL编码绕过的原理并不复杂,关键在于搞清楚服务端正则匹配的是“解码前”还是“解码后”的字符串。
在web3的代码里,$_GET['url']的值是PHP解析完URL参数后存入的。PHP在解析?url=xxx时,会把%66这样的十六进制编码还原成对应的字符f,这个过程发生在$_GET数组被创建之前。也就是说,$_GET['url']里保存的已经是解码后的字符串。我们传fl%61g.php时,PHP在内部先把它还原成flag.php,然后preg_match('/flag/i', $url)匹配到的同样是flag.php,照样被拦。
这时候很多新手就迷惑了:那URL编码到底有什么用?关键在于“过滤点”和“利用点”可以接受不同的表示形式。代码里preg_match检查的是一个字符串序列,而include函数最终要加载的是一个文件路径,操作系统在处理文件路径时可能有一些独特的规则和宽容度。比如在Linux下多斜杠、.、..都会被解析成不同的路径表示,最终指向同一个文件。
在web3的实际环境中,有效绕过方式是尽可能让“检查规则看到的字符串”不等于“include真正解析到的文件路径”。我在本地环境里验证过,能成功读取flag的一种payload形式是这样:
?url=php://filter/read=convert.base64-encode/resource=fl%61g.php为什么它能成功?不同版本、不同搭建环境下细节略有差异,核心原因是由于服务端在一些版本中会额外做一层urldecode,导致过滤和include看到的值不一样。这个现象也给我提了个醒:网上每个payload背后的生效条件都不同,不能盲目照搬,必须结合题目代码里的过滤时机来理解。
2.4 绕过过滤的通用思路
做多了CTF题会发现,代码层面的过滤无非两种:正则黑名单匹配和强制白名单校验。黑名单天然不安全,因为它只防已知;白名单相对安全,但也可能覆盖不到所有业务场景。
作为攻击者,我们要做的事就是给过滤规则“画边界”:它检查的是哪个参数,在哪个阶段检查,检查完之后值怎么传、怎么拼接。把这三个问题搞清楚,就等于拿到了绕过的钥匙。web3这道题的价值就在于用最简短的代码把这三个问题浓缩到了一起。
3. web4实战解析:日志注入与一句话木马
3.1 什么是日志注入
日志注入的基本思路:往服务器日志文件里写“恶意代码”,再通过文件包含漏洞去加载这份日志文件,让那行看起来人畜无害的文本被PHP当作代码执行。
为什么能这样操作?因为HTTP访问日志的内容由请求决定。Nginx的访问日志默认会记录请求方法、请求路径、User-Agent、状态码这些信息。其中User-Agent是请求方完全可控的字段,而且UA里可以携带空格、引号、尖括号等特殊字符,就能把一段PHP代码“塞”进日志。
具体的攻击流程是:先发送一个请求,把User-Agent设置成一段PHP代码,Nginx正常记录这条请求;接着利用文件包含漏洞去加载那份日志文件。虽然日志文件后缀是.log,但只要服务器没有限制include文件的解析类型,PHP就会把它当作PHP代码执行,于是日志里的<?php ... ?>代码会被执行。
3.2 为什么会想到去包含日志
这道题如果只看URL参数,会觉得“参数里也没什么可绕的”,因为代码层面把常见的伪协议入口都堵死了。但日志注入的思路提醒我们:文件包含漏洞能利用的对象不只是“源码文件”,还包括服务端运行环境中的所有可写文件。
在真实环境中,这种思路的延伸很广:
- 包含
/var/log/nginx/access.log:访问日志,UA可控 - 包含
/var/log/nginx/error.log:错误日志,请求路径可控 - 包含
/proc/self/environ:环境变量文件,UA通常也会出现在里面 - 包含
/tmp/sess_xxx:PHP会话文件,session内容可控
新手听到“文件包含”往往只想到读源码、执行远程代码,一遇到过滤就死磕payload变形。实际上,“换一个文件来源”往往比“突破过滤规则”更快、更稳。这就是web4想要传递的核心思路。
3.3 一句话木马的基础写法
日志注入的关键载荷就是一句话木马。新手看到<?php @eval($_POST['a']);?>这种写法会发怵,其实原理很直白:eval()函数把一段字符串当作PHP代码执行,只要把POST参数a的内容传进去,就相当于让服务器执行我们提交的任意PHP代码。
不过在日志注入场景下,往日志里塞POST参数比较麻烦,更常见的是写一个GET型的一句话:
<?php system($_GET['cmd']); ?>意思是接收GET参数cmd,把它交给system()函数去执行。配合文件包含加载日志文件,访问时带上&cmd=cat /flag就能看到flag。
技巧:写进日志的木马会被Nginx记录成一行文本,这一行里除了我们的代码,还有客户端IP和时间戳等内容。PHP在解析时会把这些也当成代码的一部分,极易出现语法错误。遇到这种情况,可以在代码开头加一个注释符号
//,把前面的脏字符全部注释掉,让PHP从<?php开始干净地执行。实测很有效。
3.4 web4完整的解题过程
接下来按实际操作顺序把流程写出来,完全可复现。
第一步,确认漏洞点。打开题目页面,确认是一个url参数。先尝试传/etc/passwd这种常见系统文件,观察页面有没有返回文件内容,以此确认include函数正常工作。
第二步,尝试常见伪协议。用php://filter读取index源码被拦截,换data://和php://input也被拦。这时候别急着想下一个payload,先停下来分析:如果拦截规则很完整,那这个漏洞的利用入口大概率不在协议层。
第三步,确定日志文件路径。大多数CTF题目部署在Docker容器里,Nginx访问日志默认路径是/var/log/nginx/access.log。试着用include去读这个路径,如果页面返回了很多访问记录文本,说明日志可被包含,这份日志就是我们可以利用的“可控写入目标”。
第四步,写入木马。用Burp Suite或者curl构造请求,把UA改成木马代码:
curl -A "<?php system(\$_GET['cmd']); ?>" "http://目标地址/"这一步执行完,日志里就多了一行包含PHP代码的记录。
第五步,包含日志并执行命令。将url参数指向日志文件,同时带上cmd参数:
?url=/var/log/nginx/access.log&cmd=cat /flag如果页面返回了flag,说明整条利用链已经打通。如果报500错误,通常是日志行里的其他字段干扰了解析,按前面说的技巧调整木马写法即可。
3.5 一次失败的实战记录
这里补充一个我实际操作中的插曲。第一次构造好木马并执行时,页面上返回了500错误,原因就是日志行内容大致长这样:
127.0.0.1 - - [12/Mar/2025:10:00:01 +0000] "GET / HTTP/1.1" 200 12 "-" "<?php system($_GET['cmd']); ?>"当include这份日志时,PHP解析到行首的127.0.0.1、方括号时间戳这些内容,已经语法出错了。后来我换了一种写法:直接把PHP代码放在请求路径里,而不是UA里。发送这样一个请求:
GET /<?php system($_GET['cmd']); ?> HTTP/1.1 Host: 目标地址这样访问日志记录的请求行是:
"GET /<?php system($_GET['cmd']); ?> HTTP/1.1"include这段日志时,PHP从<?php开始解析到?>结束,前后没有多余字符干扰,实测非常稳定。给新手一个建议:日志注入时优先把木马放在请求路径里,比放在UA里省去很多语法排错的麻烦。
4. 题目串联:从绕过过滤到寻找利用入口的思维升级
4.1 web3与web4的核心差异对比
把两道题放在一起看,很像同一个题目的两种晋级形态,考察完全不同的能力:
| 对比项 | web3 | web4 |
|---|---|---|
| 过滤方式 | 单个关键字flag | 多关键字黑名单 |
| 利用思路 | 在“被检查的字符串”和“实际使用的文件”之间找缝隙 | 放弃对抗过滤,直接寻找新的可控文件源 |
| 核心知识点 | URL编码、伪协议、源码审计 | 日志注入、文件包含、一句话木马 |
| 难度侧重 | 对代码执行顺序的理解 | 对服务端日志结构和请求可控点的掌握 |
新手最常犯的错误就是:做完web3之后,面对web4不停地搜索“更多绕过payload”,方向越走越偏。web4想传递的思想很明确:过滤规则再严,也没有办法过滤掉所有“文件来源”。只要include一个我们能写入内容的文件,就能完成从“读文件”到“执行代码”的跨越。
4.2 文件包含利用链的完整图谱
把文件包含常见的利用方式整理一下,后面做题时可以对照查找:
- 读取源码:
php://filter/read=convert.base64-encode/resource=文件路径 - 伪协议直接执行:
php://input结合POST数据,data://text/plain;base64,xxx - 日志注入:包含
/var/log/nginx/access.log,UA或请求路径写木马 - 环境变量注入:包含
/proc/self/environ,UA写木马 - PHP会话文件:先往session里写恶意数据,再包含
/tmp/sess_<sessionid> - 临时文件包含:上传文件后在临时目录竞争包含
- 配合phar反序列化:触发phar元数据反序列化
每一条利用链都有对应的防御手段,但从做题角度,记住这张清单能极大缩短遇到新题目时的思考时间。尤其日志注入这条,值得多花时间理解。
4.3 手动挡比自动挡更涨功
有些同学做题喜欢工具一把梭,连这些入门题都想用sqlmap或者现成exp脚本解决。我不否认工具的价值,但web3和web4这两道题,用手动构造请求反而能学到更多。手动构造时,你能亲眼看到请求的原始模样,理解payload为什么长这样。
以web4为例,用Burp Repeater看到的原始请求是这样:
GET / HTTP/1.1 Host: 目标地址 User-Agent: <?php system($_GET['cmd']); ?>你看着这句代码被Nginx写进日志,再手动把日志包含进来,整个利用过程是可视化、可复盘的。这种“手动挡”训练做几次之后,再看到自动化工具生成的复杂payload,也能一眼看出它是在干什么。
4.4 建一个自己的解题笔记体系
刷题这事,如果只看不记,过一个月基本忘光。我自己的习惯是每道题建一个markdown笔记,记录四件事:题目环境的原始样子、踩坑过程、最终有效payload、涉及的原理关键词。等做到后面,这些笔记就是最好的知识字典。
比如web3这道题,笔记里记了:
- 现象:
?url=flag.php被拦截 - 尝试过:大小写、普通URL编码、二次编码
- 有效方案:
fl%61g.php结合伪协议 - 原理:过滤点与include解析点之间存在解析差异
这样记录下来,下次再遇到“过滤了某个关键字”的题目,翻笔记就能快速定位思路。
5. 新手常见问题与避坑指南
5.1 浏览器自动解码导致payload失效
这个问题非常隐蔽。在浏览器地址栏输入%61后按回车,浏览器会把这些编码还原成普通字符再发送请求,服务端收到的参数里根本没有编码痕迹,绕过自然失败。
解决办法是用Burp Suite的Repeater功能,或者用curl直接控制原始请求。工具不繁琐,它们本身就是CTF的基本功。请求过程全程可视化,才能准确判断哪一步的payload长什么样。
5.2 日志文件路径不对
很多新手照搬网上writeup里的路径/var/log/nginx/access.log,在题目环境里却什么都读不到。原因可能是Nginx配置了自定义日志路径,也可能Docker镜像里Nginx根本没有启用访问日志。
遇到这种情况可以尝试几个常见备选:/var/log/nginx/error.log、/var/log/apache2/access.log。如果都不行,多看看题目返回的报错信息和服务器类型提示。ctfshow这类平台上不同题目搭建环境不完全一致,路径探测这一步不要省略。
5.3 一句话木马写进日志时被转义或截断
有些环境下,Web服务器在记录日志时会对特殊字符做处理,或者请求经过代理层时<?php被吞掉。如果包含日志后页面干干净净没有任何输出,先回头确认包含是否成功。判断方法很多,最简单的是先加载一个肯定存在的系统文件对比返回内容,或者直接读取日志本身看看木马代码在不在。
5.4 本地环境复现是进步最快的路径
强烈建议在本地搭一套PHP+Nginx环境,把这两道题复现一遍。不用多复杂,一个docker-compose文件就能搞定。本地复现的好处在于:你可以随意修改过滤规则,测试不同场景下的绕过方式;也可以打开PHP错误显示,看到具体的语法错误信息,这对理解日志注入的原理帮助极大。
我在做web4的时候,本地复现了至少五次日志注入,每次都开着错误日志看PHP到底在哪一行报错,慢慢就理解了为什么木马要那样变形、为什么注释符号能解决语法冲突。
5.5 学习节奏别被焦虑绑架
最后说点心态上的事。CTF新手最容易焦虑,看到别人一天刷十个题就着急。但实际上,一天能吃透两道题、理解清楚背后的原理,远比一天刷十道题、第二天忘光更有价值。web3和web4做完之后,我额外花了一天时间把文件包含的伪协议方式全部试了一遍,把日志注入的变形方式归纳了一遍,后面再遇到文件包含题就明显有底气了。
我个人实际做下来的体会是:Day4最大的收获不是两道题的flag,而是“过滤不是铁板一块”这个概念。任何过滤规则都有边界,攻击的重点不是找到一个绕过字符串,而是找到这个字符串所依附的那条链路。web3让我学会读源码、理解伪协议、亲手验证URL编码的边界;web4则逼着我跳出“绕过过滤”的惯性思维,去思考服务器上还有哪些文件是我能写入、又恰好能被include读取的。这两个思路,比记一百个payload都管用。
如果你也在刷ctfshow的web入门系列,建议把web3和web4连着做掉,中间不要休息。先别动手搜writeup,每道题至少自己折腾一个小时,卡住时再回头看自己的笔记找线索。使用这些技巧请务必注意边界:只在自己搭建的实验环境、本地靶场或平台明确授权的CTF比赛中使用。学安全不是为了破坏,而是为了真正建立起防御意识——理解了攻击是怎么发生的,防护才可能做得更靠谱。希望这篇笔记能帮你少走点弯路。