作者:0xcc
原文链接:https://mp.weixin.qq.com/s/vfPxiLqOVZWhFde_2fKf1Q
Dash App 是 macOS 上一款非常流行的查看离线 API 文档的应用,由个人开发者@kapeli 发布。支持离线文档查询和多种 IDE 的集成,对软件开发者是一款极为实用的生产力工具。在相当多数的 macOS 使用攻略上都能看到这款软件的推荐。
而在 2018 年我向 Dash 的开发者报告了一些安全问题,并将其设计为第一届 RealWorld CTF 预选赛的题目。出人意料的是,来自各国的 CTF 选手在很短时间内找到了更为严重的远程代码执行漏洞。在赛后和开发者邮件沟通后,Dash 很快推出了补丁之后的版本。
在 2019 年 12 月 3 日的 Dash 5 更新(https://blog.kapeli.com/dash-5)之前,Dash 一直用的是旧的 WebView API。后来因为 WebView 被标记为过时,升级到了 WKWebView 控件。
而下面提到的安全问题,无一不与 WebView 遗留 API 的设计有关。
Dash 在展示文档内容的时候主要使用 HTML 格式。在 mac 上,许多程序使用 bundle(包)来组织可执行代码和文件内容。Dash 的 bundle 后缀名为*.docset,包含如下内容:
Dash 从网上下载 docset 之后,将保存在本地。因此 WebView 当中实际出现的 URL 就是 file:/// 域下的。
WebView(对应 iOS 上的 UIWebView)载入 file 域会直接导致 UXSS。WebView 默认允许了 allowFileAccessFromFileURLs 和 allowUniversalAccessFromFileURLs,所以通过 AJAX 可以以绝对路径读取任意本地文件的内容,并发送到远程服务器。
xhr = new XMLHttpRequest();xhr.onopen = function() { alert(xhr.responseText);}xhr.open('GET', 'file:///etc/passwd', true);xhr.send();
不过在 2018 年初的 Dash 版本这招不起作用。在 WebView(和 UIWebView)中可以通过实现 NSURLProtocol 的子类来拦截特定 URL scheme 的网络请求,实现自定义的资源加载逻辑。这个类对于有 iOS 应用开发的读者来说不会陌生。
Dash 当时主要用 NSURLProtocol 实现了两种场景:
当时的 Dash 就意识到了 file:// 域文件可以任意读取的问题,便限制了只能访问 docset 包内的路径。
不过我找了一个简单的绕过。docset 本质上是一个文件夹,因此从网上下载的都是压缩好的 tar.gz 格式——而压缩包支持符号链接。因此只需要创建一个根目录 “/” 的符号链接,即可重操旧业——偷文件。
因此攻击场景就是,在存在漏洞的 Dash 上下载导入了一个恶意的 docset,浏览这个文档可能导致计算机上任意文件(如 SSH 私钥)被窃取。
Dash 在阅读文档时还有一个分享功能,会随机选择一个端口开启 HTTP 服务,网址类似:http://127.0.0.1:60815/Dash/hpzzlcsf/nodejs/api/os.html
前文提到的符号链接问题在这里同样奏效,不过这个服务是基于 GCDWebServer 实现的,所以还有一个更显而易见的路径穿越漏洞。在请求的路径中使用 ..%2F 可以被解码为 ../ 字符,从而远程读取任意路径文件。
这种攻击不需要特殊的 docset 包,只要局域网内扫描到这个服务器端口即可。
前面提到的 UIWebView UXSS 问题除了能读文件之外,还有一个不起眼但是危害不可小觑的任意 http 请求问题。不过这个问题需要有其他软件的协同,这也是 RealWorldCTF 最开始的出题思路。
近年来 VSCode 为代表的编辑器基于 node.js 和 Electron(或 CEF)技术,使用 HTML 开发界面,极大方便了扩展的生态和功能的迭代。虽然运行资源吃得不少,但是带来的体验还是让许多用户大呼真香。
在 VSCode 的历史版本(1.19.0~1.19.2)当中存在一个远程代码执行漏洞。这一系列版本的 VSCode 错误地在生产环境打开了 Electron 的远程调试端口,任何能发起跨域 http 请求的网页,都可以通过访问如下 URL 获得一个 token:http://127.0.0.1:9333/json/list
[ { "description": "node.js instance", "devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9333/c5408ce2-6f06-4a7e-a950-395d95c6804f", "faviconUrl": "<https://nodejs.org/static/favicon.ico>", "id": "c5408ce2-6f06-4a7e-a950-395d95c6804f", "title": "/private/var/folders/4d/1_vz_55x0mn_w1cyjwr9w42c0000gn/T/AppTranslocation/EE69BB42-2A16-45F3-BB98-F6639CB594B1/d/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper", "type": "node", "url": "file://", "webSocketDebuggerUrl": "ws://127.0.0.1:9333/c5408ce2-6f06-4a7e-a950-395d95c6804f"} ]
其中的 webSocketDebuggerUrl 可以直接建立一个 WebSocket 连接,接着使用 Chrome 远程调试协议(基于 JSON 和 WebSocket)即可向 node.js 解释其注入任意 js 代码,从而控制用户的计算机。
在受影响的版本当中,这个 9333 端口存在一个 DNS rebinding 问题,可以通过短时间内切换 DNS 解析的结果来绕过浏览器同源策略获得 localhost 的内容,接着 WebSocket 默认不限制跨域访问,导致任何浏览器只要访问了攻击者的网站停留大约两分钟即可被入侵。
VSCode 的修复方案是增加了针对 dns rebinding 的校验,并在后续版本中随机化调试端口。在写这篇文章的时候,所有的调试端口已经不再开启。类似地,另一款来自 Adobe 的编辑器 Brackets 也采用了 CEF 和 HTML 的方案。
虽然两个编辑器都修复了 DNS rebinding 问题,导致这个端口的响应内容无法跨域获取,不过回到 Dash 的 WebView 上,我们前面已经说了这个 UXSS 是没有同源策略限制的。
假如让 Dash 和(存在漏洞的)Brackets 或者 VSCode 同时运行,在 Dash 当中打开的恶意文档,就可以直接通过向调试端口发起请求注入 js 代码的方式执行任意本地代码。
217 战队使用预期解法在 RealWorldCTF 解出了这个题目:
https://blog.l4ys.tw/2018/07/realworld-ctf-2018-doc2own/
然而在比赛期间我们收到了非预期的 0day 解法。为了让用户有时间升级,这些 writeup 从未公开过。
PPP 战队和 CyKOR 使用了同一个命令注入问题。
我们前文提到,Dash 通过 NSURLProtocol 处理一些预定义的 URL 请求,其中有一个 dash-man-page://,会打开一个终端窗口并运行 man 命令。
”结尾,并包含一对完整的括号时,Dash 会从网址中提取字符串并拼接到 bash 命令。因此可以直接命令注入:
dash-man-page://load?query=open -a Calculator
(1)
此外如果 docset 当中存在一个名为 cat2html 的 shell 脚本,也会执行。
Eat, Sleep, Pwn, Repeat 使用了另一个(不够完美)的命令执行问题。
在 WebView(UIWebView)里提供了一个 Api,可以直接在网页的 JavaScriptCore 运行时(参考 JSContext 类)当中提供额外的函数和对象:
之前的 Dash 应用在 js 里注入了一个 window.dash 对象,可以访问 DHWebViewController 上的方法。
通过 JSContext 注入的 Objective-C 方法有命名转换规则。如果对象定义了 webScriptNameForSelector: 方法,则优先使用该方法中自定义的名字;默认情况下,方法名(Objective-C selector)当中的冒号会转义为下划线(_),而下划线则使用 替换为连续两个美元符号。
例如 Objective-C 当中的方法是 setFlag:,在 js 里调用时写作 obj.setFlag_(flag)。
另外对象可以定义一个 isSelectorExcludedFromWebScript: 方法来控制 js 能使用的 selector 列表,相当于一个白名单。在 Dash 里这个方法实现如下:
char __cdecl +[DHWebViewController isSelectorExcludedFromWebScript:](id a1, SEL a2, SEL a3)
{
return "coffeeScriptOpenLink:" != a3
&& "showFallbackExplanation" != a3
&& "openDownloads" != a3
&& "openGuide" != a3
&& "jsGoToURL:" != a3
&& "openDocsets" != a3
&& "openProfiles" != a3
&& "openGift" != a3
&& "loadFallbackURL:" != a3
&& "setUpTOC" != a3
&& "version" != a3
&& "unityConsoleLog:" != a3
&& "msdnMakeActive:" != a3
&& "openExternal:" != a3
&& "switchAppleLanguage:" != a3
&& "toggleAppleOverview:" != a3
&& "openIOSLink" != a3
&& "openPawLink" != a3
&& "closeAnnounce" != a3
&& "useSnippet" != a3;
}
其中的 openExternal: 方法从 js 接收一个字符串参数,转为 URL 之后直接用系统默认的关联协议打开:
当我们传入一个可执行文件的 bundle 的 file:/// URL 时,相当于在 Finder 里双击执行 app,也就是一个代码执行向量。ESPR 就在 docset 里嵌入了一个可执行的 .app,然后通过 js 运行:
var url = location.href.toString()
.replace('some.html', 'some.app') // file:///path/to/.app
window.dash.openExternal_(url)
这个方式有一个局限性。如果 docset 是从网上下载回来的,解压之后的文件会被标记 com.apple.quarantine 属性。双击运行其中的 app 会触发 GateKeeper,有可信的数字签名会提示用户是否继续运行,签名无效则会提示文件已损坏。
因为比赛的运维系统使用了其他上传方式,就没有受到 GateKeeper 影响。
在赛后我很快联系了作者,并找到了更多的攻击向量:
作者修复了命令注入,并在运行可执行文件前检查代码的数字签名,也检查了线上仓库的文档以确保此前没有被恶意上传过。
针对开发者直接投毒的攻击近几年时有发生,类似 Dash 这样流行的生产力工具也不失为一个可能的方式。通过举办一场 CTF 的方式竟然在极短时间内找到了存在实际危害的数个 0day 漏洞,确实硬核。
这篇文章里提到的一些具体案例和 WebView 这个遗留 API 的设计存在很大关系,苹果在 iOS 12(2018 年最新的系统是 10)之后明确标记 UIWebView 为过时 API,还在应用商店审核规则中加强限制对老旧 API 的使用,也是为了提升安全性和性能。对开发者来说升级控件并不是一个简单的查找替换过程,不过为了用户考虑,还是得做一些牺牲。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1548/