Six Packages, One Backdoor: The Packagist ‘Laravel Utility’ RAT and Why composer.lock Didn’t Save You
A quick correction first: the active Packagist supply chain attack making the rounds in 2026 isn’t against laravel-lang/lang, the legitimate translations package maintained by the Laravel Lang team. That package is fine. The attack is against three packages from a Packagist user named nhattuanbl — nhattuanbl/lara-helper, nhattuanbl/simple-queue, and nhattuanbl/lara-swagger — that were branded to look like Laravel utilities. The names are similar enough that the muscle memory blurs them together, which is exactly the social engineering surface this kind of attack exploits. If your team triaged this thinking the official translations library got popped, the alert should now move to whichever folders contain composer.lock and composer.json, and the question to answer is whether any of the nhattuanbl packages ever made it in.
The technical payload is the unimpressive part. The interesting part is the delivery model — six packages over eighteen months, three of them clean, one of them clean-but-dependent — and what that tells you about the controls that didn’t fire.
The actual packages
Six packages published by the same Packagist account between June and December 2024:
nhattuanbl/lara-helper(v5.4.7) — RAT payload insrc/helper.phpnhattuanbl/simple-queue(v1.5) — identical RAT payload insrc/helper.phpnhattuanbl/lara-swagger(v2.0) — no malicious code, but declaresnhattuanbl/lara-helperas a hard Composer dependency with adev-masterconstraintnhattuanbl/lara-media— cleannhattuanbl/snooze— cleannhattuanbl/syslog— clean
The three clean ones aren’t filler. They’re the reputation. A search for the author’s name on Packagist returns a developer with multiple working libraries, normal-looking README files, and code that does what it says it does. That’s the cover. The malicious packages then borrow that credibility — and the lara-swagger trick borrows it twice, because the carrier package itself is also clean.
Download counts at disclosure were small (37, 29, 49 according to Socket’s reporting). That number is the wrong number to focus on. A targeted supply chain attack doesn’t need scale; it needs the right thirty-seven installs. The download count tells you the campaign wasn’t broadly successful. It does not tell you whether your shop is in the thirty-seven.
What the payload does
The malicious helper.php is a 27,340-byte single-line PHP file with three layers of obfuscation stacked on top of each other: control flow shattered into randomized goto labels, every string literal hex- or octal-escaped, and every variable and function name regenerated to noise. None of those layers is novel on its own. The combination is enough to defeat shallow signature-based scanners and to make a casual cat src/helper.php look like garbage rather than like obviously-malicious garbage. The SHA-256 worth pinning in your IOC list is a493ce9509c5180e997a04cab2006a48202afbb8edfa15149a4521067191ead7.
On boot — and ‘boot’ here means the Laravel service provider auto-discovery for lara-helper, or the autoloader resolving SimpleQueue for simple-queue — the payload forks itself into a detached background process so the parent request returns normally and the application looks healthy. It then opens a raw TCP socket to helper[.]leuleu[.]net:2096, hardcoded, and starts an AES-128-CTR encrypted channel with the key esCAmxUoJkIjTV0n (also hardcoded, also worth a YARA rule). The lock file at {sys_get_temp_dir}/wvIjjnDMRaomchPprDBzzVSpzh61RCar.lock prevents multiple copies of the RAT from racing each other on a busy app server.
The command set is what you’d expect from a competent operator and nothing more: ping, info, cmd, powershell, run, screenshot, download, upload, stop. The RAT enumerates PHP’s disable_functions and falls through popen, proc_open, exec, shell_exec, system, passthru until it finds one your hardening missed. If you’ve disabled all six globally — most shops haven’t — the shell commands fail, but file read/write and screenshot still work because those don’t need a shell. The RAT version string in the recon beacon is 1.2, which implies a 1.0 and a 1.1 somewhere in the maintainer’s history. Treat that as a soft signal that this operator has shipped before.
The C2 host was unresponsive when Socket published. That doesn’t help you. The payload is still on disk in any compromised environment, the lock file is still respected, and the retry logic will reconnect the moment a new C2 lands at the same hostname. Removing the package without removing the payload file is half a remediation.
Why the dependency chain is the lesson
The lara-swagger package is the part of this attack I’d ask a junior to stare at for thirty minutes. It contains no malicious code. Reading every byte of its source would not surface a finding. A reasonable code-review SLA — “read the package before adopting” — would clear it. The compromise rides in through the require block:
"require": {
"nhattuanbl/lara-helper": "dev-master"
}
Two things are working against the defender here. First, dev-master is a floating constraint. Composer pulls whatever the master branch points to right now, every time composer install runs against a fresh vendor directory or composer update runs at all. Version pinning in your own composer.json doesn’t help when the malicious package is a transitive dependency, because the transitive constraint is the one Composer honors. Your composer.lock does freeze the resolved version, but the lock file is a snapshot of one machine’s resolution at one moment — CI runs that regenerate the lock, dev machines that haven’t synced, fresh container builds with composer install --no-dev skipped, all bypass the freeze.
Second, the trust model around Composer dependencies in the PHP ecosystem is shallower than the npm or PyPI equivalents. There’s no in-band signing, no Sigstore equivalent in production, and the verified-publisher concept that Packagist supports is opt-in and rare among small libraries. The result is that nhattuanbl/lara-swagger looks identical, in your dependency graph, to a legitimate small library written by a solo developer. Which it nearly is — minus the one line in require.
This is the structural lesson worth carrying past this specific incident: any supply chain control that operates on the top-level dependency list is theatre. The attack surface is the resolved transitive graph, and most teams cannot tell you, off the top of their head, what’s in theirs.
Hunting for it in your environment
Five places to look, in rough order of cost:
1. composer.lock grep. Cheapest possible check. Across every repo in your org:
git ls-files | xargs grep -l "nhattuanbl/" 2>/dev/null
If that returns anything, the answer to the next question stops being theoretical. Run it against composer.json too, because a deleted-but-uncommitted change won’t show in lock.
2. The payload file hash. If your EDR or file integrity monitoring will let you, search the fleet for the SHA-256 above on any path matching vendor/*/src/helper.php. PHP shops with hundreds of services may not have file hash search at scale; if not, fall back to grepping for the hardcoded AES key string esCAmxUoJkIjTV0n across vendor/ directories. The key is unique enough that false positives are vanishingly unlikely.
3. The C2 hostname in DNS logs. helper.leuleu.net resolved across passive DNS for the duration of the campaign. Even with the host currently dark, any historical resolution from one of your application servers is a hit you need to treat seriously. Pull 12 months if your DNS retention goes that far.
4. Outbound TCP/2096 from PHP-FPM hosts. Non-HTTP outbound from a web server is unusual on its own. TCP/2096 specifically is the cPanel webmail SSL port, which means a flat “alert on port 2096 outbound from non-cPanel hosts” rule actually generalizes — this rule would have caught the campaign and will catch the next operator who reuses the port out of laziness.
5. The lock file. A file named wvIjjnDMRaomchPprDBzzVSpzh61RCar.lock in /tmp on a PHP host is a single-purpose IOC with zero false positives. If you find it, you found the host.
The hunt that doesn’t work well: signature scanning the helper.php body. The obfuscation has enough entropy that any signature narrow enough to be useful is also narrow enough to be evaded by a single regeneration pass from the same operator. Hash the file or grep the key; don’t try to outsmart the obfuscator.
Remediation if you find it
The Socket and gbhackers writeups both list “rotate secrets” as a single bullet. That’s right but undersells the work. The RAT runs in the same PHP process as the application, with access to everything that process can read. For a typical Laravel install that means:
- The
.envfile in full. Database credentials, queue credentials, mail credentials, cache credentials, any third-party API keys the app uses, theAPP_KEYused for encrypting session cookies and any field-level encryption. - The configured database connections, in scope. If the app has read access to a user PII table, the RAT has read access to that PII table. The blast radius is the database role the app authenticates as, not the host.
- The mounted filesystem. Storage paths, any locally cached secrets, SSH keys if the application’s user happens to have them, AWS or GCP metadata service tokens reachable from the host.
- Any S3, GCS, or object store credentials the app uses, plus a window of time equal to your detection latency in which those credentials were valid in attacker hands.
Rotate in that order. The APP_KEY rotation is the one most teams forget; it doesn’t break running sessions in an obvious way, but it invalidates the assumption underlying every encrypted-cookie-bound auth flow you’ve shipped. Plan for a logout event across the user base, not a silent rotation.
Then, the file-level cleanup:
composer remove nhattuanbl/lara-helper nhattuanbl/simple-queue nhattuanbl/lara-swagger- Delete
vendor/nhattuanbl/recursively. Composer should do this, but verify. - Delete
/tmp/wvIjjnDMRaomchPprDBzzVSpzh61RCar.lockand any sibling files insys_get_temp_dir()with that pattern. - Audit for files with mode
0777in writable paths. The RAT’s upload command sets that permission on dropped files and the operator can use it to stage further payloads. killany detached PHP processes that don’t map to PHP-FPM workers or queue workers you recognize. The forked RAT runs under the same user as the web server, which makes it less visually obvious inps.
If the host had backups taken after the install, those backups carry the payload. Don’t restore them. Build forward.
The structural mitigations — what to actually change
Operational hygiene that would have prevented this, in decreasing order of impact:
Forbid dev-master and other branch aliases in transitive dependencies. Composer’s minimum-stability setting in your root composer.json is the lever. Set it to stable and explicitly opt in to anything below that with prefer-stable: true and named exceptions. This single config change would have stopped the lara-swagger attack vector cold, because lara-swagger required a non-stable constraint to pull lara-helper. The cost is that you can’t pull dev branches transitively. That’s the right tradeoff.
Vendor your dependencies into a private mirror. Self-hosted Packagist (via Private Packagist, Satis, or Composer’s bare path and artifact repositories) lets you pin what flows into your environments. The mirror doesn’t catch malicious packages by itself, but it gives you a chokepoint to scan at. Without the chokepoint, every developer’s machine and every CI runner pulls direct from Packagist, and your attack surface is O(developers * pipelines) rather than O(1).
Require code review on composer.json and composer.lock diffs. Treat changes to either file as a privileged code path. The diff is small, the review burden is low, and the questions to ask are mechanical: is this a new package, who is the publisher, when was the package first published, has the maintainer changed recently. None of those questions need security-team intervention; they need a checklist in the PR template.
Audit the resolved transitive graph, not the top-level list. composer show --tree against a clean install dumps it. Pipe it through grep -v of your known-good publishers and review what’s left. Most teams have never looked. The first time you run this on a mature codebase you will find at least one package nobody remembers adopting. That package is the one to start with.
Disable disable_functions-bypassable execution paths. The RAT walks popen, proc_open, exec, shell_exec, system, passthru and uses the first that works. Disabling all six in production PHP is a real hardening posture for web tier servers that have no business shelling out. It doesn’t kill this RAT — file read and screenshot still function — but it amputates the most valuable command (cmd) from the operator’s toolkit. The cost is that you can’t exec() from PHP, which a startling number of legacy applications quietly do. Audit before you flip it.
Egress filtering from PHP-FPM hosts. The application server’s outbound connectivity should be the set of hosts it actually needs to reach — database, cache, queue, mail, the specific third-party APIs in .env. Everything else, including helper.leuleu.net:2096, should be denied by default. This is the control that converts a successful install into a noisy failed callback rather than a working RAT, and it’s the control most teams keep deferring because nobody wants to maintain the allowlist.
The related Composer CVEs worth patching while you’re in here
While teams are looking at composer.lock files anyway, this is a good moment to also pull in the April 2026 Composer patches — CVE-2026-40176 (CVSS 7.8) and CVE-2026-40261 (CVSS 8.8). Both are command injection in the Perforce VCS driver, both execute on composer install against a malicious composer.json, and both are patched in Composer 2.9.6 and 2.2.27 LTS. The exploit conditions don’t require Perforce to be installed locally — the driver code runs on parse — so the patches are not optional even for shops that have never used Perforce.
These CVEs and the nhattuanbl campaign aren’t related, but they share a hunt scope: anywhere you run Composer. Roll the version bump into the same change window as the package-removal work and the lockfile audit. One PR, three problems.
The shape of the problem
The headline writeups want this to be a story about obfuscated PHP and a clever RAT. It isn’t. The RAT is journeyman work and the obfuscation is freely available tooling. What makes this campaign worth studying is the patience: an account opened in 2015, six packages published over eighteen months, three of them deliberately useful and unpaid for, all to seed two packages with payload and one package as a clean-looking carrier. The total visible download count is under 150. The campaign was designed to land in a small number of high-value environments, not to spray widely.
The defender posture this implies is uncomfortable. Reputation isn’t a control if reputation can be manufactured cheaply over a multi-year horizon, and most supply chain hygiene assumes a much shorter attacker timeframe. The structural moves above — minimum-stability: stable, vendor mirroring, transitive-graph review, egress filtering — are not glamorous and they are not new. They are also not standard practice in the median Laravel shop in 2026, and that is the gap this kind of attack is built to exploit. Close the gap before the next operator at the same playbook ships a payload with retry logic to a C2 host that’s actually up.
Sources
- Malicious Packagist Packages Disguised as Laravel Utilities (Socket)
- Fake Laravel Packages on Packagist Deploy RAT on Windows, macOS, and Linux (The Hacker News)
- Malicious Laravel Packages Deploy PHP RAT, Grant Remote Access to Attackers (GBHackers)
- Cross-Platform RAT Delivered via Malicious Laravel Packages on Packagist (Security Arsenal)
- Malicious Packages Disguised as Laravel Utilities Deploy PHP RAT (CybersecurityNews)
- Malicious Laravel Packages Deploy PHP RAT, Compromise Web Servers (Cyberpress)
- New PHP Composer Flaws Enable Arbitrary Command Execution — Patches Released (The Hacker News)
- Composer 2.9.6 Fixes Two Perforce Command Injection Vulnerabilities (Laravel News)
- PHP Supply Chain Attacks: How Malicious Packages Sneak Into composer.json (StackShield)
- Locking Down Your Laravel Project’s Packages (Darren Odden)