sqlol (CVE-2023-32422) - a macOS TCC bypass

Posted on 2023-11-15 in blog

Wow, two blogposts in two days! Is this a new writeup schedule?

No, it's not. But, since I'm presently just ill enough to not be productive, yet well enough to write, I figured I'd chip away at my horrendous (writeup) debt while I wait for the immune fairy to arrive.

Yes, it's another blogpost about a bug in my talk, and this time it's "sqlol".

This was a stunning bug and one that I was shocked to find. It was a clear oversight on Apple's part, and thus a fairly easy bug to exploit and report.

The handling of the report got quite complicated, and that is partially my fault. Apple was a great sport though, so it was one of the more pleasant experiences I had with the ASB.

Background

This bug was also found by my automation framework I discussed in the post about lateralus. In fact, this was the first bug I found using the framework, and one of the first bugs I ever submitted to the ASB. Let's check it out.

The bug

During my analysis of FDA entitled applications I encountered a lot of peculiar environment variables. There was one that stood out to me, as it's name was ominous: SQLITE_SQLLOG_DIR

Now I'm not terribly familiar with sqlite, but I do know that logging things this way can be very dangerous. A common library like libsqlite will be used in many applications - including privileged ones -, and since the destination directory is attacker-provided, it might lead to an escalation of privilege.

What does it do?

The environment variable SQLITE_SQLLOG_DIR turns on debugging functionality in libsqlite, which results in various debug files being written to a location of an attacker's choice.

These files include:

  • a FULL copy of any opened sqlite DB
  • a list of SQL statements executed by the connection
  • an index file mapping database names to these file names

This functionality only exists in the code if (lib)sqlite is compiled with the SQLITE_ENABLE_SQLLOG preprocessor symbol.

Here's the responsible file: https://www.sqlite.org/src/doc/trunk/src/test_sqllog.c

The file is very well documented, so we can rely on it for information:

** OUTPUT:
**
**   The SQLITE_SQLLOG_DIR is populated with three types of files:
**
**      sqllog_N.db   - Copies of database files. N may be any integer.
**
**      sqllog_N.sql  - A list of SQL statements executed by a single
**                      connection. N may be any integer.
**
**      sqllog.idx    - An index mapping from integer N to a database
**                      file name - indicating the full path of the
**                      database from which sqllog_N.db was copied

Somehow this - obviously dangerous - debug functionality got compiled in and shipped to millions - if not billions - of devices. Uh-oh.

The impact

Information leak

As a result of this functionality being exposed to any user, an attacker is able to leak any sqlite database. By using symlinks to these sqlite databases, a privileged app like Music could be used to leak any sensitive sqlite database, since it possesses the FDA (Full Disk Access) entitlement. To note, any other applciation with FDA could also be used, as long as they use libsqlite.

This is a severe infoleak, but since it can only be used to leak sqlite databases - of which there's many - it's not as bad as it can get. I want to use this to become a TCC bypass.

TCC bypass

You see, I noticed that these file writes were done with absolutely zero hardening in mind. This kind of makes sense, since the functionality was never really meant to run on a production system.

When writing the files, the open() system calls:

  • had predictable filenames
  • would follow symlinks
  • would overwrite files

Putting all of these together, these can be used trivially to overwrite the user's TCC.db and gain complete access to all the private, TCC-protected databases, contacts, location, microphone, webcam, and whatever else TCC is meant to protect.

There was only one issue: the content was only partially controlled, and not to an extent that would have allowed me to overwrite the entire TCC.db with whatever I wanted.

Knowing this, I submitted the inital report to Apple about the infoleak vector, while I tried to figure out how to elevate this to a full TCC bypass. This seemed like a remote possiblity at the time.

sqlite smuggling

After the report I moved on to other bugs, and I paid no mind to this one. I only came back to it occasionally, but I'd always fail to make it happen. I could control bytes going into the files, but that was useless, without a good target.

The only thing I could presumably overwrite was the user's TCC.db, but it being an sqlite file, it would have resulted in a crash or a corrupted database. This was unlike /etc/sudoers, that is happy to parse any kind of garbage.

I had to either:

  1. find a privileged target file that tolerated garbage bytes, or
  2. control the entire content being written, and target the user's TCC.db

For #1, I don't have a solution, to this day. If you do, send me a DM as this is something that would turn a lot of my findings exploitable.

For #2, this seemed like a problem. There was no way in hell I could get away with partially writing the sqlite DB, neither could I control the write from the first byte. At best I could overwrite sqlite DBs entirely, or using the query log - and perhaps some trickery - to do a partially-controlled write.

It took some time for this to click, as I haven't considered something crucial:

I could make the original sqlite files come from me.

