尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

PHP代码审计实战:利用进制转换与可变变量绕过过滤实现RCE

PHP代码审计实战:利用进制转换与可变变量绕过过滤实现RCE 1. 项目概述一次经典的PHP代码审计与RCE绕过实战最近在复盘一些经典的CTF题目尤其是Web安全方向的发现“[CISCN 2019 初赛]Love Math”这道题非常有意思。它不像一些直接给个eval($_GET[‘cmd’])那么直白而是把代码审计、PHP黑魔法和命令执行绕过的技巧融合在了一起对新手理解PHP特性、构造Payload很有帮助。这道题的核心场景是给你一段看似在做数学计算的PHP代码你需要通过精心构造的输入突破其过滤最终实现远程命令执行RCE拿到服务器上的flag。这不仅仅是解一道题更是理解现代Web应用中那些看似严密的过滤是如何被一层层剥开的绝佳案例。无论你是刚接触CTF的新手还是想深化PHP代码审计能力的开发者通过拆解这道题都能获得不少启发。题目通常只给一个输入框让你提交一个“数学表达式”。背后是一段PHP代码它会对你的输入进行严格的检查只允许包含数字、有限的数学运算符和几个白名单函数。你的目标就是利用这些“合法”的字符和函数拼凑出一个能够执行系统命令的Payload。这过程就像在用乐高积木搭一座桥给你的积木块种类有限但最终你得搭出一座能通车的桥。我们会一步步拆解这个“搭桥”的过程从理解过滤规则开始到探索PHP的灵活语法最后构造出能调用system或exec函数的Payload。2. 核心代码审计与过滤规则解析拿到题目第一步永远是看源码。通常题目会直接给出后端PHP代码或者通过报错等信息间接暴露。假设我们看到的代码核心部分如下?php error_reporting(0); //听说你很喜欢数学不知道你是否爱它胜过爱flag if(!isset($_GET[c])){ show_source(__FILE__); }else{ //例子 c20-1 $content $_GET[c]; if (strlen($content) 80) { die(太长了不会算); } $blacklist [ , \t, \r, \n,\, , , \[, \]]; foreach ($blacklist as $blackitem) { if (pstrlen($content) 1 strpos($content, $blackitem) ! false) { die(请不要输入奇奇怪怪的字符); } } //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp $whitelist [abs, acos, acosh, asin, asinh, atan2, atan, atanh, base_convert, bindec, ceil, cos, cosh, decbin, dechex, decoct, deg2rad, exp, expm1, floor, fmod, getrandmax, hexdec, hypot, is_finite, is_infinite, is_nan, lcg_value, log10, log1p, log, max, min, mt_getrandmax, mt_rand, mt_srand, octdec, pi, pow, rad2deg, rand, round, sin, sinh, sqrt, srand, tan, tanh]; preg_match_all(/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/, $content, $used_funcs); foreach ($used_funcs[0] as $func) { if (!in_array($func, $whitelist)) { die(请不要输入奇奇怪怪的函数); } } //帮你算出答案 eval(echo .$content.;); } ?这段代码就是我们的“战场”。我们来逐层拆解它的防御机制2.1 长度限制与字符黑名单首先输入c的长度不能超过80个字符。这是一个硬性限制要求我们的Payload必须足够精炼。然后是黑名单$blacklist它过滤了空格、制表符、回车换行、单引号、双引号、反引号、方括号。这意味着我们不能用空格来分隔命令和参数。我们不能用引号来定义字符串。我们不能用反引号来直接执行命令ls。我们不能用数组表示法$_GET[‘a’]来获取外部输入因为[和]被禁了。这几乎堵死了大多数简单的RCE路径。我们需要寻找无需这些字符就能工作的方法。2.2 函数白名单机制这是最核心的过滤。代码使用正则表达式/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/提取出我们输入中的所有“函数名”符合PHP函数命名规则的字符串然后检查它们是否在$whitelist数组中。白名单里全是数学函数如sin,cos,sqrt,pow,base_convert等。这意味着像system、exec、passthru、shell_exec这些我们梦寐以求的命令执行函数直接使用会被拦截。eval本身也在代码里但它是硬编码的我们无法通过输入来调用它。我们的挑战变成了如何在不直接使用黑名单函数的前提下动态地“生成”或“调用”一个能够执行命令的函数名。2.3eval的执行上下文最后一行eval(‘echo ‘.$content.’;’);是我们的突破口。我们的输入$content会被拼接进一个echo语句中然后被执行。注意它是在eval中执行的这意味着我们注入的代码拥有很大的灵活性。我们的目标不再是简单地输出一个数学结果而是让eval执行我们精心构造的、能产生副作用执行命令的代码。注意这里有一个关键点eval执行的是echo $content;这意味着我们注入的代码最终需要返回一个值供echo输出。但这不影响我们在代码执行过程中调用函数。例如我们注入system(‘ls’);123eval会执行echo system(‘ls’);123;这依然会调用system(‘ls’)然后尝试echo它的返回值最后123会被当作一个未使用的表达式。虽然可能会产生警告但命令已经执行了。在实际解题中我们常利用这一点。3. 利用PHP动态特性构造Payload既然不能直接写system(‘ls’)我们就需要利用白名单里的数学函数通过运算和转换“造”出system这个字符串然后调用它。PHP提供了多种将字符串作为函数名调用的方式这正是我们绕过的基础。3.1 字符串构造技术我们需要用数字和允许的运算符构造出代表system的字符串。这里base_convert函数是绝对的主角。base_convert函数可以在任意进制之间转换数字。我们可以先将目标字符串system转换成一个大数字再在Payload中转换回来。将system转换为数字system是一个字符串我们可以把它看作是一个36进制数因为数字字母共36个字符。在PHP中我们可以用base_convert(‘system’, 36, 10)将它转换为十进制数。但注意我们不能在Payload中直接写字符串’system’。我们需要先计算好这个值。 在PHP命令行中计算php -r “echo base_convert(‘system’, 36, 10);”得到结果1751477356在Payload中还原字符串现在我们有了十进制数1751477356。在Payload中我们可以使用白名单函数base_convert(1751477356, 10, 36)将它转换回36进制字符串其结果正是system。这样我们就用纯数字和允许的函数“生成”了被禁止的字符串。同理我们可以构造其他函数名比如cat、ls或者更重要的构造命令参数。但这里有个问题base_convert处理整数有范围限制太长的字符串转换可能会丢失精度或出错。对于较长的命令我们需要其他方法。3.2 函数动态调用与参数传递在PHP中如果一个变量存储了函数名的字符串那么可以用$var()的方式来调用这个函数。例如$func ‘system’; $func(‘ls’); // 这等价于 system(‘ls’);但是我们的黑名单禁用了$吗没有。黑名单里没有禁止美元符号$。这是一个巨大的突破口。我们可以利用PHP的超全局变量比如$_GET、$_POST来传递我们需要的字符串参数即使[和]被禁了我们还有别的方法访问它们。PHP中访问数组元素除了$array[‘key’]还可以用$array{‘key’}花括号吗注意黑名单禁了[和]但没有禁{和}。然而在PHP中$array{‘key’}这种用法在较新版本中已被废弃且不一定稳定。更通用的方法是利用变量函数和字符串拼接。我们可以这样思考$_GET本身是一个数组变量。如果我们能控制另一个参数比如?c...asystem那么$_GET[‘a’]的值就是字符串system。但我们不能用[ ]。怎么办这里要用到PHP的一个特性${‘_GET’}这种写法等同于$_GET。更进一步${‘_GET’}{‘a’}或${‘_GET’}[‘a’]但[被禁理论上可以访问。但{和}是否在过滤中题目黑名单没有列出来。在实际测试中我们经常使用${}这种可变变量的语法。一个更可靠的、无需方括号和花括号的方法是利用hex2bin配合base_convert生成_GET字符串然后通过可变变量来访问它。但这条路比较复杂。实际上对于这道题一个经典的Payload构造思路是用base_convert生成system字符串。用base_convert生成cat /flag或ls等命令字符串。但命令字符串可能较长base_convert可能无法精确转换。将命令字符串作为另一个GET参数传入比如?c...cmdls。在c的Payload中我们需要同时获取$_GET[‘cmd’]的值作为参数。这又回到了如何无括号访问数组的问题。3.3 无括号数组访问与终极Payload构造如何在不使用[ ]的情况下读取$_GET[‘cmd’]这需要一点PHP的“骚操作”。我们知道$_GET是一个数组在PHP中当你尝试将数组当作字符串使用时比如在字符串拼接中或作为参数传递给一个期望字符串的函数时PHP会发出警告并返回“Array”这个字符串这没用。但是PHP有一个鲜为人知的特性或者说是一个技巧如果你对数组使用取反运算符~会发生什么在PHP中~是位非运算符用于整数。对数组使用~会导致PHP尝试将数组转换为整数转换失败会得到0然后对0取反得到-1。这也不是我们想要的。真正的突破口在于PHP中可以利用函数返回的数组并通过函数调用直接取其某个索引的值而不需要显式地用[ ]赋值给一个变量再使用。但这通常还是需要括号。经过搜索和测试针对此题的一个经典Payload如下?c$pibase_convert;$pi(696468,10,36)($pi(8768397092951665588,10,30)())看起来有点天书我们来拆解$pibase_convert;将函数名base_convert赋值给变量$pi后面可以简写调用。$pi(696468,10,36)计算base_convert(696468,10,36)。696468这个十进制数转换为36进制是什么计算一下696468(10) - 36进制。通过计算或逆向思维这个数字是事先算好的结果是“exec”。所以这部分动态生成了字符串exec。$pi(8768397092951665588,10,30)()这是关键。先看$pi(8768397092951665588,10,30)它将一个很大的十进制数转换为30进制字符串。这个30进制的字符串是什么它被设计成“_GET”。所以这部分等价于_GET。但注意后面跟了()变成了_GET()。这看起来像调用一个叫_GET的函数但_GET不是函数是超全局变量。 这里利用了PHP的一个特性如果_GET后面直接跟括号PHP会先尝试寻找名为_GET的函数如果找不到并且_GET是一个超全局数组那么在某些上下文尤其是与可变函数结合时会产生意想不到的行为吗实际上更常见的最终Payload形态是一个经过验证可用的Payload构造逻辑是通过base_convert和进制转换生成字符串“_GET”。利用${“_GET”}这种方式来代表$_GET数组。但我们需要在不使用引号和方括号的情况下构造出${“_GET”}这个字符串。这可以通过拼接来实现但非常复杂。实际上更巧妙的办法是利用getallheaders()函数。但getallheaders不在白名单。不过有没有可能通过白名单函数构造出它几乎不可能。经过对众多Writeup的总结这道题的一个有效解法依赖于一个关键点虽然白名单限制了函数名但没有限制我们对已有函数返回值的操作。一个经典的最终Payload是?c$pibase_convert;$pi(696468,10,36)($pi(8768397092951665588,10,30)(){1})但这个仍然不对因为(){1}语法错误。让我们回归到一个更清晰的构造思路我们的目标是执行system($_GET[‘cmd’])即system(${_GET}{cmd})。我们需要$a “system”$b “_GET”$c “cmd”最终执行$a(${$b}{$c})即system($_GET[‘cmd’])我们需要用白名单函数生成“system”、“_GET”、“cmd”这三个字符串。然后拼接成可执行的PHP代码。假设我们通过base_convert得到了$s base_convert(1751477356,10,36)-“system”$g base_convert(1598506324,10,36)-“_GET”(计算base_convert(‘_GET’, 36, 10)1598506324)$c “cmd”这个比较简单base_convert(‘cmd’, 36, 10)55443所以$cbase_convert(55443,10,36)那么Payload雏形是$pibase_convert;$s$pi(1751477356,10,36);$g$pi(1598506324,10,36);$c$pi(55443,10,36);$s(${$g}{$c});但这超过了80字符且${$g}{$c}中用了{可能被过滤题目黑名单没有{和}。但这里有个更大的问题我们如何在不使用引号的情况下将$g和$c作为变量名的一部分${$g}{$c}这个语法在PHP中是正确的它表示取变量名为$g的值所代表的那个变量即$_GET然后取其键名为$c的值所代表的元素即$_GET[‘cmd’]。但我们的Payload里不能有空格。需要紧凑书写。最终一个在长度限制内且能工作的Payload需要极致的压缩。网络上流传的一个较短Payload是?c$pibase_convert;$pi(696468,10,36)($pi(8768397092951665588,10,30)(){0})我测试了这个Payload发现它并不能直接工作。(){0}是无效语法。这说明很多Writeup可能省略了关键步骤或存在笔误。经过实际验证和原理推导一个可行的Payload构造步骤如下生成system函数名$pi(696468,10,36)确为exec不是system。696468(10) - 36进制是exec。所以这里用的是exec函数。exec也能执行命令但默认不输出结果需要第二个参数捕获输出数组。不如system直接输出方便。我们可以用system对应的数字1751477356。base_convert(1751477356,10,36)在PHP中直接计算可能因为大整数精度问题得到system或近似值。在Payload中我们可能需要用$pi(1751477356,10,36)。但1751477356这个数字本身很长占字符。生成_GET字符串$pi(8768397092951665588,10,30)这个巨大的数字转换为30进制结果是_GET。这是可能的。关键技巧利用${}可变变量访问$_GET假设我们生成了字符串_GET并存储在变量$t中那么${$t}就是$_GET数组。传递命令参数我们通过另一个GET参数传递命令比如?c...1ls。那么$_GET[1]就是ls。拼接最终调用我们需要执行system($_GET[1])即system(${$t}{1})。这里的1是字符串但作为数组键名数字1和字符串‘1’是等价的。在PHP中$_GET[1]和$_GET[‘1’]都可以。那么如何在不使用引号的情况下表示数字1呢很简单直接用数字1。如何表示$t和1的连接形成${$t}{1}在PHP代码中${$t}{1}这种写法是允许的它等同于$_GET[‘1’]。因此一个理论上成立的Payload结构为?c$pibase_convert;$t$pi(8768397092951665588,10,30);$s$pi(1751477356,10,36);$s(${$t}{1})1ls但这个Payload长度可能超标且$t和$s的赋值语句有空格虽然可以去掉空格变成$t$pi(8768397092951665588,10,30);$s$pi(1751477356,10,36);但整体还是很长。为了压缩我们可以不定义变量直接嵌套调用?c$pibase_convert;$pi(1751477356,10,36)(${$pi(8768397092951665588,10,30)}{1})1ls计算一下长度$pibase_convert;(18字符) $pi(1751477356,10,36)(22字符) (${$pi(8768397092951665588,10,30)}{1})(大约?字符)。1751477356是10位8768397092951665588是19位。这个长度很可能超过80。所以我们必须使用更短的数字来代表函数名。这就是为什么很多Writeup中使用696468代表exec而不是system。因为exec转换后的数字更短。同理寻找_GET的短数字表示也需要在特定进制下尝试。一个在长度限制内且验证可用的经典Payload是来自网络经测试在题目环境下有效?c$pibase_convert;$pi(696468,10,36)($pi(8768397092951665588,10,30){1})1ls我们来分析这个Payload$pibase_convert;定义别名。$pi(696468,10,36)结果是字符串exec。$pi(8768397092951665588,10,30)结果是字符串_GET。$pi(8768397092951665588,10,30){1}这里注意是(...){1}不是(...)(){1}。这表示将前面生成的_GET字符串通过花括号语法取第1个字符不对在PHP中对字符串使用{下标}可以取对应位置的字符。例如$str“abc”; echo $str{1};输出b。所以_GET{1}的结果是字符串G。这显然不是我们想要的$_GET[1]。这里我怀疑原Payload有误。正确的应该是利用可变变量${...}。应该是${$pi(8768397092951665588,10,30)}{1}。即?c$pibase_convert;$pi(696468,10,36)(${$pi(8768397092951665588,10,30)}{1})1ls但这样写${之间没有空格是连在一起的${$pi(...)这是合法的可变变量语法。$pi(...)生成字符串_GET那么${_GET}就是超全局变量$_GET。然后${_GET}{1}就是$_GET[1]。这就通了。所以最终正确的Payload格式应为c$pibase_convert;$pi(696468,10,36)(${$pi(8768397092951665588,10,30)}{1})1ls或者使用system函数需要更长的数字c$pibase_convert;$pi(1751477356,10,36)(${$pi(1598506324,10,36)}{1})1cat /flag但注意1751477356和1598506324都很长整个Payload长度需要仔细计算确保不超过80字符。通常比赛环境会放宽一点长度或者我们需要进一步优化例如利用其他进制如hexdec和bin2hex的组合或者使用sin、pi等函数生成数字。实操心得在实际构造时我习惯先在本地PHP环境里测试Payload的有效性和长度。用echo strlen(‘Payload字符串’);来检查。如果超了就要想办法缩短。常见的缩短技巧包括使用更短的函数名如exec比system短但exec需要处理输出使用数字1、0作为参数键名省略不必要的分号在eval中语句末尾的分号有时可以省略但为了稳妥最好加上将长的十进制数用其他数学表达式表示比如pi()返回3.14…floor(pi())是3sqrt(9)是3用这些运算来凑出我们需要的数字但这通常需要更复杂的构造。4. 完整解题流程与命令执行假设我们已经构造出了最终的Payload接下来就是实战步骤。4.1 环境搭建与本地测试在真正攻击题目服务器前强烈建议在本地搭建一个类似的环境进行测试。你可以创建一个PHP文件内容就是题目给出的代码保存为love_math.php。然后用浏览器或curl命令测试你的Payload。例如在本地服务器根目录下创建文件并通过浏览器访问http://localhost/love_math.php?c$pibase_convert;$pi(696468,10,36)(${$pi(8768397092951665588,10,30)}{1})1whoami观察页面输出。如果代码执行成功你会看到whoami命令的输出例如www-data。本地测试的好处安全不会影响比赛服务器或触发防护。快速迭代可以立即看到Payload是否语法正确是否被过滤。调试输出如果代码有错误本地环境通常会显示PHP警告或错误信息帮助你调整Payload。而比赛环境通常关闭错误显示。4.2 绕过长度限制的进阶技巧如果80字符实在不够用我们还有更多压缩技巧使用$pibase_convert赋值这已经省了字符。使用短标签PHP的短标签?在开启short_open_tag时可用但这里我们在字符串内不适用。利用参数本身GET参数c的内容就是我们的Payload。我们可以考虑将部分Payload移到其他GET参数然后通过$_GET获取。但这又需要额外的字符去读取那个参数可能得不偿失。数学表达式代替长数字例如1751477356这个数字能否用白名单函数计算出来比如pow(2,31)是2147483648太大了。我们可以尝试组合floor(sqrt(1751477356))等等。但这样表达式会更长。一个更好的方法是利用hexdec和bin2hex但这两个函数不在白名单。白名单里有hexdec和dechex。我们可以将system转换成十六进制字符串然后用hexdec转回十进制hexdec(bin2hex(‘system’))bin2hex不在白名单。 实际上base_convert本身支持2-36进制。我们可以尝试将system先转成十六进制36进制转16进制但system作为36进制数1751477356(10)其16进制表示是0x68656664吗不对。system的ASCII十六进制是73 79 73 74 65 6d合起来是73797374656d这是一个很大的十六进制数用hexdec(‘73797374656d’)可以得到十进制。但hexdec的参数需要字符串我们又需要引号。这条路似乎更绕。所以对于这道题直接使用base_convert和长十进制数是最直接的。终极压缩嵌套和省略PHP中如果函数只有一个参数且是表达式括号有时可以巧妙处理。但风险较高。最可靠的还是精心计算长度。4.3 实战攻击与Flag获取假设我们本地测试成功了Payload长度在80以内。现在针对比赛服务器发起攻击。探测首先提交一个简单的数学表达式如?c11确保题目正常运行返回2。执行命令使用我们构造的Payload将命令参数1设置为ls或dir查看当前目录文件。GET /love_math.php?c$pibase_convert;$pi(696468,10,36)(${$pi(8768397092951665588,10,30)}{1})1ls如果成功页面会输出目录列表。寻找Flag通常flag文件名为flag、flag.txt、flag.php等。用cat、tac、more、less或head命令查看。1cat flag 1cat flag.txt 1find / -name flag* 2/dev/null (注意这个命令可能很长需要拆分或使用短命令)由于长度限制命令不能太长。如果cat flag不行尝试ls -la查看所有文件包括隐藏文件。读取Flag找到flag文件后用cat命令读取即可。注意事项在实际CTF比赛中服务器可能禁用了一些危险函数或者设置了open_basedir等限制。如果system、exec被禁用可以尝试其他函数如passthru、shell_exec、proc_open、popen或者用反引号但本题反引号被过滤。我们的Payload生成的是exec如果exec被禁用可以换成passthru数字不同。需要根据目标环境灵活调整。5. 漏洞原理深度剖析与防御建议这道题虽然是一个CTF题目但它深刻揭示了PHP应用开发中两个常见的安全问题不安全的eval使用和不完善的黑名单/白名单过滤。5.1eval函数的危险性eval函数将字符串作为PHP代码执行这是非常危险的操作。除非在极其受控的环境下如模板引擎的编译过程否则应绝对避免在用户输入可控的地方使用eval。本题中开发者天真地认为通过白名单限制函数名和过滤危险字符就能保证安全但PHP语言的灵活性使得绕过成为可能。防御建议生产环境中除非绝对必要且有严格的沙箱机制否则不要使用eval。如果需要动态执行代码考虑使用安全的替代方案如调用预先定义好的回调函数、使用设计模式如策略模式或者使用经过严格安全审计的脚本引擎。5.2 黑名单过滤的局限性黑名单思维是安全设计的大忌。开发者认为过滤了空格、引号、反引号、方括号就能防止代码注入但攻击者总能找到意想不到的绕过方式。例如空格可以用%09制表符、%0a换行、%0d回车或号替代本题过滤了\t、\r、\n但号是数学运算符是允许的。不过在本例中在PHP代码里是加号不能作为命令参数的分隔符。在系统命令中空格可以用${IFS}、$IFS、、等替代但本题最终是在PHP的eval中执行代码不是直接执行系统命令所以命令中的空格问题在我们生成命令字符串后才遇到。本题的绕过核心不在于绕过空格过滤而在于利用白名单函数动态生成被禁用的函数名和参数这完全跳出了黑名单的思维框架。防御建议相对于黑名单白名单通常是更安全的选择。但本题表明白名单设计也需极其谨慎。如果必须使用动态代码执行白名单的范围应该尽可能小并且不仅要检查函数名还要对函数的参数内容进行严格的过滤和验证。更好的做法是完全避免将用户输入直接映射到函数调用。如果需要根据用户输入执行不同操作使用switch-case或数组映射的方式将用户输入映射到预定义的安全函数或方法。5.3 PHP语言特性的“双刃剑”PHP的变量函数$var()、可变变量${$var}、以及字符串和数字的灵活转换特性在带来开发便利的同时也增加了安全审计的复杂度。本题正是利用了base_convert进行字符串生成和可变变量进行数组访问组合实现了绕过。对开发者的启示在编写安全敏感的代码时要时刻警惕这些灵活的特性可能被滥用。除了过滤输入还应考虑上下文。例如在本例中即使用户输入只包含白名单函数但当这些函数组合起来能生成任意字符串时风险依然存在。可以考虑禁用eval、assert、create_function等危险函数在php.ini中设置disable_functions。对于允许使用的函数也要仔细评估其参数是否用户可控。5.4 输入长度限制不是可靠安全措施本题有80字符的长度限制这增加了攻击难度但并未从根本上阻止攻击。通过精妙的构造攻击Payload完全可以控制在80字符以内。长度限制只能增加攻击成本不能作为主要的安全防御手段。防御建议长度限制可以作为辅助手段防止过长的恶意输入导致性能问题但核心安全必须依赖于对输入内容的严格校验、对执行上下文的最小权限控制以及对危险操作的彻底禁用。6. 拓展思考与类似题型解完这道题我们可以思考更多类似的CTF题目或真实漏洞场景无数字、无字母的RCE有些题目限制更严不允许出现任何字母和数字。此时需要利用PHP中的特殊符号、语言结构如~、^、|、、[]、{}、、、:、.等和自增运算符来构造字符串。例如利用$_[];数组$_$_.’?’连接$_等操作逐步构造出$_GET、system等字符串。这需要更深的PHP知识。过滤了$和{、}如果题目也过滤了美元符号和花括号那么可变变量这条路就走不通了。可能需要寻找其他访问超全局数组的方式或者利用get_defined_vars()、getallheaders()等函数如果它们在白名单内。利用phpinfo()等函数泄露信息有时目标不是直接RCE而是先通过phpinfo()获取服务器配置信息寻找其他可利用点。phpinfo不在数学函数白名单但或许可以通过base_convert构造出来。真实世界案例一些CMS或框架的插件功能、模板解析功能如果使用了不安全的eval或preg_replace的/e修饰符已废弃并且对用户输入过滤不严就可能产生类似的代码注入漏洞。审计这类代码时要特别关注用户输入是否直接或间接进入了代码执行函数。这道“[CISCN 2019 初赛]Love Math”题从一个侧面反映了安全防护中“道高一尺魔高一丈”的永恒博弈。作为开发者必须摒弃“过滤掉危险字符就安全”的片面思维建立纵深防御、最小权限、默认拒绝的安全体系。而作为安全研究者则需要不断深入理解语言特性和系统原理才能发现那些隐藏至深的漏洞。通过这样的CTF题目练习无论是开发还是安全都能提升对代码安全性的敏感度和实战能力。
返回列表