pwndocker: a CTF exploit development box packaged as one Docker image
A docker environment for pwn in ctf
At a glance
- What is it?
- pwndocker bundles the usual binary exploitation toolkit, pwntools, pwndbg, ROPgadget, ropper, one_gadget, angr, radare2 and the tracing utilities, into a single Ubuntu 22.04 based container with 32-bit support and the privileges a debugger needs. It saves an afternoon of setup per competition, at the cost of a base image that drifts from the upstream packages and a build that fetches some components from the network.
- Who is it for?
- Adopt pwndocker if you compete in CTFs and want a known-good exploitation environment up in one command, especially if you need 32-bit support alongside the debugger and the ROP tooling. Do not adopt it as a general development environment, since it is deliberately configured with seccomp:unconfined and the SYS_PTRACE capability, which is exactly what you should not grant an ordinary container.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Dockerfile, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The setup problem in a CTF pwn room
Binary exploitation work has a long tail of environment requirements. You need a debugger with architecture awareness, a scripting framework for talking to the target process, a gadget finder, a shell on the box, the right libc for the challenge, and often 32-bit libraries on a 64-bit host. Assembling that by hand before every competition wastes the time you were supposed to spend on the challenge.
pwndocker packages that environment as a container. Its description is a docker environment for pwn in CTF, built on phusion/baseimage:jammy-1.0.4, described as a modified Ubuntu 22.04 baseimage for docker. The base image choice matters, because phusion baseimages ship with a full init system, which is what lets services such as tmux and a proper shell environment behave the way they do on a normal machine rather than the stripped down environment of a minimal ubuntu image.
The repository is small by design. The top level holds a Dockerfile, a docker-compose.yml, a README.md, a LICENSE.md and a .github directory with the build action. Everything else is inside the image.
The audience is CTF players and students of low-level security who want a repeatable starting point rather than a personal dotfile setup. The licence is GPL-3.0, the default branch is master, and there is no separate homepage beyond the repository itself.
What the image does not try to be is a hardened runtime. It is deliberately permissive, because the tools it ships need to attach to processes and disable kernel-level syscall filtering to do their job.
What the image actually ships
The README lists the included software with one-line descriptions, and the list reads like a curated pwn workstation rather than a general build image.
For scripting and process control there is pwntools, described as a CTF framework and exploit development library, and pwndbg, a GDB plug-in for low-level developers, hardware hackers, reverse engineers and exploit developers. There is also pwngdb, described simply as gdb for pwn, and gdb-multiarch plus the 32-bit debugging libraries come from the apt layer in the Dockerfile.
The ROP side is covered by ROPgadget, which facilitates ROP exploitation, and ropper, which finds gadgets to build ROP chains for different architectures. one_gadget is included as a searching tool for execve('/bin/sh', NULL, NULL) gadgets for amd64 and i386.
For static analysis there is angr, a platform-agnostic binary analysis framework, and radare2, described as a rewrite from scratch of radare providing a set of libraries and tools for working with binary files. The Dockerfile installs the latest radare2 release, and the changelog shows the project tracking radare2 versions over time, including a move to version 4.4.0 with the r2pipe Python binding.
On the observability side, seccomp-tools provides tools for seccomp analysis, ltrace traces library function calls, strace traces system calls, and tmux is there as a terminal multiplexer so you can keep several sessions alive.
The Dockerfile adds the supporting build environment as well, including nasm for assembly, cmake, ruby, g++-multilib, elfutils, file, patchelf and vim, plus zsh with ohmyzsh added in 2023.
Starting the container and getting a shell
The compose file defines a single service named pwn, with the container named pwn_test and the image referenced as skysider/pwndocker. Bringing it up and opening a shell is two commands:
docker-compose up -d
docker exec -it pwn_test /bin/bashThe first command starts the container in the background, and the second attaches an interactive bash session to it. The container name in the exec command has to match the container_name in the compose file, which is pwn_test, not the service name pwn.
The compose file is minimal, and it is worth reading in full because the two settings it does carry are the ones that matter:
version: '3'
services:
pwn:
container_name: pwn_test
image: skysider/pwndocker
security_opt:
- seccomp:unconfined
cap_add:
- SYS_PTRACE
ports:
- "23946:23946"The image is pulled from Docker Hub under the skysider organisation. The published port 23946 is mapped so a listening service inside the container, which is where you would point a challenge server, is reachable from the host.
Note that the compose file uses the legacy docker-compose spelling rather than the docker compose plugin form, and it carries a version key that recent Compose releases warn about.
Why seccomp has to be disabled and ptrace added
The two non-obvious lines in that compose file exist for concrete reasons.
security_opt with seccomp:unconfined removes Docker's default seccomp profile from the container. Docker ships a filter that blocks a set of syscalls for safety, and a number of the tools in this image, and the processes they debug, collide with it. The default profile also blocks ptrace entirely, which is what a debugger needs to attach to a process.
cap_add with SYS_PTRACE grants the capability explicitly so that a debugger can trace, read memory from and inject into another process, whether that process is inside the container or the one you are attacking.
Both settings weaken isolation, and the reason they exist is the same: the tools shipped are developer tools that expect to observe and control other processes. This is a workstation container, not a sandbox for running untrusted code.
That distinction has a direct consequence for anyone tempted to reuse the image for other work. A container with seccomp unconfined and SYS_PTRACE is a container that can do much more to the machine running it than an ordinary one, and the image ships no compensating controls. Use it for challenge work on a machine you trust, and do not treat it as a safe place to run a binary of unknown provenance from a contest you did not write.
One more detail is visible in the Dockerfile: the multiarch support is explicit. dpkg --add-architecture i386 is run before the apt install, and libc6:i386, libc6-dbg:i386 and lib32stdc++6 are installed alongside the 64-bit equivalents. Challenge binaries are frequently 32-bit, and this is the part of the setup that is genuinely tedious to reproduce by hand.
Running a challenge binary against a different libc
The README spends more space on one practical question than on anything else: how to run a binary against a libc other than the one in the image. That is the single most common obstacle in a pwn challenge, because the exploit depends on the exact libc version the target used.
The first attempt is the preload route:
LD_PRELOAD=./libc.so.5 ./testWhen that fails, the fallback uses the libc-database, which the image stores at /var/lib/libc-database. You identify or find the libc, download the matching loader and libraries into the version directory, then point the binary at the downloaded loader with patchelf:
cd /var/lib/libc-database
./download <libc id> # try to get by ./identify or ./find, download ld.so and libpthread.so and so on to libs/<libc id>, refer to https://github.com/niklasb/libc-database,
cp libs/<glibc_version>/ld-{version}.so /tmp/ld.so
patchelf --set-interpreter /tmp/ld.so ./test
./testThe same idea works from pwntools, which is usually the neater route because the process is launched under your control:
from pwn import *
p = process(["/path/to/ld.so", "./test"], env={"LD_PRELOAD":"/path/to/libc.so.6"})The Q&A also covers the case where the downloaded loader works but a system library is missing, showing an ldd listing and the error about libselinux.so.1. The documented fix copies libselinux.so.1 and libpcre2-8.so.0 from an Ubuntu 18.04 container into the libc-database directory, because the versions built for Ubuntu 22.04 need a newer glibc than the challenge's loader provides. That is a niche problem, and it is the kind of detail that a hand-built environment rarely gets right.
A build that pulls moving targets from the network
The Dockerfile is short, and several of its steps are not version pinned, which is the main thing to weigh before relying on a published image tag.
The base image is pinned, phusion/baseimage:jammy-1.0.4, and the apt package list is unpinned in the usual way. The ohmyzsh install runs the upstream install script straight from raw.githubusercontent.com with curl piped into sh. The radare2 step queries the GitHub API for the latest release, extracts the tag name with grep and awk, downloads the amd64 .deb package and installs it.
The Python layer is configured to use the Tsinghua PyPI mirror as the index URL, with the mirror host marked as trusted, and installs packages with --no-cache-dir including ropgadget, z3-solver, smmap2, apscheduler, ropper and unicorn. The mirror choice is a speed and reachability decision for the author's region, and it also means the packages you get depend on that mirror's state.
The net effect is that the same Dockerfile produces different contents over time. That is fine for a personal toolbox, and it is exactly why a competition should start by building the image locally rather than pulling the skysider/pwndocker tag, so that you know what is inside it.
The changelog shows how the image has been maintained. It records an update to Ubuntu 22.04 with pwntools 4.14.1 on 2025-09-14, the addition of zsh and ohmyzsh with pwntools 4.11.0 on 2023-10-22, glibc versions 2.33 to 2.36 with pwntools 4.9.0 on 2023-01-27, and the introduction of a pwntools_version Docker build argument on 2022-03-06 with 4.8.0b0 set in the repository build action. The most recent entry, 2026-03-10, updated the software and removed the bundled inner libc and ida linux_server.
Licence, maintenance and the limits of the approach
The repository is licensed under GPL-3.0, with the terms in a LICENSE.md file. For a container whose contents are mostly other people's projects, each of which carries its own licence, the practical point is that the GPL-3.0 grant covers the files in this repository, such as the Dockerfile and the compose file, and the upstream tools retain their own terms.
The repository is not archived and the last push was on 2026-04-12, which is in the same window as the 2026-03-10 changelog entry. There are no GitHub releases, so the versions are tracked in the changelog and in the published image rather than in release artefacts.
The limitations are clear enough to name. The image is a convenience, not a reproducible environment, because the build pulls whatever the radare2 release API and the configured PyPI mirror currently serve. It is Ubuntu 22.04 based, so it inherits the age of that release and its package versions. It is deliberately unsandboxed, with seccomp disabled and ptrace granted, so it should not be the place where you run a binary from an unknown source. And the debugging privileges it grants are exactly what a security tool should have and exactly what a general purpose container should not.
Where it wins is the boring part. If you are spending a competition evening assembling a toolchain instead of solving challenges, the value is in the parts you would otherwise assemble by hand: multiarch libraries, the debugger plug-ins, the gadget finders, libc-database, and the ability to swap libc versions without rebuilding your workstation.
Editorial conclusion
Adopt pwndocker if you compete in CTFs and want a known-good exploitation environment up in one command, especially if you need 32-bit support alongside the debugger and the ROP tooling. Do not adopt it as a general development environment, since it is deliberately configured with seccomp:unconfined and the SYS_PTRACE capability, which is exactly what you should not grant an ordinary container. Before you rely on it in a competition, rebuild the image yourself rather than pulling skysider/pwndocker, because the Dockerfile installs radare2 from whatever the GitHub release API returns and pulls Python packages from a mirror index, so the contents of a published tag are not pinned to a known set of versions.
Frequently asked questions
How do I start the pwndocker CTF environment?
Run docker-compose up -d and then docker exec -it pwn_test /bin/bash. The compose file names the service pwn and the container pwn_test, and the exec command has to use the container name.
Which exploitation tools does the pwndocker image include?
The listed software includes pwntools, pwndbg, pwngdb, ROPgadget, ropper, one_gadget, angr, radare2, seccomp-tools, tmux, ltrace and strace, on top of the apt packages in the Dockerfile such as gdb-multiarch, nasm and patchelf.
Why does the pwndocker compose file need seccomp:unconfined and SYS_PTRACE?
Docker's default seccomp profile blocks ptrace and several other syscalls, which stops a debugger from attaching to a process. The compose file therefore disables the seccomp profile and adds the SYS_PTRACE capability, which is why the container should be treated as a workstation rather than an isolation boundary.
How do I run a CTF binary against a different libc in pwndocker?
Try LD_PRELOAD=./libc.so.5 ./test first, and if that fails use the libc-database under /var/lib/libc-database, copy the matching loader with cp and point the binary at it using patchelf --set-interpreter /tmp/ld.so ./test. The same trick can be done from pwntools with process and an LD_PRELOAD environment.
Does pwndocker support 32-bit challenge binaries?
Yes. The Dockerfile runs dpkg --add-architecture i386 and installs libc6:i386, libc6-dbg:i386 and lib32stdc++6, so 32-bit targets can be debugged and exploited from the same container as 64-bit ones.
Official sources
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.
[](https://hysenlabs.com/projects/skysider-pwndocker)Was this useful?