Library / SDK
Gallopsled/pwntools avatar
Gallopsled/pwntools

pwntools: what the CTF toolkit actually does, and how to install it

CTF framework and exploit development library

13,728 stars1,858 forksPythonNOASSERTION

At a glance

What is it?
pwntools is a Python library for writing exploits against CTF binaries and remote services. This article covers how it installs on Ubuntu, what the remote and shellcraft workflow looks like, and where the abstraction breaks down.
Who is it for?
pwntools is for people writing exploit scripts against Linux binaries and remote services, especially in CTF settings, and for anyone who wants ELF parsing, ROP gadget search and shellcode generation behind one Python import. It is not the right tool for Windows target work, for kernel or browser exploitation where the framework has no dedicated primitives, or for anyone who needs a stable API surface across major versions.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 27 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem pwntools removes from exploit scripting

Writing an exploit by hand means gluing together a socket, a parser for the target binary, an assembler for the payload, and a debugger you attach at the right moment. Each of those is a separate tool with a separate interface, and the glue code is where most of the time goes. pwntools puts those pieces behind one Python namespace. The README describes it as a CTF framework and exploit development library, written in Python and designed for rapid prototyping, with the stated goal of making exploit writing as simple as possible.

The intended user is someone who already understands the vulnerability and wants to stop rewriting boilerplate. The README example is four lines: import everything, set the architecture and OS in a context object, open a remote connection, then send a shellcraft payload and hand the terminal over with r.interactive(). That last call is the part that matters in practice. It turns the exploit script into an interactive session once the payload lands, which is how most CTF work actually proceeds: land the shell, then type commands by hand.

The repository topics list the audience explicitly: capture-the-flag, exploit, pwnable, rop, shellcode, wargame. This is not a general purpose networking library that happens to be used for security work. It is scoped to the workflow of getting code execution on a target and then talking to it.

How the pieces fit: context, tubes, ELF and shellcraft

The architecture is a set of cooperating modules under pwnlib, re-exported through a single import. Four of them carry most of the weight.

The context object is global state. Setting context(arch='i386', os='linux') tells every other module what it is targeting, so asm() and shellcraft do not need the architecture passed again. This is convenient and also the main source of surprises: a context set at the top of a script silently changes the behaviour of calls made much later, and in a long exploit script it is easy to lose track of what is currently set.

Tubes are the I/O abstraction. A remote connection, a local process, an SSH session and a serial port all present the same interface, so send, recvuntil and interactive work the same way regardless of what is on the other end. The examples directory in the repository includes remote.py, ssh.py and port_forward.py, which is a fair summary of the surface: the framework expects you to swap the transport without rewriting the exploit logic.

ELF wraps pyelftools and gives you symbol lookup, PLT and GOT access, and section data as attributes rather than as parsed structures you maintain yourself. ROP is built on top of that plus ropgadget, and assembles a chain from a list of gadgets and constants. Shellcraft is a template system: the repository's setup.py walks pwnlib/shellcraft/templates to collect template files at build time, and those templates are rendered by mako. That is why shellcraft.sh() can produce architecture-specific shellcode from a Python call.

The dependency list in pyproject.toml shows how much of this is delegated rather than implemented: capstone for disassembly, pyelftools for ELF, ropgadget for gadget search, unicorn for emulation, paramiko for SSH, and pyserial for serial. pwntools is largely an integration layer over those libraries, which explains both its breadth and the fact that many of its rough edges are inherited.

Installing pwntools on Ubuntu and running a first remote session

The README states that pwntools is best supported on 64-bit Ubuntu LTS releases, specifically 22.04 and 24.04, and that most functionality should work on any Posix-like distribution. It also states that Python 3.10+ is supported since version 5.0.0, and that 4.x is the line to use for older interpreters and Python 2.7.

The quick path from the README installs the system prerequisites, upgrades pip, then installs the package:

bash
sudo apt-get update
sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools

The README notes that some features, specifically assembling and disassembling foreign architectures, need non-Python dependencies and points to the full installation page at docs.pwntools.com for those. So a plain pip install gets you the Python-only surface, and cross-architecture assembly is a separate concern.

Once installed, the README's own example is the shortest real use. It targets a host and port, builds shellcode for the configured architecture, sends it, and drops into an interactive session:

python
from pwn import *
context(arch = 'i386', os = 'linux')

r = remote('exploitme.example.com', 31337)
# EXPLOIT CODE GOES HERE
r.send(asm(shellcraft.sh()))
r.interactive()

What you should see after running this is a Python session that has sent the payload and then hands your terminal to the remote process, so keystrokes go to the target rather than to your shell. If the target is not actually running the service, remote() fails at connection time, before any payload is sent.

For local work the same pattern applies with process() in place of remote(), and the examples directory has attach.py and gdb_api.py for the debugger side. The repository also ships a pwn command line entry point, defined in setup.py as pwn=pwnlib.commandline.main:main, with a set of subcommands generated from the files in pwnlib/commandline. Several of those, including asm, cyclic, disasm, hex, unhex and template, are listed in setup.py as deprecated scripts, which means they still install but route through a deprecated_main handler. If you are scripting against them, that is a signal to move to the Python API instead.

Where pwntools stops being the right tool

The clearest boundary is the platform. The README's support statement is Ubuntu LTS first, Posix-like second. Windows is not in that list, and the classifier in pyproject.toml is Operating System :: POSIX :: Linux. If your target or your development machine is Windows, the framework is not built for that case, and the search interest in installing it on Windows does not change what the README says.

