Icinga Web 中的路徑遍歷漏洞
我們最近在Icinga Web中發現了兩個代碼漏洞,這些漏洞允許攻擊者通過運行任意PHP代碼來破壞運行它的服務器。作為我們研究的一部分,我們在PHP引擎中發現了一個未修補的漏洞,它可以利用其中一個發現。本文介紹了這兩個漏洞的技術細節,以及維護人員應該如何修復它們。
在同一篇博文中同時討論PHP和C代碼并不常見。我們將盡最大努力保持它的趣味性。讓我們深入了解它。
影響
部署Icinga最常見的方法是使用與Icinga監控服務器通信的管理界面Icinga Web。
我們發現了一個路徑遍歷漏洞(CVE-2022-24716),該漏洞可被濫用以泄露服務器上的任何文件。它可以在沒有身份驗證和事先不知道用戶帳戶的情況下被利用。我們還發現了CVE-2022-24715,它會導致從管理界面執行任意PHP代碼。
如果攻擊者可以通過首先披露配置文件并修改管理員密碼來訪問數據庫,那么可以很容易地將它們從未經身份驗證的位置連接到服務器。
我們強烈建議將您的icingaweb2實例更新到2.8.6、2.9.6或2.10,即使它沒有直接暴露在互聯網上。
雖然我們不會發布概念驗證,但利用這些發現很簡單。我們還建議假設Icinga Web配置中存在的任何秘密(例如數據庫憑據)可能已被泄露。作為預防措施,應當輪流使用它們。
技術細節
我們假設Icinga Web 2是使用2.9.5-1.hirsute版本并遵循官方文檔進行部署的。正如您稍后將在CVE-2022-24715-Remote Code Execution部分中看到的那樣,此設置使攻擊者的利用變得稍微復雜一些,對我們的安全研究人員來說就更有趣了。
任意文件泄露(CVE-2022-24716)
上下文
Apache HTTP服務器配置為使用其模塊mod_rewrite將所有傳入請求分派給index.php,這種設置對于僅提供一個入口點的現代PHP應用程序來說非常常見:
htaccess
RewriteEngine on
RewriteBase /icingaweb2/
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]
第一個腳本加載webrouter.php,然后嘗試根據請求的路徑將請求分派到正確的軟件組件:
首先處理重要的靜態資源(css/icinga.css、css/icinga.min.css等),支持ETag標頭、縮小和服務器端緩存;
根據請求參數動態生成的圖像(svg/chart.php,png/chart.php);
以lib/開頭的路徑的請求由StaticController處理;
其他一切都交給控制器。
動態路由器總是審查過程中的一個有趣的部分:它們必須根據用戶控制的數據構建路徑,因此很容易出現路徑遍歷漏洞。
識別代碼漏洞
StaticController的重要代碼如下所示:它首先遍歷現有庫以找到與請求URL匹配的庫,然后將關聯的資產路徑連接到客戶端提供的值:
Icinga/Web/Controller/StaticController.php
$assetPath = ltrim(substr($request->getRequestUri(), strlen($request->getBaseUrl()) + 4), '/');
$library = null;
foreach ($app->getLibraries() as $candidate) {
if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) {
$library = $candidate;
$assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/');
break;
}
}
// [...]
$assetRoot = $library->getStaticAssetPath();
$filePath = $assetRoot . DIRECTORY_SEPARATOR . $assetPath;
[...]
$app->getResponse()
[...]
->setBody(file_get_contents($filePath));
}
StaticController的代碼有兩個安全問題:
庫可以宣布一個空的資產路徑,在這種情況下,文件的路徑僅使用用戶輸入構建。例如,icinga/icinga-php-thirdparty。
用戶輸入可以包含目錄遍歷序列(../),從而產生預期目錄之外的最終路徑。例如,icinga/icinga-php-library。
影響
因此,攻擊者可以泄露本地文件系統的任何文件。我們可以通過官方演示實例來確認這個漏洞,例如通過獲取文件/etc/hosts的內容:
$ curl https://icinga.com/demo/lib/icinga/icinga-php-thirdparty/etc/hosts -v [...] 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.1 demo-icinga2 172.17.0.3 2a2f396a3e13
攻擊者還可以以incingaweb2配置文件為目標。其中,它們還包含Web界面使用的數據庫憑據。
如果攻擊者可以訪問數據庫服務,他們可以使用這些憑據更改現有帳戶的密碼并獲得對實例的經過身份驗證的訪問權限。根據此訪問權限,我們研究了這種情況,后來找到了一種在實例上執行任意代碼的方法。
在非默認部署中,Icinga也可以被告知使用本地文件系統上的SSH私鑰。可以通過使用這種技術讀取它們,然后以監控代理的身份轉移到其他系統。
遠程代碼執行(CVE-2022-24715)
初步調查結果
經過身份驗證的用戶可以編輯資源,以便以后從其他配置文件中引用它們。其中一種資源類型是SSH密鑰,需要將其寫入本地文件系統才能使用。
我們發現在[1]處沒有對SshResourceForm的參數用戶執行驗證。它允許攻擊者使用目錄遍歷序列(例如../ )在[2]的預期目錄之外寫入SSH密鑰:
application/forms/Config/Resource/SshResourceForm.php
public static function beforeAdd(ResourceConfigForm $form)
{
$configDir = Icinga::app()->getConfigDir();
$user = $form->getElement('user')->getValue();
$filePath = $configDir . '/ssh/' . $user; // [1]
if (! file_exists($filePath)) {
$file = File::create($filePath, 0600);
// [...]
$file->fwrite($form->getElement('private_key')->getValue()); // [2]
我們的第一個假設是認為這個漏洞是沒有用的,因為SSH密鑰是用openssl_pkey_get_private()驗證的;編寫一個同時也是有效PEM證書的PHP腳本聽起來并不容易。
這個函數調用是唯一的障礙,值得花時間深入研究如何實現它。正如文檔中提到的,這個函數是PHP的Cryptography Extensions的一部分,它的代碼位于php-src/ext/openssl。
我們需要更深入
在PHP引擎源代碼中查看這個實現時,可以注意到PHP中OpenSSL模塊特有的一個屬性。此類庫通常提供一種加載數據的方法,要么基于它將打開和讀取的文件名,要么基于數據本身(在這種情況下,由用戶來處理任何I/O操作)。
在這里,兩種方法都被自動支持:如果參數$private_key以file://為前綴,它會為用戶讀取文件。否則,該參數被視為是證書的值。
這導致在其實現中出現一些相當不常見的控制流:
php-src/ext/openssl/openssl.c
static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len)
{
EVP_PKEY *key = NULL;
X509 *cert = NULL;
bool free_cert = 0;
char * filename = NULL;
// [...]
} else {
// [...]
if (Z_STRLEN_P(val) > 7 && memcmp(Z_STRVAL_P(val), "file://", sizeof("file://") - 1) == 0) {
filename = Z_STRVAL_P(val) + (sizeof("file://") - 1);
if (php_openssl_open_base_dir_chk(filename)) {
TMP_CLEAN;
}
}
// [...]
if (filename) {
in = BIO_new_file(filename, PHP_OPENSSL_BIO_MODE_R(PKCS7_BINARY));
} else {
in = BIO_new_mem_buf(Z_STRVAL_P(val), (int)Z_STRLEN_P(val));
}
在上面的代碼片段中,zval *val是通過表單提交的私鑰的內部表示形式。val是二進制安全的,這意味著即使PHP引擎包含NULL字節,它也可以在數據旁邊跟蹤其長度(以字節為單位)來處理完整的字符串。但是,libssl API(BIO_*)僅適用于以NULL結尾的字符數組,這些數組本質上不是二進制安全的:處理活動將在第一個NULL字節處停止。
攻擊者可以使用這個屬性來繞過openssl_pkey_get_private()執行的驗證,同時保留將任意數據放入資源文件的能力:PHP在磁盤上搜索證書時會在第一個NULL字節處停止,但完整數據將寫入到目標文件!
然后,攻擊者可以將有效載荷分為4個部分:
輸入易受攻擊代碼路徑的強制前綴file://;
服務器上有效PEM證書的路徑,例如,我們的測試虛擬機中的/usr/lib/python3/dist-packages/twisted/test/server.pem;
一個NULL字節;
要編寫的文件內容,這里是一個執行外部命令的小PHP腳本。

使用官方Linux軟件包安裝時,Icinga Web 2的PHP腳本部署在/usr/share/icingaweb2下。它們歸root用戶所有,因此不能使用HTTP服務器運行時使用的www-data的身份進行修改。
雖然這可以防止基于在此目錄下植入PHP文件并訪問它們的直接利用,但我們發現了攻擊者可以用來獲取任意代碼執行的另一種技術。
Icinga有一個模塊的概念,即擴展接口功能的自包含第三方代碼(例如,添加Grafana支持)。這些模塊默認存儲在/usr/share/icingaweb2/modules下,但管理員也可以直接從界面更改此路徑。
設置global_module_path期望模塊所在的路徑以冒號分隔。將此值更改為先前演示的漏洞可以寫入的路徑,例如/dev/shm/,將global_module_path設置為/dev/,并啟用名為shm的新模塊,允許執行任意PHP代碼。
補丁
這兩個漏洞都與類似的易受攻擊的代碼模式相關,并通過在構建目標路徑(067ec0f、b7c31eb)后引入新的驗證步驟來解決:
構建路徑;
調用realpath():目錄遍歷序列,解析符號鏈接,并確保目標文件存在;
它確保由realpath()調用產生的路徑仍在預期的目錄下。
在將SSH資源的值寫入磁盤之前,還會對它們進行進一步的格式驗證,以防止使用file://。
我們還聯系了PHP維護人員,以解決OpenSSL核心擴展功能中的NULL字節注入問題。由于沒有任何其他功能旨在驗證證書的格式,其他軟件很可能使用相同的易受攻擊的功能。
我們提供了補丁和測試用例,以方便維護人員采用;在撰寫本文時,bug ticket仍處于開放狀態。盡管如此,我們還是選擇公開記錄這個錯誤,因為我們認為安全風險很低,而且Icinga Web 2已經提供了幾周的額外修復。
時間線
結語
在本篇文章中,我們介紹了IT監控解決方案Icinga Web 2中兩個非常相似的漏洞背后的技術細節。這兩個漏洞可以在一次攻擊中結合使用,以完全破壞Icinga服務器。在研究這些漏洞的過程中,我們還發現了PHP解釋器本身的一個bug。我們在這里提醒大家,在實現一種語言的內置函數時,可能會發現一些意想不到的屬性,這些函數可以允許利用其他安全漏洞。
我們強烈建議不要將此類系統按原樣暴露在Internet上:它們只能通過受信任的源IP地址(例如VPN端點)訪問或置于集中式身份驗證系統之后。
我們要感謝Icinga和PHP維護人員及時回復并幫助解決我們發現的問題。
參考及來源:https://blog.sonarsource.com/path-traversal-vulnerabilities-in-icinga-web/
