CVE-2019-14744 KDE4/5命令执行漏洞简析

CVE-2019-14744 KDE4/5命令执行漏洞简析

漏洞简介

  • 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被执行:

文件内容如下:

  1. PoC2:

创建一个文件名为” .directory”的文件:

使用vi写入内容(此处有坑,KDE的vi输入backspace键会出现奇怪的反应,很不好用):

写入payload:

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

  1. 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):

1
2
3
4
5
6
7
8
9
10
11
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给出的漏洞细节中,可以看到追踪代码的过程。整个漏洞的执行过程如下:
    首先,创建恶意文件:

    1
    2
    3
    4
    payload.desktop
    [Desktop Entry]
    Icon[$e]=$(echo hello>~/POC.txt)

进入文件管理器,此时系统会对.desktop文件进行解析;进入解析Icon的流程,根据文档中的说明,参数中带有[$e]时会调用shell动态解析命令:

kdesktopfile.cpp:

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

跟进,发现调用了KConfigPrivate::expandString(aValue)
kconfiggroup.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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]属性的功能:
  • 此外,官方还不忘吐槽了一波:
    1
    2
    3
    4
    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,这个做法以及众人的反应值得去品味。

参考资料