Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hosting Machine Challenges

TODO: Docker macvlan networking

Machine challenges can spawn a new environment of machines for players, which can be done using QEMU. Players will need to connect to a VPN (e.g., WireGuard) to access the dynamically created machines.

The VPN will grant users FULL access to your Docker network or Kubernetes pods network.

For Kubernetes, you should write a NetworkPolicy to restrict users' access to machine challenges. For Docker, ensure that Raya and your databases are running on a separate Docker network.

VPN Setup (Docker)

I personally use WireGuard with wg-easy.

The server is configured to route traffic to the Docker network.

Update 10.254.0.0/16 to match your Docker network.

WG_ALLOWED_IPS=10.8.0.0/24, 10.254.0.0/16

POST_UP

iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE;   iptables -A FORWARD -i wg0 -d 10.254.0.0/16 -j ACCEPT; iptables -A FORWARD -i wg0 -s 10.254.0.0/16 -j ACCEPT

POST_DOWN

iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE;    iptables -D FORWARD -i wg0 -d 10.254.0.0/16 -j ACCEPT; iptables -D FORWARD -i wg0 -s 10.254.0.0/16 -j ACCEPT

VPN setup (Kubernetes)

The setup is not different from the docker setup above, just change the docker network address to the Pods CIDR.

Be careful not to disrupt your CNI. For instance, if you're using Calico, it might be configured to use the first detected network interface to determine the node IP. It's possible that wg0 could be the first detected. To avoid this, add a regular expression to skip wg.* interfaces

Machine image

Currently, I deploy machine challenges as a docker container that runs QEMU, the image has a copy of the machine, each time a container is started virt-customize updates the machine image (patch flag.txt so that it has a dynamic flag / add a dynamic user / add a machine environment variable / etc..)

FROM debian:12-slim AS builder

RUN apt-get update && \
    apt-get install -y qemu-system-x86 libguestfs-tools pwgen && \
    rm -rf /var/lib/apt/lists/*

# The OVA file of the challenge
COPY ctf-image.ova /root/ctf-image.ova

RUN tar -xvf /root/ctf-image.ova -C /root/

RUN rm /root/ctf-image.ova

RUN qemu-img convert -f vmdk -O qcow2 /root/*.vmdk /root/ctf-image.qcow2
RUN rm /root/*.vmdk

FROM debian:12-slim AS runner

RUN apt-get update && \
    apt-get install -y qemu-system-x86 libguestfs-tools pwgen && \
    rm -rf /var/lib/apt/lists/*

COPY --from=builder /root/ctf-image.qcow2 /root/ctf-image.qcow2

# Environment variables
ENV VM_MEMORY=1G
ENV VM_CPU=1

# Start script to set up and run the VM
COPY start_vm.sh /root/start_vm.sh
RUN chmod +x /root/start_vm.sh

# Run the start script
CMD ["/root/start_vm.sh"]
#!/bin/bash

VM_IMAGE="/root/ctf-image.qcow2"

echo "Generated SSH password for ctf-player: $SSH_PASSWORD" > /root/ssh_password.txt
echo "Generated SSH password for ctf-player: $SSH_PASSWORD"

# Add a player user
virt-customize --format qcow2 -a "$VM_IMAGE" --password "ctf-player:password:$SSH_PASSWORD"

# Add flag.txt
echo "$FLAG" > flag.txt

# Add a dynamic flag.txt to the machine
virt-customize --format qcow2 -a "$VM_IMAGE" --upload flag.txt:/home/ctf-player/flag.txt
# change the Redis password
virt-customize --format qcow2 -a "$VM_IMAGE" --run-command "sed -i 's/^requirepass .*/requirepass $FLAG/' /etc/redis/redis.conf"


# Ports used by the machine
PORTS_TO_FORWARD=("22" "8000" "8080" "443")

# build hostfwd arguments
HOSTFWD_ARGS=""
for PORT in "${PORTS_TO_FORWARD[@]}"; do
    HOSTFWD_ARGS="$HOSTFWD_ARGS,hostfwd=tcp::${PORT}-:${PORT}"
done
# trim the leading comma
HOSTFWD_ARGS="${HOSTFWD_ARGS#,}"

qemu-system-x86_64 -hda "$VM_IMAGE" -m "$VM_MEMORY" \
	-enable-kvm -smp "$VM_CPU" -boot c -nographic -serial mon:stdio \
	-device e1000,netdev=net0 \
	-netdev user,id=net0,$HOSTFWD_ARGS

PreInstanceCreation.lua

local function generate_hex(length)
	local hex = ""
	for i = 1, length do
		hex = hex .. string.format("%x", math.random(0, 15))
	end
	return hex
end

local password = generate_hex(10)

raya.docker_options = {
    devices = {"/dev/net/tun", "/dev/kvm"},
	environment = {string.format("SSH_PASSWORD=%s", password)}
}

PostInstanceCreation.lua

raya.description = string.format([[
Connect to the VPN. 

#### Connection details
```sh
ssh ctf-player@%s
```

#### Password 
```
%s
```
]], raya.container.container_ip_address, string.match(raya.docker_options.environment[1], "SSH_PASSWORD=(%w+)"))

raya.instance = {
	show_port = false,
	host = raya.container.container_ip_address
}

Screenshots

A machine challenge in Raya