Since I have access to Music's folders, there is nothing that keeps me from overwriting Cache.db (for example), and then using the copy of that to overwrite the user's TCC.db. This way, I could put whatever tables and rows in the resulting database, which ought to be enough.

I have made a demo exploit, but it kept failing. Music was unhappy with not finding the tables that it'd normally expect. So, even though I overwrote the user's TCC.db with arbitrary content, Music would reset the entire DB when it realised that tables were missing.

To prevent this, I could try killing Music after the copy is made, but the resulting exploit would be pretty unreliable. So, instead I decided to "smuggle" a valid TCC.db into Cache.db. I can do this without a problem, as a sqlite DB can contain lots of tables. It's also unlikely that a given program would check all tables in the database or for the table's names to collide.

Since the table's names were different, I wrote a quick and dirty PoC, and to my surprise this worked perfectly. TCC.db got overwritten, carrying with it the list of tables that a valid Cache.db would have.

This is game over, at that point I had full control over the contents of TCC.db. Well, full control over the tables that matter anyway :)

The exploit

With the sqlite smuggling trick in our pocket, we can easily construct an exploit.

  • dump a valid TCC.db to tcc.sql
  • dump a valid Cache.db to cache.sql
  • merge the two sql files, use that to create payload.db
  • use payload.db to overwrite Music's Cache.db
    • any other app and DB would also work, as long as they don't touch the extra tables we smuggled in
  • trigger the bug with a directory location we control
  • stop the program
  • see what files get generated
  • symlink the file that contains the DB copy to TCC.db
    • if we use SQLITE_SQLLOG_REUSE_FILES=1, we can rely on these filenames staying the same
  • trigger the bug again to overwrite our target file

Demo and code

Demo video

The full exploit code is on my github: https://github.com/gergelykalman/CVE-2023-32422-a-macOS-TCC-bypass-in-sqlite

The fix

To fix the bug, Apple simply removed this functionality.

Root causes

This was a fairly trivial bug, a pretty clear oversight on Apple's part. This functionality should have never been compiled in on a production build, and frankly it came as a huge surprise to me that it existed. Also that it was not discovered much earlier by someone else. To that point:

The collision

Months deep into the process, Apple deemed this a collision with another researcher: Wojciech Reguła. I had to find this out from the Apple Security Releases, as they haven't told me.

At the time I was doubtful whether I was the earlier submitter, so I decided to reach out. Now, I know Wojciech for a while now and he's a great researcher as well as a super nice guy. We discussed timelines, and it seemed that I was a few months earlier.

After adjudication Apple confirmed that I submitted first. Sorry Wojciech!

Apple's response time and communication

Response time: not good enough

The fix went out in 180 days since my initial report. This is a long time and I feel partially responsible. The initial report got filed as an infoleak, since I didn't know that a more serious PoC can be produced. With that said, it should not have taken Apple this long to fix the bug, even if it's "just" an infoleak.

They certainly could have done a better job here.

Communication: great

This ticket was one of those where the communication went really well. It's great when that happens as it can really soften the blow of an otherwise frustrating experience.

This time around I was definitely the one to blame for muddying the waters, as it was unclear to me how the parts of a chain would be paid out, etc... Thankfully the person on the other end was great, and they held my hand and they answered my questions. They were helpful and great in general. Thanks for that!

The bounty

Apple awarded me $30,500 for this bug, which was pretty fair. Like lateralus, I thought this could affect iOS as well, but I had to conclude that that was not the case. For a TCC bypass on macOS, it's a pretty good amount.

Conclusion

All in all this was a pleasant experience, the only problem I can bring up is that it took a long time. That, and perhaps the multiple issues with the ticket status.

Aside from that, this is how bug bounties go in a roughly ideal case. There's a proper report with a PoC, a helpful and (really) responsive triager, and a satisfying resolution at the end.

Personally there were some things that I learned while I looked back on this:

#1: Always submit unique issues separately

You will get paid for each bug (if they qualify). There's no need to introduce confusion by squeezing multiple bugs into a single ticket: One bug, one impact, one PoC, one ticket.

Now this can't always be done, you might not know about a better impact until you perform later research. It usually makes sense to report the bugs as early as you can, to avoid any collision issues (sorry Wojciech), but if you only figure out a higher impact later on, you might need to face a much longer fix time.

There's nobody to blame for this, it's just how it is sometimes.

#2: Always submit an exploit with max impact (if you can)

This is self-explanatory, but having a reliable, high-impact PoC is a great way to expedite the process. If the Apple engineer on the other end has a happy time with reproduction, you will have a short timeline. Make the exploits as good as you can if you want this to go fast.