The second boundary is version churn. The README says Python 3.10+ is required from version 5.0.0, while the most recent release listed is 4.15.0 from 2025-10-12. That means the documentation is describing a version line ahead of the latest tagged release, and anyone pinning to 4.x is working against a README that assumes 5.x. Reading the README as a description of your installed version is a mistake here.

The third is the nature of the abstraction. pwntools is an integration layer over capstone, pyelftools, ropgadget and others. When a gadget search returns something unexpected, or a disassembly disagrees with objdump, the problem may be in the underlying library, and the pwntools API does not necessarily expose enough to tell you which layer is wrong. For a quick CTF exploit that is an acceptable trade. For tooling you intend to maintain, it means debugging through a stack you did not choose.

Finally, the project is explicit that not everything is Python-only. Cross-architecture assembly needs external dependencies, so a container or CI image built from the pip install line alone will fail on those calls.

pwntools against a plain socket and struct approach

The obvious alternative is to write the same exploit with Python's socket module and struct, plus a disassembler like objdump invoked from the shell. That approach has no dependency tree: no capstone, no unicorn, no paramiko. It also has no context object, no tube abstraction, and no shellcraft, so you write the payload bytes by hand or generate them with an assembler you invoke yourself.

The difference is not capability so much as where the code lives. With pwntools, the architecture and OS are global state and the payload generation is a function call. With the standard library approach, every one of those decisions is explicit at the call site, which is more verbose but also more legible to someone reading the script six months later. If your exploit is a single send of a fixed byte string, the framework is overhead. If it involves parsing an ELF, finding a ROP chain, and switching between local and remote targets during development, the framework is doing real work.

A middle position is to use pwntools for the parts that are genuinely tedious, ELF parsing and gadget search, and keep the socket layer as plain Python. Nothing in the library requires you to adopt the tube abstraction wholesale, and the modules are importable individually rather than only through the star import the README shows.

Maintenance, licensing and what to check before you commit

The repository is not archived, and the last push to the dev branch was on 2026-09-03. The most recent tagged release is 4.15.0 from 2025-10-12, preceded by 4.15.0beta1 and 4.14.1, both from 2025-03-24. So the dev branch moves ahead of the release tags, which is consistent with the README documenting 5.0.0 behaviour that has not yet appeared as a tagged release. If you depend on pwntools, deciding whether to track dev or pin to a release is a real choice, and the README's install instructions do not resolve it for you.

Upgrade cost is dominated by the dependency list. The package pulls in paramiko, mako, pyelftools, capstone, ropgadget, pyserial, requests, pygments, pysocks, packaging, psutil, intervaltree, sortedcontainers, unicorn, rpyc, colored_traceback and unix-ar, plus backports.zstd on Python versions below 3.14. That is a wide surface to keep current in a pinned environment, and it is the main reason a pwntools upgrade is rarely a one-line change.

On licensing, the situation is not a clean single licence. The README badge says MIT, but pyproject.toml declares the licence as "Mostly MIT, some GPL/BSD, see LICENSE-pwntools.txt", and the repository carries LICENSE-pwntools.txt rather than a plain LICENSE file. The badge and the package metadata disagree in emphasis, and the metadata is the one that points at the actual file. If you are redistributing pwntools or bundling it into a product, read LICENSE-pwntools.txt and establish which components fall under which terms. This is not legal advice, and the file is the authority, not the badge.

Editorial conclusion

pwntools is for people writing exploit scripts against Linux binaries and remote services, especially in CTF settings, and for anyone who wants ELF parsing, ROP gadget search and shellcode generation behind one Python import. It is not the right tool for Windows target work, for kernel or browser exploitation where the framework has no dedicated primitives, or for anyone who needs a stable API surface across major versions. Before adopting it, verify which Python version you are on, since the README says Python 3.10+ is required from version 5.0.0 and 4.x is the line for older interpreters, and read LICENSE-pwntools.txt rather than trusting the MIT badge in the README.

Frequently asked questions

How do I install pwntools?

The README gives a four-line path: update apt, install python3, python3-pip, python3-dev, git, libssl-dev, libffi-dev and build-essential, upgrade pip, then run pip install --upgrade pwntools. It states that pwntools is best supported on 64-bit Ubuntu LTS 22.04 and 24.04, and that most functionality works on other Posix-like systems.

How do I install pwntools on Ubuntu?

The README's install block is written for Ubuntu: apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential, then python3 -m pip install --upgrade pwntools. It names 22.04 and 24.04 as the best supported releases.

How do I install pwntools on Kali Linux?

The README does not name Kali specifically. It says pwntools is best supported on 64-bit Ubuntu LTS releases and that most functionality should work on any Posix-like distribution, which is the closest statement it makes for Debian-derived systems.

How do I install pwntools on macOS?

The README lists OSX among the Posix-like distributions where most functionality should work, but the supported configuration it names is 64-bit Ubuntu LTS. It does not provide a macOS-specific install command.

Can I install pwntools on Windows?

The README does not list Windows among supported platforms. It names Ubuntu LTS first and Posix-like distributions second, and the package classifier in pyproject.toml is Operating System :: POSIX :: Linux.

Which Python version does pwntools need?

The README states that pwntools supports Python 3.10+ since version 5.0.0, and that 4.x should be used for older Python versions as well as Python 2.7. The pyproject.toml file sets requires-python to >=3.10.

Official sources

  1. Gallopsled/pwntools on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gallopsled-pwntools.svg)](https://hysenlabs.com/projects/gallopsled-pwntools)