cybersecnatlab/challenge-jail
Docker container for easy setup of "PWN" challenges.
4.0K
Docker container for easy setup of "PWN" challenges.
To use this container, set up a docker-compose.yml
using the following
template. You shouldn't need a Dockerfile
for simple cases.
services:
chall:
image: cybersecnatlab/challenge-jail:<tag>@sha256:<digest>
restart: unless-stopped
init: true
privileged: true
ports:
- 'HOST_PORT:1337'
volumes:
- './PATH/TO/CHALLENGE_EXECUTABLE:/home/user/chall:ro' # Challenge executable
- './PATH/TO/CUSTOM_LOADER:/home/user/ld-linux.so:ro' # optional
- './PATH/TO/CUSTOM_LIBRARIES_DIR:/home/user/libs:ro' # optional
# Optional custom command line (default: ["/home/user/chall"])
command: ["/home/user/chall", "--example", "custom", "args", "here"]
environment:
- 'BINDMOUNTS=/container/path:/jail/path,...' # default: unset
- 'FLAG=flag{hello_world_[random 8]}' # default: unset, no flag
- 'FLAG_IN_ENV=false' # default: false
- 'FORWARD_STDERR=false' # default: false
- 'FORWARDED_ENV=FOO,BAR,...' # default: unset
- 'LOG_STDERR=false' # default: false
- 'NETWORK=false' # default: false
- 'POW_BITS=26' # default: unset, no PoW
- 'POW_BYPASS_HASH=...' # default: unset, no PoW bypass
- 'RLIMIT_AS=8192' # default: 4096 = 4GiB
- 'RLIMIT_FSIZE=16' # default: 1 = 1MiB
- 'RLIMIT_NOFILE=128' # default: 32
- 'RLIMIT_STACK=99' # default: container soft-limit
- 'SET_INTERPRETER=/home/user/ld-linux.so' # default: unset, use container loader
- 'SET_RPATH=/home/user/libs' # default: unset, use container libraries
- 'TEMPDIRS=/path/to/a:/path/to/b:...' # default: unset
- 'TEMPDIRS_SIZE=4194304' # default: 4194304 = 4MiB
- 'TIMEOUT=60' # default: 60
# Optional additional custom vars to keep, specified in FORWARDED_ENV
- 'FOO=69420'
- 'BAR=Hello, World!'
NOTE that cybersecnatlab/challenge-jail:latest
does not exist. This is on
purpose to avoid mistakes. You should always pin the image to a specific
version using cybersecnatlab/challenge-jail:<tag>@sha256:<digest>
. The
SHA256 digets can be found selecting a specific tag in the "Tags" section on
Docker Hub.
At the bare minimum, the container expects to have capabilities to be able to
correctly invoke nsjail
: it must be run either using --privileged
on the docker
command line or using privileged: true
in
docker-compose.yml
.
socaz
is used as a TCP forking server to execute the challenge. If
provided in the FLAG
environment variable, a flag template is passed to
socaz
to generate a flag for each new connection to the challenge.
nsjail
is used for sandboxing, with only a few options exposed
through environment variables. It is currently not possible to provide arbitrary
nsjail
configurations.
By default, the command executed inside the jail after each connection is
/home/user/chall
with no arguments. It is possible to specify a custom command
and/or add arbitrary arguments through the command:
key under the service
defined in docker-compose.yml
. In any case, the working directory for the
executed command will be /home/user
.
Note that, although the command:
key accepts various syntaxes, the best
one to use to avoid ambiguity is the array syntax. For example:
["/path/to/exe", "--foo", "bar"]
.
Configuration can be done directly through the docker-compose.yml
file using
the following environment variables. These variables will not be visible to
the challenge executable.
Variable | Default | Accepted values | Description |
---|---|---|---|
BINDMOUNTS | Unset | Comma-separated (, ) list of colon-separated src:dst mount specs | Paths to mount as read-only bind mounts inside the jail. Each mount spec (src:dst ) adds one --bindmount_ro to the nsjail command line. Each mount spec is passed as is, where src is a path in the container, dst is the path mounted inside the jail. |
FLAG (1) | Unset, no flag | String representing a socaz flag template (1) | Flag to use for the challenge. If set, it will be passed to socaz (1) on each conncetion and will be available at /home/user/flag within the jail. |
FLAG_IN_ENV | false | true , false , 1 , 0 | Whether to keep the flag in the FLAG environment variable instead of writing it to a file (after being passed to and parsed by socaz ). |
FORWARD_STDERR | false | true , false , 1 , 0 | Whether standard error of the challenge should be forwarded through the network. This will pass --stderr to socaz . Incompatible with LOG_STDERR=true . |
FORWARDED_ENV (2) | Unset | String representing a comma-separated (, ) list of env var names | List of environment variables to be forwarded to the program. By default only FLAG will be forwarded if FLAG_IN_ENV is enabled. Some well-known variables will also be set (not forwarded) regardless. (2) |
LOG_STDERR | false | true , false , 1 , 0 | Whether standard error of the challenge should be logged by socaz (and thus visible in Docker logs). This will pass --debug to socaz . Incompatible with FORWARD_STDERR=true . |
NETWORK | false | true , false , 1 , 0 | Whether or not network access should be enabled. If true, --disable_clone_newnet is added to the nsjail command line. |
POW_BITS | Unset, no PoW | Non-negative integer | Proof of work bits required on new connections before launching the challenge (implemented by socaz ). |
POW_BYPASS_HASH | Unset, no PoW bypass | String representing a SHA-1 hash (exactly 40 hex chars) | Hash of a token used to bypass the proof of work. If POW_BYPASS_HASH is set to sha1(TOKEN) then answering 99:TOKEN will pass the PoW check (implemented by socaz ). Useful to let checkers bypass the PoW. |
RLIMIT_AS | 4096 | Non-negative integer | RLIMIT_AS (in MiB) for setrlimit(2) : maximum size of process virtual address space. Passed as --rlimit_as to nsjail . |
RLIMIT_FSIZE | 1 | Non-negative integer | RLIMIT_FSIZE (in MiB) for setrlimit(2) : maximum size for newly created files. Passed as --rlimit_fsize to nsjail . |
RLIMIT_NOFILE | 32 | Non-negative integer | RLIMIT_NOFILE for setrlimit(2) : 1 more than the highest possible file descriptor number for the process. Passed as --rlimit_nofile to nsjail . |
RLIMIT_STACK | Container soft-limit | Non-negative integer | RLIMIT_STACK (in MiB) for setrlimit(2) : maximum size for the stack of the process. Passed as --rlimit_stack to nsjail . |
SET_INTERPRETER | Unset, use container loader | Absolute path within container | Path to the dynamic loader to use for the challenge binary. If provided, the challenge binary will be patched to use this as PT_INTERP through patchelf . |
SET_RPATH | Unset, use container libraries | Absolute path within container | Path to a directory containing libraries for the challenge binary. If provided, the challenge binary will be patched to use this directory as DT_RPATH through patchelf . |
TEMPDIRS (3) | Unset, no temp dirs | Colon-separated (: ) list of absolute directory paths | Directories to be created as temporary R/W tmpfs mounts inside the jail (3). One --mount option is added to the nsjail command line for each path specified. Useful if the challenge binary needs some place to create files. |
TEMPDIRS_SIZE | 4194304 (4MiB) | Non-negative integer | Size (in bytes) of the tmpfs mounts specified in TEMPDIRS . Sets the size= mount option for each mount. |
TIMEOUT | 60 | Positive integer | Time (in seconds) after which the challenge binary is killed. Passed as --time_limit to nsjail . |
(1) The string specified in FLAG
is processed by socaz
and can contain
instructions (patterns) to generate the actual flag content. Refer to
socaz
documentation for more information on this. On each connection
to the service, socaz
will generate a new flag before forking to execute the
challenge binary. This flag will then be used for the duration of the
connection.
(2) Some well-known variables (HOME
, USER
, PATH
, PWD
,
OLDPWD
, SHLVL
) will always be set regardless of the variable names specified
in FORWARDED_ENV
. For your own sanity, avoid specifying names of other
configuration environment variables there (or do it, at your own risk)! Variable
names starting with an underscore or containing whitespace will not be
accepted.
(3) If their path is not explicitly specified in TEMPDIRS
, temporary
directories that already exist in the Docker container (such as /tmp
,
/dev/shm
, etc) will also be present inside the jail, although read-only.
Custom dynamic loader and/or custom dynamic libraries can be set up in different ways:
Using the SET_INTERPRETER
and SET_RPATH
configuration environment
variables (see Configuration above). This requires the
challenge executable to be an ELF binary and be placed at /home/user/chall
in the container. It will be patched once at container startup using
patchelf
to set PT_INTERP
and/or DT_RPATH
.
volumes:
- './challenge:/home/user/chall:ro'
- './libs:/home/user/libs:ro'
environment:
- 'SET_RPATH=/home/user/libs'
- 'SET_INTERPRETER=/home/user/libs/ld-linux.so'
NOTE that while this is the simplest option, it will result in a challenge binary inside the container that is slightly different than the real one, as the original is patched and replaced at container startup. For simple cases this shouldn't matter, but if the challenge does funny things or relies on being unchanged (e.g., it computes hashes of sections of memory or does other weird things), this may cause trouble. YMMV.
Providing a challenge binary that is already set up to use custom loader
and/or libraries. That is, the binary itself already declares a custom
interpreter path in the PT_INTERP
program header and/or custom library
search paths in DT_RPATH
or DT_RUNPATH
in the dynamic section. Refer to
man 8 ld.so
for more information. All these paths should refer
to directories inside the container (for example, mounted as additional
volumes).
volumes:
# Binary already knows to use libs and/or loader inside /home/user/libs
- './challenge:/home/user/chall:ro'
- './libs:/home/user/libs:ro'
Mounting your own /lib/xxx/...
directories inside the jail using a custom
set of bind mounts through the BINDMOUNTS
environment variable
(see Configuration above).
volumes:
- './libs:/lib/custom:ro'
environment:
# Replace the entire /lib/x86_64-linux-gnu in the jail
- 'BINDMOUNTS=/lib/custom:/lib/x86_64-linux-gnu'
NOTE that this approach will make it impossible for the challenge binary
to exec
existing binaries such as the ones in /bin
(as they would need
the original libraries). That is, unless you also want to replace /bin
to have binaries that run using the libs you replaced. You do you boss.
For simple cases, collecting dynamically linked libraries (including the loader
itself) from a given ELF binary can be automated using ldd
as
follows on the host system where the binary was built:
mkdir libs
ldd binary | awk '/=>/ {print $3; next}; /ld-linux/ {print $1};' | xargs -I X cp X libs
Then you can easily use those libraries and loader inside the container:
volumes:
# ...
- './libs:/home/user/libs:ro'
environment:
# ...
- 'SET_INTERPRETER=/home/user/libs/ld-linux-x86-64.so.2'
- 'SET_RPATH=/home/user/libs'
For more complex cases, consider providing a custom Dockerfile
with additional
build steps that build/copy/install libraries from another system/distro
somewhere in the container. That way, you don't have to distribute a copy of the
libraries with the challenge, which may be heavy, but only the Dockerfile
. For
example:
FROM weird-distro:xxx AS weird-distro
# Optional build steps here...
FROM cybersecnatlab/challenge-jail:ubuntu-noble@sha256:XXXXXXXXXX
COPY --from=weird-distro /lib/x86_64-linux-gnu /home/user/libs
docker pull cybersecnatlab/challenge-jail