ForceMemo: Hundreds of GitHub Python Repos Compromised via Account Takeover and Force-Push

Back to Blog

The StepSecurity threat intelligence team was the first to discover and report on an ongoing campaign — which we are tracking as ForceMemo — 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.

Varun Sharma

View LinkedIn

March 14, 2026

Share on X

Share on X

Share on LinkedIn

Share on Facebook

Follow our RSS feed

Table of Contents

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 compromised setup.py in amirasaran/django-restful-admin (70 stars) — obfuscated malware is appended at the end of the legitimate file. A pip install . or python setup.py install would execute 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

Phase 1: Account Takeover

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.

Phase 2: Stealth 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:

  1. Takes the latest legitimate commit on the default branch
  2. Rebases it, appending obfuscated malware to a key Python file (setup.py, main.py, app.py, etc.)
  3. 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.

The rebased commit on amirasaran/django-restful-admin — the commit message and author are preserved from the legitimate merge, but the committer date reveals the actual attack date (March 10, 2026).

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)

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 GitHub Events API for amirasaran/django-restful-admin — the PushEvent at 2026-03-10T21:58:02Z shows the force-push replacing the clean commit (260ca63) with the malicious one (17849e1).

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.

Legitimate activity on amirasaran/django-restful-admin — PR #16 was merged normally (commit 260ca635). The attacker then rebased this merge commit with malicious code and force-pushed to master.

Phase 3: Solana Blockchain 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:

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)

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:

  1. api.mainnet-beta.solana.com
  2. solana-mainnet.gateway.tatum.io
  3. go.getblock.us
  4. solana-rpc.publicnode.com
  5. api.blockeden.xyz
  6. solana.drpc.org
  7. solana.leorpc.com
  8. solana.api.onfinality.io
  9. solana.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.

GitHub code search for the marker variable across the BierOne account — all repositories contain the identical malware, demonstrating account-level compromise.

Phase 4: Payload Execution

Once the malware retrieves the payload URL from the Solana memo, it:

  1. Downloads Node.js v22.9.0 from nodejs.org to the user’s home directory (cross-platform: Windows/macOS/Linux, x64/ARM)
  2. Fetches the encrypted JavaScript payload from the URL, receiving an IV and secret key in HTTP response headers
  3. Writes a JS file (i.js) that decrypts and executes the payload via the downloaded Node.js
  4. 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:

  1. 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
  2. Payload URL fetch (T+20s) — Connection to 217.69.0.159:80 — the URL decoded from the Solana transaction memo
  3. 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
  4. Node.js extracted (T+24s) — /home/runner/node-v22.9.0-linux-x64/bin/node deployed — Harden-Runner automatically detected the new binary and attached TLS monitoring
The GitHub Actions build log from the controlled execution — after setup.py –version prints 1.1.3 (legitimate), the malware immediately downloads Node.js v22.9.0 from nodejs.org and extracts it to the runner’s home directory. Full workflow run.
Harden-Runner network insights from the controlled execution — the malware’s outbound connections to Solana RPC endpoints and nodejs.org are clearly visible, alongside the normal GitHub Actions infrastructure traffic.

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.

Protect your CI/CD pipelines

StepSecurity monitors outbound network connections from your GitHub Actions runners, detects anomalous activity like the ForceMemo campaign, and secures your software supply chain.

Start Free with StepSecurity →

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

GitHub code search for the malware marker variable lzcdrtfxyqiplpd — hundreds of results across Python repositories.

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 compromised
  • HydroRoll-Team (Organization) — 6 repos compromised
  • BierOne (User) — 6+ repos compromised
  • gnlxpy (User) — 6 repos compromised
  • Fo2sh88 (User) — 6 repos compromised
  • watercrawl (Organization) — 4 repos compromised
  • tavasolireza (User) — 4 repos compromised
  • BishalBudhathoki (User) — 4 repos compromised
  • iperformance (User) — 4 repos compromised
  • amirasaran (User) — 3 repos compromised
  • KeithSloan (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 on pip 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 12BierOne/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/GDML restored 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:

  1. Search for the marker variable in any Python files you’ve cloned: grep -r "lzcdrtfxyqiplpd" .
  2. Check for ~/init.json on your system — this is the malware’s persistence file
  3. Check for downloaded Node.js in your home directory: ls ~/node-v22*
  4. Check for i.js in any recently-cloned project directories
  5. 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:

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.

How Harden-Runner Detects This Attack

Harden-Runner monitors network traffic, file system changes, and process activity on GitHub Actions runners. It is designed to catch exactly the kind of anomalous behavior that supply chain attacks like ForceMemo produce.

When we ran the compromised setup.py with Harden-Runner in audit mode, it captured every outbound connection the malware made:

  • api.mainnet-beta.solana.com:443 — Solana blockchain C2 query
  • 217.69.0.159:80 — Payload URL fetched from the Solana memo
  • nodejs.org:443 — Node.js v22.9.0 download

All three connections were flagged as “Not in baseline” — meaning they had never been seen in any previous run of the workflow. A Python setup.py has no legitimate reason to contact Solana RPC endpoints, download Node.js, or connect to unknown IPs.

Once a baseline is established, Harden-Runner can block any outbound connection that is not in the allowed list. This would have prevented the malware from reaching the Solana C2, downloading Node.js, or exfiltrating data.

Protect your CI/CD pipelines

StepSecurity monitors outbound network connections from your GitHub Actions runners, detects anomalous activity like the ForceMemo campaign, and secures your software supply chain.

Start Free with StepSecurity →

 

Latest articles

Related articles