Four typosquats of requests went up on PyPI in a 75.6-second burst: 0requests, py-0requests, py-1requests, and py-2equests. Every analyzed version did the same thing on import: collect potentially sensitive environment variables and try to send them somewhere.
The destination was localhost.
That is the lead finding for this cluster. The packages behave like an env-variable stealer, but the exfiltration target never left the victim machine. Analyst Kamil Mańkowski (kam193) filed three of them into OSV under campaign 2026-09-0requests on September 3, 2026, classified the set as PROBABLY_PENTEST, and grouped a fourth package, 0requests, into the same campaign even though it has no OSV advisory yet. The same campaign system links this operator to a May 2026 wave that shipped a full Telegram RAT.
Registry findings
requests is the intended typo surface. The four published names sit on a small combinatorial grid: an optional py- prefix, a digit (0, 1, or 2), and either the full word requests or the r-dropped form equests.
| Package | Prefix | Digit | r present | Similarity to requests (stripped) |
|---|---|---|---|---|
0requests | none | 0 | yes | 0.941 |
py-0requests | py- | 0 | yes | 0.941 |
py-1requests | py- | 1 | yes | 0.941 |
py-2equests | py- | 2 | no | 0.875 |
0requests is a near-miss of the real package name. Someone typing pip install 0requests after a fat-finger, or mistaking a digit-prefixed name for a versioned release, is the most plausible accidental install path. The py-* names lean harder on convention: developers who expect wrappers like py-yaml or py-dotenv can assume a py-requests helper exists and land on one of these instead.
py-2equests breaks the pattern by dropping the r. The publish order also skips ahead to digit 2 before 1 (see timestamps below). The cleaner reading is an automation typo or an incomplete naming template, not a polished leet-speak variant. Adjacent grid slots such as 1requests, 2requests, py-2requests, and py-3requests were checked on PyPI and returned 404. The operator published exactly four names and stopped.
All four packages are gone. PyPI JSON and simple-index lookups for every name return 404. There is no surviving publisher email, no sdist or wheel hash in the OSV records, and no tarball to re-extract. kam193’s advisories describe behavior and campaign membership. They do not include package integrity hashes. The SHA256 values attached to the OSSF origins are hashes of the advisory documents themselves, not of the Python artifacts.
Import-time behavior
By the time we checked the registry, every package URL was already 404. What follows is reconstructed from the kam193 advisory text mirrored into OSV, not from a local extract of __init__.py or setup.py.
The advisory text is identical across py-0requests, py-1requests, and py-2equests:
During import, the code exfiltrates potentially sensitive env variables. In all analyzed versions the exfiltration target was a localhost, suggesting it was just a test.
That wording matters. The trigger is import-time module execution, not a pip install hook. A developer (or CI job) has to actually import the module before the collection runs. That is slower to fire than an npm preinstall script, and it is also easier to miss if the package sat installed but unused.
What the advisories do not preserve:
- The exact Python source
- Which environment variable names were filtered
- Which localhost port was contacted
- Whether
setup.pyor a build hook also ran at install time - Whether the HTTP client was
urllib,http.client, or something else
The campaign JSON lists abuse categories exfiltration_env_variables and typosquatting, and the IOC array is empty. That empty list is consistent with a localhost-only destination: there is no external host, IP, or URL to extract.
Localhost C2 and the May campaign
Localhost as the exfil target means no analyzed sample sent data to an attacker-controlled server. A victim who imported one of these packages would have attempted a connection to 127.0.0.1 (or equivalent). Unless something was listening on that port, the connection fails locally and nothing leaves the machine.
That still leaves three proportional readings:
- A pentest or red-team kit that expects a listener on the same host or tunnel
- A packaging test that used localhost as a placeholder before a live C2 rewrite
- A namespace probe that claims PyPI name slots without generating outbound malicious traffic
The May 2026 campaign 2026-05-py-requests makes options 2 and 3 more interesting than a one-off lab exercise. That earlier wave, also tracked by kam193, is classified malicious rather than probably_pentest. It published typosquats of high-download libraries including py-requests, numpy-lib, pandas-data, and rich-util, and the campaign description is blunt: persistent malware acting as a RAT, focused on stealing data and modifying copied cryptocurrency wallet addresses.
Documented abuse categories for that May set include RAT behavior, clipboard modification, browser-data theft, crypto theft, persistence, and remote script execution. The listed IOCs were 8.217.75.147, http://8.217.75.147:3000/payload, and http://8.217.75.147:3000/download/tg_rat.py. The filename alone points at a Telegram-mediated RAT. A TCP probe of 8.217.75.147:3000 on September 4, 2026 timed out. The May C2 is offline.
A same-day continuation wave under the kam193 label 2025-06-alembic-util reused the operator lineage with a different relay pattern (GitHub issue comments and a Gist-hosted payload). Advisories in that wave explicitly note a likely continuation of 2026-05-py-requests. The September 2026-09-0requests cluster sits on the same naming lineage (py-requests becoming digit-prefixed py-Nrequests variants), but with a sharp capability downgrade: localhost only, env vars only, no persistence claimed in the advisory text.
We did not recover publisher identity for the September packages, and we did not independently prove account-level continuity beyond kam193’s campaign grouping. Treat the May link as analyst attribution from that campaign system, not as a new identity we recovered from PyPI metadata.
Publish timestamps
PyPI no longer exposes publish times for these removed packages. The best available clock is campaign_assigned_at from kam193’s per-package JSON, which is when the analysis system classified each name into 2026-09-0requests. Those timestamps sit inside the same minute as the OSV filings and are the closest proxy we have for the publish session.
| Order | Package | campaign_assigned_at (UTC) | Delta from first |
|---|---|---|---|
| 1 | 0requests | 2026-09-03T17:00:47.354895Z | +0.0s |
| 2 | py-0requests | 2026-09-03T17:01:32.349906Z | +45.0s |
| 3 | py-2equests | 2026-09-03T17:01:50.492095Z | +63.1s |
| 4 | py-1requests | 2026-09-03T17:02:02.982698Z | +75.6s |
Four packages in 75.6 seconds is scripted. Intervals shrank as the session ran (45.0s, then 18.1s, then 12.5s), which is consistent with an automated uploader that paused after the first accept and then accelerated. The digit order is 0, then 2, then 1, not a clean sequence. Combined with the equests spelling break, the session looks more like a generated naming grid with a mistake than a carefully ordered release plan.
Detection lag for this cluster was near zero. kam193 assigned the packages in the same window they appeared, filed OSV records for the three py-* names between 17:01:32Z and 17:02:03Z, and an OSSF import batch pulled all three advisories at 17:16:47Z within about two milliseconds of each other. Our input alerts arrived at 2026-09-04T00:00:07Z, roughly seven hours after publish.
If you installed these packages
These samples fire on import, not during pip install alone. The risk window is any process that actually imported the module after install.
rg -n "0requests|py-0requests|py-1requests|py-2equests" requirements*.txt Pipfile* poetry.lock uv.lock pyproject.toml 2>/dev/nullpip list 2>/dev/null | rg -i "0requests|py-0requests|py-1requests|py-2equests"If either command finds a hit:
- Uninstall the package and purge it from requirements or lockfiles
- Review whether the module was imported in app code, notebooks, or CI
- If a local listener could have been present on the exfil port, rotate environment secrets that process could read (cloud keys, tokens, database URLs)
- Hunt for related typosquats of
requests,numpy,pandas,rich,aiohttp,gradio, andweb3 - Watch for new
py-Nrequestsor digit-prefixed*requestsnames at version0.0.1
Do not assume a clean install history just because PyPI now 404s. Packages can be installed from a cached mirror, an internal index, or a downloaded wheel that outlived the registry page.
Indicators
| Indicator | Type | Value | Method |
|---|---|---|---|
0requests | PyPI package | 0.0.1 | kam193 campaign JSON; no OSV advisory at research time |
py-0requests | PyPI package | 0.0.1 | OSV MAL-2026-15860 |
py-1requests | PyPI package | 0.0.1 | OSV MAL-2026-15861 |
py-2equests | PyPI package | 0.0.1 | OSV MAL-2026-15862 |
| Campaign name | Analyst label | 2026-09-0requests | kam193 package-campaigns |
| Classification | Analyst category | probably_pentest | kam193 / OSV advisory text |
| Exfil target | Behavior | localhost | Identical advisory text across analyzed versions |
| Trigger | Behavior | import-time | Advisory text (“During import”) |
| Prior campaign | Linked campaign | 2026-05-py-requests | kam193 campaign JSON and naming lineage |
| Prior C2 | IP / URL | 8.217.75.147:3000 (/payload, /download/tg_rat.py) | May campaign IOC list; probed offline on 2026-09-04 |
| Prior package set | PyPI names | py-requests, numpy-lib, pandas-data, rich-util | May campaign JSON |
Affected versions
| Package | Version | Assigned (UTC) | Status | OSV |
|---|---|---|---|---|
0requests | 0.0.1 | 2026-09-03T17:00:47Z | Removed from PyPI | none filed at research time |
py-0requests | 0.0.1 | 2026-09-03T17:01:32Z | Removed from PyPI | MAL-2026-15860 |
py-2equests | 0.0.1 | 2026-09-03T17:01:50Z | Removed from PyPI | MAL-2026-15862 |
py-1requests | 0.0.1 | 2026-09-03T17:02:02Z | Removed from PyPI | MAL-2026-15861 |
Assigned times are kam193 campaign_assigned_at values, used here as the publish-session proxy because registry timestamps are gone with the packages.
