作者:
HACHp1@知道创宇404实验室
日期: 2019/08/08
原文链接:
https://paper.seebug.org/1006/
漏洞简介
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 ExpansionSo 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 EntriesThe 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}
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 featurewould possibly be. The old documentation only mentions $(hostname) asan example, which can be done with $HOSTNAME instead.
总结
参考资料
[1] 漏洞细节:
https://gist.github.com/zeropwn/630832df151029cb8f22d5b6b9efaefb
[2] 发现者推特:
https://twitter.com/zer0pwn/status/1158167374799020039
[3] 演示视频:
https://www.youtube.com/watch?v=l4z7EOQQs84
[4] 官方修复细节:
https://mail.kde.org/pipermail/kde-announce/2019-August/000047.html
[5] 修复补丁:
https://cgit.kde.org/kconfig.git/commit/?id=5d3e71b1d2ecd2cb2f910036e614ffdfc895aa22