Dissecting a Polymorphic WebP Donut Stager Delivering CrystalX RAT
Overview
A new campaign popped up recently serving a polymorphic threat disguised as a WebP image file (app_JnCFbH.webp) hosted on AWS (documented on URLhaus #3910780).

If you browse directly to the staging URL, you get a blank black page with a broken image icon:

However, hidden inside this container is a multi-stage loader pipeline designed to deliver CrystalX RAT (CrysRat).
Let's dissect how this delivery chain works, how to unpack the final payload, and how its embedded C2 configuration was decrypted.
CrystalX RAT Polymorphic Stager
A multi-stage loader delivered under the guise of a WebP image file, utilizing rolling subtractive XOR and Donut encryption to execute a secure CrysRat agent.
app_JnCFbH.webp)Decryption Chain
The malware authors put together a neat assembly line to unpack the payload in memory without writing it to disk:
Stage 1: x64 GetPC Shellcode
The WebP image file contains a high-entropy data block appended to the end of the image. The stager starts by finding its own position in memory (a standard "GetPC" technique) and executes a basic stub to unpack the first layer in-place.
Type: polymorphic shellcode stagerStage 2: Backwards Subtractive XOR
Once the initial shellcode is active, it runs a backwards loop over its own payload bytes, subtracting a rolling key byte from each position. It's a simple but effective trick to break static signature detection.
Type: subtractive XOR decryption loopStage 3: Donut Loader Execution
Donut wraps .NET assemblies in a native shellcode wrapper. The stager runs the Donut code, which extracts its configuration block, decrypts the embedded payload using the Chaskey block cipher in CTR mode, and decompresses it using APLib.
Type: reflective in-memory assembly loadingBreaking Down the Unpacking Tricks
The Backwards Subtractive XOR Evasion
Why did the malware authors write a subtractive rolling XOR loop that runs backwards?
Standard security tools often look for repeated byte sequences or run simple linear XOR key solvers to check for hidden executables. By processing the byte array from the end of the file backwards (len(cipher) - 1 down to 0), and using the ciphertext byte from the previous step as the next key, the stager creates a non-linear decryption stream. This effectively breaks static pattern detectors, making the file look like high-entropy noise until the code runs.
Here is the Python equivalent of this backwards rolling loop:
# Simulating the backwards rolling subtractive XOR loop
cipher = bytearray(raw_data)
key = 0x6f
for i in range(len(cipher) - 1, -1, -1):
temp = cipher[i]
cipher[i] = (temp - key) & 0xff
key = temp
Reflective Loading with Donut
Once decrypted, Stage 2 hands execution over to Donut (a popular, open-source reflective loader framework). Donut is a devious choice for threat actors:
- CLR Bootstrapping: Since the main payload is written in .NET (which requires the Windows Common Language Runtime to run), Donut's native x64 shellcode bootstraps the runtime from inside a host process. It resolves APIs like
CorBindToRuntimeExto load the CLR directly into memory. - AppDomain Hijacking: Once the CLR is active, Donut boots up a new AppDomain and loads the assembly (
app_JnCFbH_payload.exe) entirely in-memory. Because no .NET file is written to the hard drive, standard file system hooks and basic EDR scans are completely bypassed. - Double Layer Cryptography: The stager payload within the Donut container is additionally protected using the Chaskey block cipher (in CTR mode) and compressed via APLib to keep the footprint as tiny and obfuscated as possible.
Configuration & Key Derivation
CrystalX RAT keeps its configuration settings (like the C2 address, installation folders, and certificate keys) hidden as encrypted Base64 strings inside the .NET assembly's static fields.
To decrypt these values, replicate the stager's custom key derivation. The malware uses PBKDF2 (Rfc2898DeriveBytes) with a hardcoded SHA-1 password hash, a static 32-byte salt, and 50,000 iterations to derive a 32-byte AES-256 key.
Encrypted Value Layout
Each encrypted string field follows a simple binary structure:
- HMAC Checksum (first 32 bytes): Used to verify the integrity of the data.
- AES Initialization Vector (next 16 bytes): The random IV used for CBC mode.
- AES Ciphertext (remaining bytes): The actual encrypted data.
Extract and decrypt these fields using the following Python script:
import base64
import hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# Key derivation parameters (redacted for safety)
password = b"<redacted>"
salt = bytes([<redacted>])
iterations = 50000
# Derive the AES-256 Key
derived = hashlib.pbkdf2_hmac('sha1', password, salt, iterations, dklen=96)
aes_key = derived[0:32]
def decrypt_crys_string(b64_str):
data = base64.b64decode(b64_str)
iv = data[32:48]
ciphertext = data[48:]
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted = decryptor.update(ciphertext) + decryptor.finalize()
# Trim PKCS7 padding
pad = decrypted[-1]
return decrypted[:-pad].decode('utf-8')
Decrypted Config Values
By applying the decryptor to the stager's fields, the plaintext C2 configuration can be pulled:
Version: '1.1.0' (CrystalX Client Version)
C2 Server: '47.237.101.246:6700;' (Command & Control Host)
Install Folder: 'SubDir' (Target folder under AppData)
Filename: 'Client.exe' (Dropped stager name)
Mutex: 'e362ccdb-9861-4423-82c1-34c4413b08b4' (Bot campaign identifier)
Auto-Start Key: 'Crys Client' (Registry run key name)
TLS & Command Verification
CrystalX doesn't communicate using standard WebSockets or HTTP. Instead, it establishes raw TCP socket connections using asynchronous calls (BeginConnect/BeginReceive) wrapped in TLS via an embedded copy of the BouncyCastle library.
The malware developers also implemented solid security checks to protect their botnet from hijackers and security analysts:
- C2 Certificate: The stager embeds a complete self-signed X.509 certificate (
CN=Crys RAT Server CA) signed with a 4096-bit RSA key. The client checks this certificate to authenticate the C2 server during the TLS handshake. - Command Signature Verification: The configuration includes a 4096-bit RSA public key modulus. Before executing any command sent from the C2 server, the client uses
RSACryptoServiceProvider.VerifyHashto check the command's cryptographic signature, making sure it came from the real bot master.

Threat Context: Webcrystal & MaaS Rebranding
The CrystalX RAT family was originally distributed under the name Webcrystal RAT in early 2026.
Distributed as a Malware-as-a-Service (MaaS) package marketed via Telegram channels, CrystalX is a Go/C# hybrid derived from older WebRAT (Salat Stealer) codebases. It is known for combining standard data theft (browser credential harvesting, Discord token logging, and cryptocurrency address clipping) with remote VNC control and unique "prankware" modules designed to disorient victims (mouse shaking, desktop icon hiding, screen rotation) while background exfiltration takes place.
Indicators of Compromise (IOCs)
| Indicator | Type | Description |
|---|---|---|
http://47.129.178.214:8092/app_JnCFbH.webp | URL | Polymorphic WebP Stager (AWS Singapore) |
47.237.101.246:6700 | C2 | CrystalX RAT C2 Server |
app_JnCFbH_payload.exe | PE | Unpacked CrystalX RAT Payload |
e362ccdb-9861-4423-82c1-34c4413b08b4 | Mutex | Campaign Mutex |









