cybersecnatlab/challenge-jail

By cybersecnatlab

Updated 7 days ago

Docker container for easy setup of "PWN" challenges.

Image
Security
Developer Tools

4.0K

Challenge-Jail

Docker container for easy setup of "PWN" challenges.

Docker compose template

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.

Usage

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

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.

VariableDefaultAccepted valuesDescription
BINDMOUNTSUnsetComma-separated (,) list of colon-separated src:dst mount specsPaths 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 flagString 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_ENVfalsetrue, false, 1, 0Whether 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_STDERRfalsetrue, false, 1, 0Whether 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)UnsetString representing a comma-separated (,) list of env var namesList 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_STDERRfalsetrue, false, 1, 0Whether 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.
NETWORKfalsetrue, false, 1, 0Whether or not network access should be enabled. If true, --disable_clone_newnet is added to the nsjail command line.
POW_BITSUnset, no PoWNon-negative integerProof of work bits required on new connections before launching the challenge (implemented by socaz).
POW_BYPASS_HASHUnset, no PoW bypassString 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_AS4096Non-negative integerRLIMIT_AS (in MiB) for setrlimit(2): maximum size of process virtual address space. Passed as --rlimit_as to nsjail.
RLIMIT_FSIZE1Non-negative integerRLIMIT_FSIZE (in MiB) for setrlimit(2): maximum size for newly created files. Passed as --rlimit_fsize to nsjail.
RLIMIT_NOFILE32Non-negative integerRLIMIT_NOFILE for setrlimit(2): 1 more than the highest possible file descriptor number for the process. Passed as --rlimit_nofile to nsjail.
RLIMIT_STACKContainer soft-limitNon-negative integerRLIMIT_STACK (in MiB) for setrlimit(2): maximum size for the stack of the process. Passed as --rlimit_stack to nsjail.
SET_INTERPRETERUnset, use container loaderAbsolute path within containerPath 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_RPATHUnset, use container librariesAbsolute path within containerPath 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 dirsColon-separated (:) list of absolute directory pathsDirectories 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_SIZE4194304(4MiB)Non-negative integerSize (in bytes) of the tmpfs mounts specified in TEMPDIRS. Sets the size= mount option for each mount.
TIMEOUT60Positive integerTime (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.

Dynamic loader and libraries

Custom dynamic loader and/or custom dynamic libraries can be set up in different ways:

  1. 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.

  2. 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'
    
  3. 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.

Addiotional Notes

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 Command

docker pull cybersecnatlab/challenge-jail