PyPI

The qzc690385 PyPI Supply Chain Attack

Updated 7 Nov 2026 · Incident date 7 Nov 2026

qzc690385 on PyPI ran shell commands at install from Xinference model names and sent the output back, found on 7 November 2026.

On 7 November 2026, the PyPI package qzc690385 went up in twelve versions in under five hours. Every version runs shell commands when pip installs it. The package holds no real code. Its __init__.py is a comment.

The setup.py file talks to an Xinference server on the same machine. It reads commands from the names of registered models and writes the output back as new model names. Version 1.0.0 also starts a shell server that listens on five ports.

What happened

pip runs setup.py when it builds a source download. Every version of qzc690385 ships only a source download, so the code runs at install. The user does not need to import the package.

The code works with the Xinference API at http://127.0.0.1:9997/v1/model_registrations/flexible. Xinference is a server that runs AI models. The versions differ, but they share one channel:

  1. The code reads the list of registered models. It keeps the names that start with xcmd0.
  2. It decodes the rest of each name from base64 and runs the result with /bin/sh. With no command, 1.0.4 runs id;hostname;uname -a.
  3. It encodes the output in base64 and registers new models named zqx9p plus a chunk of the output. Anyone who can read the model list can read the output.

Version 1.0.0 does more. It writes a small HTTP server to /tmp/.zqx and starts it in the background with nohup. The server listens on all interfaces on ports 9998, 9999, 8089, 8099 and 8777. It runs any shell command that a GET request sends with the token kq7731.

Version 1.1.1 writes /tmp/kiki_c2.py and starts it in the background. That script repeats the read, run and send-back steps every 6 seconds, so it keeps taking commands after the install ends.

Amazon Inspector reported 1.0.0 and 1.0.1 to the OpenSSF malicious packages database. OSV published the record MAL-2026-18751 at 10:21 UTC. The author kept uploading new versions after that.

Vigilance caught version 1.0.4 at 13:53 UTC on 7 November 2026, 1 minute 57 seconds after it went up on PyPI. The first bad version, 1.0.0, went up at 09:53 UTC. Counted from that upload, the time to catch was 4 hours. The OSV record MAL-2026-18751 came out at 10:21 UTC, before the catch. The scan showed one file, setup.py, that gained decode-and-run code and machine profiling. Sentinel saw version 1.0.0 at 09:55 UTC, but 1.0.0 had no earlier version to compare with, and the judge did not read it.

Vigilance reported versions 1.0.0 to 1.1.1 to PyPI at 14:35 UTC. PyPI quarantined the project by 14:50 UTC. No files are left to install.

Affected versions

  • Every version from 1.0.0 to 1.1.1, all uploaded on 7 November 2026.
  • The OSV record lists 1.0.0 and 1.0.1 only. The later versions use the same channel.
  • No clean version exists. PyPI quarantined the project on 7 November 2026.

Indicators of compromise

  • Package name qzc690385, any version.
  • POST and GET requests to http://127.0.0.1:9997/v1/model_registrations/flexible.
  • Xinference models named xcmd0..., zqx9alive, zqx9p..., zqx9loop, zqx9err, zqx8... or zqxp....
  • Files /tmp/.zqx, /tmp/kiki_c2.py, /tmp/kiki_c2.log, /tmp/kiki_r.sh, /tmp/kiki_out.txt and /tmp/kiki_err.txt.
  • A process that listens on port 9998, 9999, 8089, 8099 or 8777.
  • SHA-256 of qzc690385-1.0.0.tar.gz: d94e0d5acdff4d40b504f49a9c258e070201bf4e077e5d674eda702216b2cc08

How to check

Check whether the package is installed.

pip show qzc690385

Look for the files and the background processes it leaves.

ls -la /tmp/.zqx /tmp/kiki_* 2>/dev/null; ps aux | grep -E "[.]zqx|kiki_c2"

If you run Xinference, list the registered models and look for the names above.

curl -s http://127.0.0.1:9997/v1/model_registrations/flexible | grep -oE "(xcmd0|zqx)[A-Za-z0-9_-]*"

What to do now

  1. Uninstall the package with pip uninstall qzc690385.
  2. Stop the background processes and delete the files in /tmp listed above.
  3. Delete every Xinference model whose name starts with xcmd0 or zqx.
  4. Find out who can reach your Xinference API. Anyone who can register a model can send commands to a machine that installed this package.
  5. Treat the machine as compromised. Rotate every secret that the installing user can read.

What Vigilance would show (rebuilt from public reports)

Vigilance compares the version you trust with the new one and names the file that gained a new power. The block below is rebuilt from the public reports in the words Vigilance prints. It is not a captured scan, because the malicious release is not redistributed.

vigi diff --old qzc690385-prev --new qzc690385-current
files scanned: 45

HEADS UP  1 file can now do things the old version could not. The rest changed and gained nothing.

CHANGED    setup.py
           It now runs a command on its own when it is installed and runs a hidden, encoded command. It did not before.

Frequently asked questions

Which qzc690385 versions are malicious?

Every version from 1.0.0 to 1.1.1. Each one runs shell commands from setup.py when pip installs it.

Does qzc690385 need Xinference to do harm?

The command channel uses an Xinference server on the same machine. Version 1.0.0 also starts its own shell server on five ports, and that server does not need Xinference.

Sources

  1. osv.dev/vulnerability/MAL-2026-18751
  2. bad-packages.kam193.eu/pypi/package/qzc690385
  3. pypi.org/project/qzc690385/

More supply chain attacks

All 126 attacks in the library · What is a supply chain attack? · How to prevent supply chain attacks

Check the next update before you install it

Vigilance compares the version you trust with the new one. It names the one file that gained a power the old version did not have.

Start the free 14-day trialBrowse the attack library