作者: HACHp1@知道創宇404實驗室
日期: 2019/08/08

漏洞簡介

KDE Frameworks是一套由KDE社群所編寫的庫及軟件框架,是KDE Plasma 5及KDE Applications 5的基礎,并使用GNU通用公共許可證進行發布。其中所包含的多個獨立框架提供了各種常用的功能,包括了硬件集成、文件格式支持、控件、繪圖功能、拼寫檢查等。KDE框架目前被幾個Linux發行版所采用,包括了Kubuntu、OpenMandriva、openSUSE和OpenMandriva。

2019年7月28日Dominik Penner(@zer0pwn)發現了KDE framework版本<=5.60.0時存在命令執行漏洞。

2019年8月5日Dominik Penner在Twitter上披露了該漏洞,而此時該漏洞還是0day漏洞。此漏洞由KDesktopFile類處理.desktop或.directory文件的方式引起。如果受害者下載了惡意構造的.desktop或.directory文件,惡意文件中注入的bash代碼就會被執行。

2019年8月8日,KDE社區終于在發布的更新中修復了該漏洞;在此之前的三天內,此漏洞是沒有官方補丁的。

一些八卦

  • 在Dominik Penner公開此漏洞時,并沒有告訴KDE社區此漏洞,直接將該0day的攻擊詳情披露在了Twitter上。公布之后,KDE社區的人員與Penner之間發生了很多有意思的事情,在這里不做描述。

影響版本

  • 內置或后期安裝有KDE Frameworks版本<=5.60.0的操作系統,如Kubuntu。

漏洞復現

環境搭建

  • 虛擬機鏡像:kubuntu-16.04.6-desktop-amd64.iso
  • KDE Framework 5.18.0
  • 搭建時,注意虛擬機關閉網絡,否則語言包下載十分消耗時間;此外,安裝完成后進入系統要關掉iso影響,否則無法進入系統。

復現過程及結果

PoC有多種形式,此處使用三種方式進行復現,第1、2種為驗證性復現,第3種為接近真實情況下攻擊者可能使用的攻擊方式。

1.PoC1:
創建一個文件名為”payload.desktop”的文件:

在文件中寫入payload:

保存后打開文件管理器,寫入的payload被執行:

文件內容如下:

2.PoC2:

創建一個文件名為” .directory”的文件:

使用vi寫入內容(此處有坑,KDE的vi輸入backspace鍵會出現奇怪的反應,很不好用):

寫入payload:

保存后打開文件管理器,payload被成功執行:

3.PoC3:
攻擊者在本機啟動NC監聽:

攻擊者將payload文件打包掛載至Web服務器中,誘導受害者下載:

受害者解壓文件:

解壓后,payload會被執行,攻擊者接收到反連的Shell:

  • 漏洞影響:雖然直接下載文件很容易引起受害者注意,但攻擊者可以將惡意文件打包為壓縮文件并使用社會工程學誘導受害者解開壓縮包。不管受害者有沒有打開解壓后的文件,惡意代碼都已經執行了,因為文件解壓后KDE系統會調用桌面解析函數。此時受害者就容易中招。

漏洞原理簡析

  • 在Dominik Penner公布的細節中,對該漏洞已經有著比較詳細的解釋。在著手分析漏洞前,我們先學習一下Linux的desktop entry相關的知識。

desktop entry

  • XDG 桌面配置項規范為應用程序和桌面環境的菜單整合提供了一個標準方法。只要桌面環境遵守菜單規范,應用程序圖標就可以顯示在系統菜單中。
  • 每個桌面項必須包含 Type 和 Name,還可以選擇定義自己在程序菜單中的顯示方式。
  • 也就是說,這是一種解析桌面項的圖標、名稱、類型等信息的規范。
  • 使用這種規范的開發項目應該通過目錄下的.directory.desktop文件記錄該目錄下的解析配置。

詳見:https://wiki.archlinux.org/index.php/Desktop_entries_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)

漏洞的產生

KDE的桌面配置解析參考了XDG的方式,但是包含了KDE自己實現的功能;并且其實現與XDG官方定義的功能也有出入,正是此出入導致了漏洞。

