A couple hours after publication the Apple Security Changelogs were updated across the board, and they added me to CVE-2022-26704. I knew this was in the works, but it's still good to see. Thank you :)
This post is a writeup of batsignal, a macOS local privilege escalation bug from my talk:
This is a blogpost I'm dreading to write. It's about my first ever report in the ASB (Apple Security Bounty)
and it was by far the worst experience I ever had in it. It took
332 days to get a bounty, after
2 bypasses and at least
There was a silent fix. There was me forgetting to send an email with a working exploit. It was the opposite
From report to bounty:
- 332 days passed
- I sent in 2 exploits
- 7 total PoCs, if we count minor revisions
- I exchanged ~40 emails with Apple
About the bug
batsignal is an unprivileged user to root
The user can be any user on the system, including
I found it in November 2021 and sent the report to Apple a month later.
Due to collisions and fixes I have developed two variants:
v3 (alfred), but that is tracked
as a separate bug, even though the underlying issue is the same.
In case you are not aware of what
Spotlight is, it's the search service on macOS. Since it indexes the disks on
the system, it's an interesting service for security research purposes.
Spotlight service consists of a couple daemons, the two that we are interested in right now are:
mds- runs as
mds_stores- runs as
There is a third one:
mdsync, but that's not our topic right now. Going forward, by
Spotlight I will collectively
mds_stores. They are doing slightly different things and have slightly different sandboxes,
but both run as
root and that's what matters.
To do it's job,
Spotlight has to have access to files on the system and this is perhaps the reason why these daemons
root, I'm not entirely sure.
We won't investigate the file parsing aspect of the service right now, but instead we will take a look at a curious behaviour I noticed while poking around: the index files of a volume are stored on the volume. Since this works with disk images I can provide, it opens up some interesting avenues to exploitation.
It's worth noting that this is a classic speed vs security tradeoff: On the one hand, carrying the indexes with the volume keeps it available and keeps searches fast. This is good. On the other hand, an attacker can use this feature to try and get remote code execution on a machine or to escalate privileges locally. This is bad.
I'm only bringing this up because
Spotlight does have the ability to store these index files locally: it does so for
network volumes, but not for local disks. Obviously this was a conscious decision and if I had to speculate I'd say it's
done so that volume indexes don't fill up the valuable local disk. The takeaway here is that this is not likely to
Spotlight does file operations on the volume in a
SIP-protected directory called
I don't think there is a point to talk about a concrete bug here, as this is a design issue. The above behaviour is the bug.
At the risk of confusing you, we have to take a detour. You see, running a root daemon on a mounted filesystem by itself is not a huge deal. This is true with one major caveat: It's only safe to do so if you can guarantee that the volume is trusted.
So, on a normal Linux system - as far as I know - a regular user can't mount a volume. Furthermore they can't trigger a root daemon to operate on the freshly mounted filesystem. I might be wrong here, so send a DM if I am.
This is different on macOS. On macOS - to my horror - disk mounting is so ubiquitous that it's one of the first things any user does after installing the system. No prompts, no passwords, no problems. As it happens, also no security.
Before you think that I am off my meds, spewing hyperbole on hyperbole: let me introduce you to some nastiness that is allowed for all users on macOS, including non-admin ones.
Any macOS user can:
- mount their own disk image
- umount, update or move the mountpoint
- set mount flags like
Here are a few ways this can help an attacker:
For #1 they can prepare a malicious image.
For #2 they can "rugpull" daemons, change mount flags, etc...
For #3 they can simply use
noowners so that they can write to any file that only
root should be writing. Arguably
the same could be achieved using other means - like inheriting ACLs - but this is more convenient.
With all of these amazing tools we can easily disarm
SIP, since as far as filesystems are concerned
SIP is just a regex engine.
So, how would we break a regex engine?
Universal SIP bypass on user-mounted volumes
To protect the
SIP will try to match it by name. It - most likely - uses the
mount-relative-regex. I'm saying most likely because that's how it allows
Spotlight daemons access to
it. This is available in the sandbox policies of
mds_stores, but the mechanism by which it prevents everyone
else access is hidden from us. Thus I can't say with 100% confidence that this is the same
mechanism, but it's certainly a safe bet.
With these assumptions in place, we already know how to break the protection. We need to change the name.
Obviously this is not allowed while the volume is mounted, but we can defeat
SIP by unmounting it.
Editing the disk image is like editing anything else: Now it's not a filesystem anymore, it's just a precise collection
There are two major ways to attack:
offlinemeans we can prepare the disk in advance but we can't mess with it while it's mounted
onlinemeans that we can manipulate it while the disk is mounted and the target application is running
While I had some partial success using the
online method, it was usually too limited, with the exception for the
v1. Here it was enough, and was used for mostly convenience reasons.
Developing a more generic and powerful
online attack is still on my todo list. With that said, I will stick with
offline method from now on. It's more limited, but good enough for now. It also has the added benefit of
not requiring command execution on the host, making it an ideal infection vector.
offline (unmounted) disk manipulations we can mess with any protected file/directory:
- we can delete / modify / replace
- we can hardlink to the volume's root for later access
- we can modify permissions
Before we manipulate the image, we have to pick our filesystem. There are many choices, but in my tests
was the best. You might have luck with
HFS (the legacy one) or various
HFS+ modes like,
journaled, case sensitive, etc... You might want to mess with
VFAT (MS-DOS on macOS), or anything else as long as
Spotlight uses it as a backing volume for the indexes. Sadly, network filesystems won't work. That'd be too easy.
For the curious ones here is the full list of filesystems that you can mess with using the command line right now:
mount_9p mount_afp mount_cd9660 mount_devfs mount_fdesc mount_hfs mount_msdos mount_smbfs mount_udf mount_webdav mount_acfs mount_apfs mount_cddafs mount_exfat mount_ftp mount_lifs mount_nfs mount_tmpfs mount_virtiofs
This list is not exhaustive by the way, these are just the ones that come with a console mount command. There are others filesystems supported by xnu proper as well:
$ ls xnu/bsd/miscfs/ bindfs deadfs devfs fifofs Makefile mockfs nullfs routefs specfs union
Also there's 3rd party ones, etc... I don't want to digress too much. I will be using
HFS+ for the rest of this,
you'll see why a little later.
Now we have the filesystem taken care of, what now? Well, we want to make it do things it really should not do. I found two good ways to make this happen.
#1 using a real filesystem driver:
We could use Linux as it has support for a lot of filesystems, and it understands
HFS+ just fine. It has much less
HFS+ driver, so we can use it to create forbidden structures. You have to use force parameters to get
the image to mount as
rw, but for my small scale tests everything worked fine.
Alternatively, we could use anything else that can reliably write to the filesystem of our choice. For this
is an obvious choice, but I did not experiment with it. Nevertheless, it should work fine.
#2 editing the binary directly:
A more barbaric - yet effective - solution is to just edit the binary by hand. During one of my quick and dirty tests I noticed
that I can mess with the
HFS+ volume by search and replace. This worked shockingly well as long as
I was somewhat careful.
It's worthy to note that this is not really possible for any complex - typically newer - filesystems, only simple -
typically older - ones like
Since this was reliable and quick to do locally, I used it in all of my exploits. I could get away with this as I only needed to change a string on the filesystem. For anything more complex you really need to use a filesystem driver.
HFS+ we can simply do the following:
The Python code above would change
900, resulting in
.Spotlight-V900. This change
is arbitrary, any other regex-breaking modification would work. You could also use a larger signature if you don't
exactly match in 2 places. If the signature matches 2 locations, you are good to go.
With this dumb tool in our arsenal we can already see some good results.
So, to bypass the
- we create and mount an
- we turn
mdutil -i on /MY/VOLUME, so
/.Spotlight-V100gets created on the volume
- we unmount
- we edit the binary image to break the regex match
.Spotlight-V900(or anything else)
- we mount
- we do whatever we want under
- we unmount
- we reverse the binary edits
- we mount again
- this time
Spotlightwill run and operate on malicious content
- optionally we can trigger a
Spotlightindex rebuild with
mdutil -E /MY/VOLUME
- this time
Now this is all well and good, but so far we did nothing useful. Let's fix that.
batsignal v1 - symlink attack
Architectural mistakes aside, one of the concrete bugs in
Spotlight was the naive way it wrote files.
Spotlight (correctly) assumes that it's protected by
SIP, it doesn't really make an effort to work with files
I've often seen this in "defense in depth" systems. Once components realize there's a "mitigation" protecting them, they won't focus development effort on security. This immediately defeats the attempt to have "defense in depth".
Knowing this, we can invert our general thinking and think about systems like this not as "hardened" but as "low-effort-enabled".
Spotlight we can trigger insecure file writes by writing a "cacheable" file on the volume. When this happens,
the file's content will get indexed and the extracted text gets written to a new file in the Cache directory.
This would result in a file called
a UUID, and
[X] is the inode number of the original file.
Many file formats are supported, but for simplicity I went with a pdf called
payload.pdf, the content of which
was one line of text:
ALL ALL=(ALL) NOPASSWD:ALL.
There is one minor twist. First
[X].tmp would get written and it would get
[X].txt. The write would
be done with
write(), and in the
open() call symbolic links would be followed.
This is clearly a major vulnerability.
While trying to exploit this however, it became apparent that yet another issue exists:
Spotlight from actually following symbolic links that point outside the volume. This was a smart
move on Apple's part.
Luckily for us,
the policy files explicitly enable
Spotlight to write to
This also includes the permission to follow links to this location. Since that directory is writable by our user, we can just
/etc/sudoers there as
[X].tmp. This defeats the extra protection.
NOTE: In my slides and presentation I omitted this step for simplicity, which might have been confusing. Sorry about that.
- create a disk
- mount (at
payload.pdfto the volume
- wait for the payload file to get indexed
- this step creates the Cache directory and files
- do the disk image edit trick
- move the
Cachefolder to the root of the volume, and replace it with a symlink
- NOTE: this ^^^ step is optional, but really convenient for debugging as we can edit the contents online
- this symlink will be followed, since it's within the volume
- reverse the disk image edit
- create hardlink to
- create a symlink to
- we can do this since
./mnt/Cacheis the real cache directory
- we can do this since
- issue a reindex
- at this point
/etc/sudoerswould be overwritten with our payload
That's all there is to it.
Roughly two months after sending this in I noticed a fix in the current beta that killed this vector:
Spotlight was disallowed from accessing
batsignal v2 - union mount attack
Thinking a bit about bypasses I managed to figure it out fairly quickly: A week after the fix in the beta I have sent in information about the bypass. Crucially I didn't include a PoC. This was a mistake on my part.
The bypass was simple:
Just use union mounts, since
mount-relative-regex is completely blind to them, or rather
are too transparent.
To exploit (this is mostly from my original report):
- create a volume
- mount it, copy payload, activate spotlight
- wait until
Spotlightcreates the Cache folder and contents
- get the UUID
- create the same directory layout outside the volume:
- remount with union
- issue a reindex - This time spotlight will erroneously use our directory outside the volume, allowing us to use hardlinks
- hardlink the
[X].tmp(18.tmp on my machine) file to sudoers as before
- reindex to trigger the overwrite of 18.tmp with our content
This is not terribly different from the previous one, but it achieves the same end result. Since I didn't include a working PoC at first, Apple never got back to me.
I requested an update about a week later at the end of the original 90 day deadline. They said they are "investigating", and asked me if I am willing to withhold disclosure. I agreed. A few days later they requested more information and thanked me for my cooperation.
I sent a working PoC the next day, the first iteration of
There were 4 minor revisions of
v2. One of these (the 3rd one) I just simply completely forgot to send.
That was my mistake, I only realized that after roughly a month of silence from Apple. Oops.
About 3 months later I sent the final revision of
v2. I won't bore you with the details of all of these versions,
they essentially did the same thing.
Yet another 4 months - and a lot of back and forth - later I received the email about the bounty.
If you like timelines, there's a detailed one at the bottom.
The fix was simple:
Spotlight can not access files on union mounts anymore.
I can already spoil the ending for you: it's not going to be enough.
Demo and code
Here's the exploit code: https://github.com/gergelykalman/no-CVE-batsignal-a-macOS-LPE-in-Spotlight
Conclusion of the bug
In conclusion there are major design issues that exist on macOS that are unlikely to get fixed. I
demonstrated in the post how attempts at patching this failed repeatedly. The newest bypass,
alfred (which is
v3) will get it's own writeup soon, but the story is not over yet.
I suspect there will be many more bypasses to come, for one simple reason: an attacker has too much leverage.
To sum this up succintly:
Securing file accesses of privileged applications is impossible if an attacker can control the filesystem.
For all of my efforts of sending bypasses, ~40 emails, countless retests and waiting for 11 months, I was
finally awarded with a bounty of
I was conflicted whether I should talk about this at all, but I feel like there isn't a lot of good information out there about concrete bounties. Apple certainly is not helping this transparency and I understand why. Bugs are really hard to price.
With that said, the bounty amounts on the security website are misleading to a degree that - at the time - I felt like I was cheated. I still feel conflicted about it, even though I have sent in many other reports and I ended up having a much easier time with those. For the newcomers to the program, and for the unfortunate ones who do experience collisions, I felt like it was important to write this down publicly. Also this is somewhat therapeutic for me personally, as I had to spend close to a year with my mouth shut.
I hope this part provides some value to the people who are frustrated by Apple's bounty program, and if you are not interested in my self-indulgent ranting, please feel free to skip it.
Did I get ripped off?
The amount was a lot less than I expected and I was thoroughly bummed out. After feeling angry and somewhat sad, I talked to some friends which helped a lot. Some of them suggested that I should rethink this. So, to do that I needed to figure out a way to come up with an objective valuation.
This is really hard to do without personally being invested in the bug, but it's especially difficult if you are. Nevertherless, I tried to set aside my own biases and expectations.
The Apple security website did not exist then, but it was known that a macOS
LPE is worth more. The -
since released - security website says this is a
$50,000 bug on iOS, starting from the sandbox, with a quality
report and exploit. Please note that this is the maximum payout.
To get a somewhat objective valuation, let's see what I have.
Since app sandbox escapes generally go only for 5k, that requirement can pretty much be ignored, since - correct me if I'm wrong here - it doesn't matter whether parts of a chain are submitted together or separately. Thus I'm happy losing 5k and not have to deal with an app sbx escape.
As for the report quality: mine was definitely not the best, but it was not bad. I sent in multiple valid exploits, performed extensive testing of betas and even sent in bypasses. I would not expect to get a severely reduced bounty here.
Now for the big one: My bug is on macOS not iOS, so it's definitely worth less. It has been publicized that Apple had about 2 billion active devices in January of 2022. Looking at the latest earnings report it seems that iOS outsells macOS pretty consistently by a factor of 5x-7x (source). iPads and other wearables account for about another macOS sized chunk, but the big deal here is obviously iOS. I know that this is only the last 9 months though, so I looked at statcounter.com for futher data. According to them, iOS is about 2x as large as macOS as far as active devices are considered.
This whole analysis might be totally off the rails so I don't want to digress further, but these numbers - roughly speaking - make sense to me.
Taking all of this into account, does
17k still seem like a reasonable bounty? It kinda does. Since it's roughly
1/3 of the
50k - which is the maximum anyway - it is not something I can stay too mad at for too long. It's
acceptable if we can take the maximum payouts at face value.
This also brings us to the bigger question:
Do we think it's okay for one of the richest companies on Earth to award
researchers with a maximum of
$50,000 for responsibly disclosed, weapons-grade exploits + documentation? Exploits
with a blast radius of more than a billion devices, - and not only that - devices that are specifically marketed
as being secure?
This question is silly and rhetorical: Apple pays as much as Apple chooses to pay.
There's not a single thing me or you or anyone else can do to change it, so the real question for us is whether this is worth it, for us, researchers.
I will take a deep dive into that mess in a future blogpost, but as far as this one is considered, I'm glad it's over. I hope you are too.
- 2021.12.12: Report sent to Apple
- 2022.01.11: Issue was reproduced
- 2022.01.18: Fix promised in Spring 2022
- 2022.02.25: I noticed there's a fix in the current beta that removes a crucial directory from the SIP profile, breaking the original exploit. I told Apple this and that I think I can bypass the fix.
- 2022.03.03: First bypass sent to Apple
- 2022.03.05: Apple: investigating the potential bypass
- 2022.03.14: Request for update
- 2022.03.16: Apple: still investigating, will you still withhold disclosure?
- 2022.03.22: Apple: we need clarification (fair enough, I didn't send a PoC, just a writeup)
- 2022.03.23: I sent a PoC (the first version of batsignal v2)
- 2022.03.25: Apple: reviewing
- 2022.04.04: I ask for an update, tell Apple that the bypass works on 12.3.1 still
- 2022.04.12: Apple: still investigating
- 2022.04.19: I ask for an update tell Apple that the bypass works on 12.4 (21F5058e) still
- 2022.04.26: Apple: "We are tracking this as a non-near term solution."
- 2022.05.16: Released fix for CVE-2022-26704 (Description: A validation issue in the handling of symlinks was addressed with improved validation of symlinks.)
- 2022.05.22: I notice CVE-2022-26704 in the changelog, ask for an update as it was originally credited to anonymous
- 2022.05.27: Apple says this is not as a result of my report
- 2022.06.30: I sent in the final version of batsignal v2
- 2022.07.06: Apple thanks me for the new PoC and says that NO change has been made due to my report
- 2022.07.20: Released fix for CVE-2022-32801 (Description: This issue was addressed with improved checks.)
- 2022.07.28: I saw the fix in Ventura so I told Apple, also told them that this is bypassable
- 2022.07.29: Apple appreciates the update and says I should open a new ticket if I can bypass the fix
- 2022.08.22: I notified Apple in the old thread about the exploit working perfectly on latest 12.5.1 still
- 2022.08.23: Apple: no changes have been made due to your report in 12.5.1
- 2022.09.13: I noticed a silent fix in release 12.6, asked Apple about it
- 2022.09.16: Apple says NO changes were made as a result of my report in 12.6, but they're tracking changes Ventura, asked how I'd like to be credited
- 2022.09.16: I realize that I collided with Joshua Mason, told Apple
- 2022.10.26: Apple confirms additional changes required due to my report, asks for the postponing of my disclosure. They say I should get an update regarding my bounty in a few weeks
- 2022.10.26: I confirm that I will wait for the fix
- 2022.11.09: Bounty awarded :)