This is not a guarantee though, if you found a bug in a particularly nasty to fix place, it might take ages for Apple to get this done. Unfortunately, this is also something that we have to live with.

Advice / Request for Apple:

Considering how I spent quite a bit of time discussing questions about the program and not about the bug in question, it occurred to me that setting up a separate "ASB Support" communication channel might make sense. This way I could directly ask these questions separately, and avoid taking valuable time away from triagers, as well as it would be a lot less distracting for everyone involved.

Timeline

  • 2022-10-28: Report sent to Apple, including the infoleak exploit in BASH (at this time it was not clear whether this is more than an infoleak, but I knew that it can also do a partially-controlled file write, which I also put in my report)
  • 2022-11-12: I sent the same exploit in, rewritten in obj-c
  • 2022-11-14: Apple is investigating
  • 2022-11-16: Apple asks if I had app sandboxing enabled as they didn't see it
  • 2022-11-16: I confirm I had it enabled
  • 2022-12-07: I tell Apple that I see my exploit breaking now. I ask if they managed to reproduce it with sandboxing on at all and whether the sqlite exploit and the sandbox escape should be separate tickets
  • 2022-12-08: Apple confirms they did not, but they are addressing the issue anyhow. They also inform me that I should submit the sandbox escape separately if it's not related to sqlite
  • 2022-12-09: I ask if this would count as a full chain still and whether I should submit separately
  • 2022-12-10: Apple says unique issues should be reported separately, they say nothing about what happens if a chain is sent in like that, but they reiterate that each will be paid according to the ASB's rules
  • 2022-12-15: I submit the TCC.db overwrite exploit for the first time. I also submit the sandbox escape issue separately in another ticket
  • 2022-12-15: Apple says they're reviewing it
  • 2023-01-18: I request an update
  • 2023-01-18: Apple thanks for my patience and says this will be fixed in a future release, after which it'll be evaluated for bounty
  • 2023-03-06: I check on the ticket and now it's silently closed. I tell this to Apple
  • 2023-03-06: I message them again saying that my exploit still works on 13.3beta2
  • 2023-03-07: Apple apologises about the ticket status issue, confirms that this will be fixed. Apple asks for more time essentially, and tells me the report only qualifies for the bounty after it's fixed. Apple thanks me for my work.
  • 2023-03-28: I ask for an update as I saw no fix in the latest release, I tell Apple that this will be public at the time of the OBTSv6 conference (in about 6 months)
  • 2023-03-29: Apple says a new fix will be issued in the following months, they confirm that I will be credited.
  • 2023-04-26: I spot a fix in 13.4beta3, I let Apple know, the fix looks solid but I only glanced at it
  • 2023-04-26: Apple thanks me, says they'll reach out if they have an update
  • 2023-05-15: I report to Apple that the CVE's text is incorrect (it says the bug is an infoleak, possibly due to me picking a bad title for the report and not sending the TCC.db overwrite initially)
  • 2023-05-18: Apple says the text is correct and won't be changed
  • 2023-05-18: I ask Apple to confirm whether this is correct in which case I'd be publishing the information
  • 2023-05-18: I resend the earlier exploit as it seemingly fell through the cracks
  • 2023-05-19: Apple says they're retesting, thanks me for following up
  • 2023-05-22: The ticket moved to "resolved" status, I ask Apple why
  • 2023-05-23: Apple confirms that they could reproduce the TCC bypass, and that they'll be updating the CVE text to reflect this. They confirm the bug will be adjudicated and they thank me for my time and patience and apologise for the confusion.
  • 2023-05-23: I apologise for the confusion as well as the title doesn't say TCC bypass. That did not occur to me as a possibility when I originally filled out the report.
  • 2023-05-23: Apple thanks me and says it's no problem
  • 2023-06-02: Apple reaches out and tells me the CVE text got updated, with another comment incoming after adjudication
  • 2023-06-02: I tell them that this is great news, however it became a collision somehow (with Wojciech Reguła). I ask why that is and when will the bug be adjudicated.
  • 2023-06-02: Apple says they'll have more info after the adjudication is complete.
  • 2023-06-08: Apple says the bug is awarded with $30,500
  • 2023-06-08: I thank Apple, I ask why it's only 1/3 of the advertised max
  • 2023-06-08: Apple says that the issue is only exploitable on macOS, but they welcome me to prove them wrong. (fair enough)
  • 2023-06-09: I tell Apple I'll look into it
  • 2023-06-09: Apple says I can take my time
  • 2023-06-11: I tell Apple that I got nothing substantial, the env var setting is not possible on iOS, like with my other bug. I accept the bounty and thank Apple for their hard work.
  • 2023-06-12: Apple thanks me for giving it a shot and we discuss payment info