在KDE文檔中有如下的話(https://userbase.kde.org/KDE_System_Administration/Configuration_Files#Shell_Expansion):

Shell Expansion

So called Shell Expansion can be used to provide more dynamic default values. With shell expansion the value of a configuration key can be constructed from the value of an environment variable.

To enable shell expansion for a configuration entry, the key must be followed by [$e]. Normally the expanded form is written into the users configuration file after first use. To prevent that, it is recommend to lock the configuration entry down by using [$ie].
Example: Dynamic Entries

The value for the "Email" entry is determined by filling in the values of the $USER and $HOST environment variables. When joe is logged in on joes_host this will result in a value equal to "joe@joes_host". The setting is not locked down.

[Mail Settings]
Email[$e]=${USER}@${HOST}
  • 為了提供更加靈活的設置解析,KDE實現并支持了動態配置,而此處的${USER}尤其令人注意,該項取自環境變量,可以推測,此處與命令執行肯定有聯系。

  • 每當KDE桌面系統要讀取圖標等桌面配置時,就會調用一次readEntry函數;從Dominik Penner給出的漏洞細節中,可以看到追蹤代碼的過程。整個漏洞的執行過程如下:
    首先,創建惡意文件:

payload.desktop

[Desktop Entry]
Icon[$e]=$(echo hello>~/POC.txt)

進入文件管理器,此時系統會對.desktop文件進行解析;進入解析Icon的流程,根據文檔中的說明,參數中帶有[$e]時會調用shell動態解析命令:

kdesktopfile.cpp:

QString KDesktopFile::readIcon() const
{
    Q_D(const KDesktopFile);
    return d->desktopGroup.readEntry("Icon", QString()); 
}

跟進,發現調用了KConfigPrivate::expandString(aValue)
kconfiggroup.cpp:

QString KConfigGroup::readEntry(const char *key, const QString &aDefault) const
{
    Q_ASSERT_X(isValid(), "KConfigGroup::readEntry", "accessing an invalid group");

    bool expand = false;

    // read value from the entry map
    QString aValue = config()->d_func()->lookupData(d->fullName(), key, KEntryMap::SearchLocalized,
                     &expand);
    if (aValue.isNull()) {
        aValue = aDefault;
    }

    if (expand) {
        return KConfigPrivate::expandString(aValue);
    }

    return aValue;
}

再跟進,結合之前對KDE官方文檔的解讀,此處是對動態命令的解析過程,程序會把字符串中第一個出現的$(與第一個出現的)之間的部分截取出來,作為命令,然后調用popen執行:
kconfig.cpp

QString KConfigPrivate::expandString(const QString &value)
{
    QString aValue = value;

    // check for environment variables and make necessary translations
    int nDollarPos = aValue.indexOf(QLatin1Char('$'));
    while (nDollarPos != -1 && nDollarPos + 1 < aValue.length()) {
        // there is at least one $
        if (aValue[nDollarPos + 1] == QLatin1Char('(')) {
            int nEndPos = nDollarPos + 1;
            // the next character is not $
            while ((nEndPos <= aValue.length()) && (aValue[nEndPos] != QLatin1Char(')'))) {
                nEndPos++;
            }
            nEndPos++;
            QString cmd = aValue.mid(nDollarPos + 2, nEndPos - nDollarPos - 3);

            QString result;

// FIXME: wince does not have pipes
#ifndef _WIN32_WCE
            FILE *fs = popen(QFile::encodeName(cmd).data(), "r");
            if (fs) {
                QTextStream ts(fs, QIODevice::ReadOnly);
                result = ts.readAll().trimmed();
                pclose(fs);
            }
#endif

自此,漏洞利用過程中的代碼執行流程分析完畢;可以看到KDE在解析桌面設置時,以直接使用執行系統命令獲取返回值的方式動態獲得操作系統的一些參數值;為了獲得諸如${USER}這樣的系統變量直接調用系統命令,這個做法是不太妥當的。

官方修補方案分析

  • 官方在最新版本中給出了簡單粗暴的修復手段,直接刪除了popen函數和其執行過程,從而除去了調用popen動態解析[e]屬性的功能:

  • 此外,官方還不忘吐槽了一波:
Summary:
It is very unclear at this point what a valid use case for this feature
would possibly be. The old documentation only mentions $(hostname) as
an example, which can be done with $HOSTNAME instead.

總結

  • 個人認為這個漏洞在成因以外的地方有著更大的意義。首先,不太清楚當初編寫KDE框架的開發人員的用意,也許是想讓框架更靈活;但是在文檔的使用用例中,只是為了獲取${USER}變量的值而已。在命令執行上有些許殺雞用牛刀的感覺。
  • 從這個漏洞可以看出靈活性與安全性在有的時候是互相沖突的,靈活性高,也意味著更有可能出現紕漏,這給開發人員更多的警示。
  • 漏洞發現者在沒有通知官方的情況下直接公布了漏洞細節,這個做法比較有爭議。在發現漏洞時,首先將0day交給誰也是個問題,個人認為可以將漏洞提交給廠商,待其修復后再商議是否要公布。可能國際上的hacker思維與國內有著比較大的差異,在Dominik Penner的Twitter下竟然有不少的人支持他提前公布0day,他自己也解釋是想要在defcon開始之前提交自己的0day,這個做法以及眾人的反應值得去品味。

參考資料


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1006/