Airmail 3 is a sleek and featureful alternative to Apple Mail on MacOS. We chose this application as a target for reverse engineering to gain a better understanding of how MacOS applications work on a low level.
We began by downloading the freely available Beta version of Airmail 3 and setting up a new email account for testing. We then extracted the application bundle using 7zip and opened the file named Info.plist within a text editor.
Info.plist is the application’s property list and contains key information about the application.
This file revealed enough information for us to begin mapping out the application’s remote attack surface. We decided to focus our efforts on the custom “airmail://” URL scheme that is registered by the application.
We used IDA to open application binary found in “Airmail_Beta.app\Airmail Beta.app\Contents\MacOS\Airmail Beta”. Our goal was to try and understand how this scheme was implemented through reverse engineering. We began by searching for the string “airmail://” using IDA. This returned the following results:
This revealed that Airmail URIs are structured as URLs. Interestingly, “airmail://message?mail=%@&messageid=%@” includes “message” where a format specifier (%@) exists for other strings. Reviewing cross-references reveal that this string is used in -[LightModel urlformessage].
This method appeared to be building an Airmail URL rather than parsing one, so it was not very useful. However, it did give us the idea to search only for the string “message”. This revealed the following two exact matches:
The cross-references to the cstring revealed several objc2_prop references, but only one CFString reference.
Although the “message” CFString is referenced within several code blocks, the most interesting reference is a constant named commandMessage. After we zeroed in on the commandMessage constant, we noticed that it was in the company of several other command constants. Each of these command constants are referenced in -[AppDelegate getUrl:withReplyEvent:], which is the method in which the airmail URL scheme is processed.
Our analysis of -[AppDelegate getUrl:withReplyEvent:] reveals that parsing of the message command begins with a call to messageForUrl. This method parses an Airmail URL and uses the mail and messageid parameter values to retrieve a message from the database Airmail uses to store email messages.
After messageForUrl, a block invoke function calls -[Logic SelectMessageInTable:] to complete the operation of the message command. We noticed that the archive, delete, reply, and open commands also begin by calling messageForUrl.
However, the compose and send commands relate to new messages, and do not require a mail or messageid parameter. Between these two commands, the send command seemed most useful. We analyzed the parser for the send command and were able to unravel the following URL structure.
In this URL, the only parameter that requires prior knowledge is the account parameter. This parameter determines from which configured Airmail account to send the new email message. Based on our observations, an account name is equal to the account’s associated email address by default. In addition, Airmail’s send command does not require re-authentication. Not only does this allow local applications to send emails through Airmail’s URL scheme, but it also introduces a dangerous phishing primitive.
By default, Airmail 3 allows for HTML content within emails. This means that an attacker may abuse the send command though a hyperlink in an email. Modern applications should typically request permission from the user prior to forwarding requests to custom URL handlers.
Unfortunately, permission is not requested by Airmail, and the user is not prompted when the handler processes the send command. Instead, Airmail will instantly send an attacker crafted email from the target account. At first glance, this may seem like a negligible issue, but this attack becomes much more concerning when file attachments are considered. The send parser identifies attachment parameters by the attachment_ prefix.
If the value of an attachment parameter relates to an accessible file path, then the file is attached to the outbound message i.e. any file accessible by Airmail 3. Of course, this requires that an attacker know the path of the file they would like to attach. Luckily, relative file paths are acceptable values, and “../../../../” references the user’s home directory.
Airmail 3 stores its application data within SQLite databases. Reviewing the table schemas of several Airmail databases revealed that email messages are stored within a database named Message_0_1_50000.db for each account. The path to this database is relatively deterministic and is built as follows:
../../../Group Containers/2E337YPCZY.airmail/Library/Application Support/ it.bloop.airmail.beta11 /Airmail/[email protected]_1/ Message/Message_0_1_50000.db
Given this information, it becomes clear how an attacker could abuse the deterministic path for Message_0_1_50000.db and craft a payload that exfiltrates a user’s emails by attaching this database to an email sent to themselves. We demonstrate this attack scenario in the video below:
To better understand what was going on, we consulted WebKit’s source code. According to the source, OBJECT elements are defined by the HTMLObjectElement class, which is a sub-class of HTMLPlugInImageElement. Other sub-classes of HTMLPlugInImageElement include HTMLEmbedElement and HTMLAppletElement. Overall, the source code cleared up several things for us:
The last discovery was most enlightening, as it was revealed through testing that FRAME elements were blacklisted, and IFRAME elements refused to load any resources. At this point, we decided to debug the application with lldb to learn more about what was going on. While reviewing the application’s disassembled code, we had come across the method -[Logic OpenURL:] several times.
According to Apple, OpenURL simply opens the location at the specified URL, so we had a suspicion that this was the method eventually called to open our HTML Plug-In resource. We used lldb to set a break point on this method, then triggered the bug. After the breakpoint was hit, we reviewed the thread backtrace to see how it was called.
(lldb) thr b
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000107395e10 Airmail Beta`-[Logic OpenURL:]
frame #1: 0x00000001073fc748 Airmail Beta`-[TheWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 6136
As is seen above, -[Logic OpenURL:] was called by -[TheWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:]. webView:decidePolicyForNavigationAction:request:frame:decisionListener: is responsible for routing a navigation action internally or to an external viewer.
We were able to locate this call to OpenURL in IDA by jumping to the method at the offset referenced in frame #1. Working backwards through the code blocks revealed a very clear path to this OpenURL call. One of the first things we noticed was that within the first code block of this method, any navigation requests made by IFRAME elements are blacklisted. This explained why IFRAME resources refused to load while Plug-In element resources occasionally did.
This incomplete blacklist of HTMLFrameOwnerElements resulted in a partial bypass of the WebView’s navigation policy handler. Regardless, this did not explain why Plug-In element resources were only occasionally loaded.
By stepping through the method with lldb, we were able to identify the conditions that would cause our navigation requests to diverge from the desired code path. There are a series of checks that must be passed to continue along the path towards OpenURL. The checks depend on the value at [rbp+var_C8], which contains a pointer to the currentEvent of the WebView’s window.
In the case of most navigation requests, the currentEvent is NULL, which would fail the first check. However, requests triggered by a HTMLFrameOwnerElement will include a currentEvent reference populated with the most recent Event. The second check compares the Event type of currentEvent to the value 0x02 then 0x1A. If the Event type does not match either of these values, then the second check is failed.
We were able to find Event type definitions in IOLLEvent.h from Apple. Event type 0x02 corresponds to the NX_LMOUSEUP Event and 0x1A to NX_OMOUSEUP. These checks appear to try and confirm that a navigation request was triggered by clicking a hyperlink.
However, our partial bypass of the navigation policy using Plug-In elements introduces a race condition in which a Frame navigation request may be fulfilled if the currentEvent is either NX_LMOUSEUP or NX_OMOUSEUP. This explained why clicking an email message resulted in the processing of a Plug-In resource by OpenURL on a seemingly random basis; The action of clicking an email message concluded with a NX_LMOUSEUP Event!
Despite its lack of reliability, this attack vector is particularly dangerous due to the minimal user interaction required for exploitation. Many users are trained to avoid clicking untrusted links in an email, but how many are trained to avoid clicking the email message itself? This security issue allows for the previously described payload to be triggered when a user clicks to view an email message. See the video below for a demonstration of this attack scenario:
At VerSprite, we approach security from a holistic risk management perspective, understanding security from business and attacker perspectives.
Our approach goes beyond assessing security controls. We examine credible threats to understand the likelihood of a real-world abuse case and measure the magnitude of business impact if a breach should occur. By developing a holistic business risk view, security decisions become business decisions. Explore Security Offerings →