The StepSecurity threat intelligence team has discovered an ongoing campaign in which an attacker is compromising hundreds of GitHub accounts and injecting identical malware into hundreds of Python repositories. The earliest injections date to March 8, 2026, and the campaign is still active with new repos continuing to be compromised. The attack targets Python projects — including Django apps, ML research code, Streamlit dashboards, and PyPI packages — by appending obfuscated code to files like setup.py, main.py, and app.py. Anyone who runs pip install from a compromised repo or clones and executes the code will trigger the malware.

The attacker gains access to developer accounts, takes the latest legitimate commit on each repo's default branch, rebases it with malicious code appended, and force-pushes — making it appear as if nothing changed. The commit message, author, and author date are all preserved from the original. We have filed security issues on the most notable affected repositories to notify maintainers.
This is an ongoing campaign. We are actively tracking this attack and will continue to update this blog post as new information becomes available. Some of these repositories may still contain the malicious code. If you use any Python packages installed directly from GitHub, check that the code on the default branch matches the last legitimate commit from the original author.
How the Attack Works
Step 1: Account Compromise
The attacker is compromising GitHub accounts — likely through stolen personal access tokens, OAuth tokens, or credential stuffing. The evidence for account-level compromise is clear: when an account with multiple repositories is taken, every repo under that account gets injected. For example, user BierOne has had 6 repos compromised, the organization wecode-bootcamp-korea has had 6 repos hit, and HydroRoll-Team has had 6.
Step 2: Stealthy Code Injection via Force-Push
The injection method is sophisticated. Rather than opening a pull request or creating a new commit (both of which would be visible in the repo's activity feed), the attacker:
- Takes the latest legitimate commit on the default branch
- Rebases it, appending obfuscated malware to a key Python file (
setup.py,main.py,app.py, etc.) - Force-pushes to the default branch
The commit message and author date are preserved from the original commit — only the committer date reveals the tampering. The committer email is also set to the string "null" across many of the malicious commits, which appears to be a fingerprint of the attacker's tooling.

Here are the date discrepancies we found across the most notable repositories:
amirasaran/request_validator— author date: 2017-04-24, committer date: 2026-03-10 (9 year gap)BierOne/relation-vqa— author date: 2019-04-11, committer date: 2026-03-13 (7 year gap)BierOne/bottom-up-attention-vqa— author date: 2021-06-01, committer date: 2026-03-13 (5 year gap)biodatlab/siriraj-assist— author date: 2024-03-19, committer date: 2026-03-13 (2 year gap)amirasaran/django-restful-admin— author date: 2024-11-15, committer date: 2026-03-10 (16 month gap)uknfire/tsmpy— author date: 2025-06-04, committer date: 2026-03-08 (9 month gap)KeithSloan/ImportNURBS— author date: 2025-08-28, committer date: 2026-03-10 (6 month gap)BierOne/ood_coverage— author date: 2024-10-25, committer date: 2026-03-12 (5 month gap)KeithSloan/GDML— author date: 2026-02-06, committer date: 2026-03-11 (33 day gap)
Step 3: Force-Push Evidence from the GitHub Events API
The GitHub Events API captures push events with before and after commit SHAs. For amirasaran/django-restful-admin, we can see the exact moment the default branch was replaced:
// March 10, 21:58 UTC — master force-pushed with malicious code
{
"type": "PushEvent",
"actor": "amirasaran", // compromised account
"created_at": "2026-03-10T21:58:02Z",
"ref": "refs/heads/master",
"before": "260ca635...", // clean commit (legitimate PR #16 merge)
"after": "17849e1b..." // malicious rebased commit
}

The before SHA (260ca635) is the legitimate merge commit from PR #16. The after SHA (17849e1b) is the attacker's rebased commit with malware appended to setup.py. Because the attacker uses the compromised account's own credentials, the push appears to come from the repo owner.

The Malware: Solana Blockchain as C2
The injected code is appended to the end of whatever Python file the attacker targets. It's obfuscated with three layers: base64 decoding, zlib decompression, and XOR decryption (key: 134). The variable names are randomized 15-character strings, but the base64 payload blob is identical across all compromised repos, stored in a variable named lzcdrtfxyqiplpd.
# Obfuscation wrapper (appended to end of legitimate Python file)
# -*- coding: utf-8 -*-
aqgqzxkfjzbdnhz = __import__('base64')
wogyjaaijwqbpxe = __import__('zlib')
idzextbcjbgkdih = 134
qyrrhmmwrhaknyf = lambda d, o: bytes([b ^ idzextbcjbgkdih for b in d])
lzcdrtfxyqiplpd = '<4,800-character base64 blob>'
runzmcxgusiurqv = wogyjaaijwqbpxe.decompress(
aqgqzxkfjzbdnhz.b64decode(lzcdrtfxyqiplpd))
ycqljtcxxkyiplo = qyrrhmmwrhaknyf(runzmcxgusiurqv, idzextbcjbgkdih)
exec(compile(ycqljtcxxkyiplo, '<>', 'exec'))
After deobfuscation, the malware executes the following sequence:
1. CIS Country Exclusion
The malware first checks if the system is Russian — examining locale settings, timezone, and UTC offset. If the system is Russian, execution is skipped entirely. This is a well-known pattern in Eastern European cybercrime operations to avoid targeting domestic systems.
# Comments in the deobfuscated code are in Russian:
# "Эмуляция объекта Buffer из Node.js" (Emulation of Node.js Buffer object)
# "Получение подписей для адреса Solana" (Getting signatures for Solana address)
# "Проверка, находится ли система в России" (Checking if system is in Russia)
2. Solana Blockchain C2 Channel
Instead of connecting to a traditional C2 server that could be taken down, the malware reads its instructions from the Solana blockchain. It queries a specific Solana address for transaction memos containing JSON data with a payload URL:
Solana C2 address: BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC
The malware tries 9 different Solana RPC endpoints as fallbacks, making it highly resilient to any single endpoint being blocked:
api.mainnet-beta.solana.comsolana-mainnet.gateway.tatum.iogo.getblock.ussolana-rpc.publicnode.comapi.blockeden.xyzsolana.drpc.orgsolana.leorpc.comsolana.api.onfinality.iosolana.api.pocket.network
Using the blockchain as a C2 channel means the attacker can update the payload URL at any time by posting a new transaction — and no one can delete or censor the instructions once they're on-chain.

3. Node.js Download and Encrypted Payload Execution
Once the malware retrieves the payload URL from the Solana memo, it:
- Downloads Node.js v22.9.0 from
nodejs.orgto the user's home directory (cross-platform: Windows/macOS/Linux, x64/ARM) - Fetches the encrypted JavaScript payload from the URL, receiving an IV and secret key in HTTP response headers
- Writes a JS file (
i.js) that decrypts and executes the payload via the downloaded Node.js - Creates a persistence file (
~/init.json) with a 2-day recheck timer to avoid repeated execution
The final JS payload is encrypted with AES, making static analysis of the second stage impossible without the server-side key. Based on the infrastructure pattern (Solana C2 + Node.js execution + AES encryption + CIS exclusion), this is consistent with known crypto wallet stealer / infostealer campaigns.
Harden-Runner Analysis: Catching the Malware in Action
To confirm the malware behavior, we ran the compromised setup.py from amirasaran/django-restful-admin in a controlled GitHub Actions environment with StepSecurity Harden-Runner monitoring all network activity. The results confirm the full attack chain described above.
Within seconds of executing python3 setup.py, Harden-Runner captured the following network activity from the python3.12 process:
- Solana C2 query (T+10s) — DNS resolution of
api.mainnet-beta.solana.com(208.91.111.195:443) — the malware querying the blockchain for its C2 instructions - Payload URL fetch (T+20s) — Connection to
217.69.0.159:80— the URL decoded from the Solana transaction memo - Node.js download (T+21s) — DNS resolution of
nodejs.org(172.66.128.70:443) — downloading Node.js v22.9.0 to execute the encrypted JavaScript payload - Node.js extracted (T+24s) —
/home/runner/node-v22.9.0-linux-x64/bin/nodedeployed — Harden-Runner automatically detected the new binary and attached TLS monitoring


None of these connections belong in a Python project's CI/CD pipeline. A setup.py has no legitimate reason to contact Solana RPC endpoints, download Node.js, or connect to unknown IPs. Harden-Runner's network egress monitoring flags exactly this kind of anomalous activity. Add Harden-Runner to your workflows to detect compromised dependencies before they can exfiltrate data.
The full workflow run with Harden-Runner insights is available at: StepSecurity Insights Dashboard.
Scope of the Campaign
So far, we have identified hundreds of Python repositories across hundreds of GitHub accounts injected with identical malware, and the number continues to grow. The targeted repos include Django web applications, machine learning research code, Streamlit dashboards, Flask APIs, and Python packages installable via pip install from GitHub URLs. Several of the compromised repos have setup.py files — meaning a pip install directly from the repo executes the malware during installation.
The full list of affected repositories can be found by searching GitHub for the malware's marker variable:
GitHub Code Search: lzcdrtfxyqiplpd

Account-Level Compromise: Repeat Victims
The strongest evidence for account-level compromise is the pattern of multiple repositories being hit per account. These numbers are growing as the campaign continues:
wecode-bootcamp-korea(Organization) — 6 repos compromisedHydroRoll-Team(Organization) — 6 repos compromisedBierOne(User) — 6+ repos compromisedgnlxpy(User) — 6 repos compromisedFo2sh88(User) — 6 repos compromisedwatercrawl(Organization) — 4 repos compromisedtavasolireza(User) — 4 repos compromisedBishalBudhathoki(User) — 4 repos compromisediperformance(User) — 4 repos compromisedamirasaran(User) — 3 repos compromisedKeithSloan(User) — 2 repos compromised
File Types Targeted
The attacker's tooling selects the most prominent Python entry point in each repo:
main.py— most common target (~70 repos)setup.py— triggers onpip install .(~25 repos)app.py— Flask/Streamlit apps (~25 repos)manage.py— Django projects (~20 repos)app/__init__.py— package init files (~8 repos)- Various:
streamlit_app.py,run.py,config.py,cli.py,noxfile.py
Timeline
Campaign Timeline (March 2026)
- March 8 — Earliest injections detected:
metalogico/issued,uknfire/tsmpy,gnlxpy/*repos,wecode-bootcamp-korea/*repos - March 10 — Major wave:
amirasaran/*repos (including 70-star django-restful-admin),KeithSloan/ImportNURBS,watercrawl/*repos - March 11 — Continued:
KeithSloan/GDML - March 12 —
BierOne/ood_coverage(34-star ICLR paper) - March 13 — Latest wave:
BierOne/bottom-up-attention-vqa,BierOne/relation-vqa,biodatlab/siriraj-assist,HydroRoll-Team/HydroRoll - March 14 — First repos begin reverting (e.g.,
KeithSloan/GDMLrestored at 14:05 UTC). StepSecurity publishes initial findings and files security issues on affected repos. - Ongoing — Campaign remains active. We are continuing to monitor and will update this post.
Indicators of Compromise
- Solana C2 address:
BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC - Marker variable in code:
lzcdrtfxyqiplpd - XOR decryption key:
134 - Committer email (fingerprint):
"null"(string) - Node.js version downloaded: v22.9.0
- Persistence file:
~/init.json - JS payload file:
i.js(in script directory) - Code comments language: Russian
- CIS exclusion: Skips execution on Russian locale/timezone
Solana RPC Endpoints Contacted
api.mainnet-beta.solana.com
solana-mainnet.gateway.tatum.io
go.getblock.us
solana-rpc.publicnode.com
api.blockeden.xyz
solana.drpc.org
solana.leorpc.com
solana.api.onfinality.io
solana.api.pocket.network
Why We Call It ForceMemo
We are tracking this campaign as ForceMemo, derived from its two most distinctive technical artifacts:
- Force — the attacker injects malware by force-pushing to the default branch of compromised repositories. This technique rewrites git history, preserves the original commit message and author, and leaves no pull request or commit trail in GitHub's UI. No other documented supply chain campaign uses this injection method.
- Memo — the malware uses Solana blockchain transaction memos as its command-and-control channel, reading payload URLs from memo data attached to transactions on a specific Solana address. This makes the C2 instructions immutable and censorship-resistant.
How to Check If You're Affected
If you install Python packages directly from GitHub (e.g., pip install git+https://github.com/...) or clone and run Python repos:
- Search for the marker variable in any Python files you've cloned:
grep -r "lzcdrtfxyqiplpd" . - Check for
~/init.jsonon your system — this is the malware's persistence file - Check for downloaded Node.js in your home directory:
ls ~/node-v22* - Check for
i.jsin any recently-cloned project directories - Review git commit history of repos you've cloned — look for commits where the committer date is significantly newer than the author date
Disclosure
We filed security issues on the most notable affected repositories to notify maintainers:
- amirasaran/django-restful-admin #17
- amirasaran/request_validator #3
- BierOne/bottom-up-attention-vqa #9
- BierOne/ood_coverage #6
- metalogico/issued #7
- biodatlab/siriraj-assist #2
- KeithSloan/ImportNURBS #15
StepSecurity Threat Intelligence
StepSecurity threat intelligence was the first to discover and publicly report on this campaign. The team is actively monitoring the situation and will continue to update this post. StepSecurity continuously monitors the open source and CI/CD ecosystem for emerging threats — including supply chain attacks, compromised GitHub Actions, malicious packages, and account takeover campaigns like this one.
StepSecurity customers receive threat intelligence alerts directly in their dashboard, with actionable guidance on whether they are affected and how to remediate.



