diff --git a/user-container/Dockerfile b/user-container/Dockerfile index 3e8c32d..de97fc4 100644 --- a/user-container/Dockerfile +++ b/user-container/Dockerfile @@ -19,7 +19,6 @@ RUN rm -f /etc/dpkg/dpkg.cfg.d/excludes && \ grep \ man-db \ manpages \ - openssh-server \ xxd \ sudo \ && if [ "$(dpkg-divert --truename /usr/bin/man)" = "/usr/bin/man.REAL" ]; then rm -f /usr/bin/man && dpkg-divert --quiet --remove --rename /usr/bin/man; fi \ @@ -32,11 +31,6 @@ ENV LESSCHARSET=utf-8 # Create ieee user RUN useradd -m -s /bin/bash ieee -RUN mkdir -p /etc/ssh/sshd_config.d /run/sshd && \ - rm -f /etc/ssh/ssh_host_* && \ - printf 'PermitRootLogin no\nAllowUsers ieee\nPasswordAuthentication yes\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nUseDNS no\nX11Forwarding no\nAllowAgentForwarding no\nAllowTcpForwarding no\nGatewayPorts no\nPermitTunnel no\nDisableForwarding yes\n' > /etc/ssh/sshd_config.d/workshop.conf && \ - chmod 600 /etc/ssh/sshd_config.d/workshop.conf - # Sudo: ieee can ONLY run the verifier helpers and fixed challenge emitters as root RUN echo "ieee ALL=(root) NOPASSWD: /opt/verifier.sh, /opt/cmdlog.sh, /opt/pipes-step1.sh, /opt/pipes-noisy.sh" > /etc/sudoers.d/ieee && \ chmod 440 /etc/sudoers.d/ieee @@ -71,15 +65,27 @@ RUN printf '#!/bin/bash\nset -e\nname=$(basename "$0")\ncase "$name" in\n ver # Run setup to create all challenge files and directories RUN /opt/setup.sh -# Add transparent command logging to .bashrc (logged via sudo to root-only file) -RUN echo '' >> /home/ieee/.bashrc && \ - echo 'export LANG=C.UTF-8' >> /home/ieee/.bashrc && \ - echo 'export LESSCHARSET=utf-8' >> /home/ieee/.bashrc && \ - echo '_log_cmd() { local c=$(history 1 | sed "s/^[[:space:]]*[0-9]*[[:space:]]*//"); [ -n "$c" ] && cmdlog "$c"; }' >> /home/ieee/.bashrc && \ - echo 'PROMPT_COMMAND="_log_cmd;${PROMPT_COMMAND}"' >> /home/ieee/.bashrc && \ - chown ieee:ieee /home/ieee/.bashrc +# Keep a clean baseline for challenge 7.3 resets. +RUN cp /home/ieee/.bashrc /opt/.bashrc.baseline && chmod 644 /opt/.bashrc.baseline + +# Add transparent command logging outside the student's ~/.bashrc so challenge 7.3 +# cannot break later verifier checks by accident. +RUN printf '%s\n' \ + 'export LANG=C.UTF-8' \ + 'export LESSCHARSET=utf-8' \ + '_workshop_log_cmd() {' \ + ' local c' \ + ' c=$(history 1 | sed "s/^[[:space:]]*[0-9]*[[:space:]]*//")' \ + ' [ -n "$c" ] && cmdlog "$c"' \ + '}' \ + 'case ";${PROMPT_COMMAND:-};" in' \ + ' *";_workshop_log_cmd;"*) ;;' \ + ' "") PROMPT_COMMAND="_workshop_log_cmd" ;;' \ + ' *) PROMPT_COMMAND="_workshop_log_cmd;${PROMPT_COMMAND}" ;;' \ + 'esac' \ + > /etc/profile.d/workshop-shell.sh && \ + chmod 644 /etc/profile.d/workshop-shell.sh # Entrypoint runs as root (starts services, then drops to ieee) WORKDIR /home/ieee -EXPOSE 22 ENTRYPOINT ["/opt/entrypoint.sh"] diff --git a/user-container/entrypoint.sh b/user-container/entrypoint.sh index 173b991..8db2848 100644 --- a/user-container/entrypoint.sh +++ b/user-container/entrypoint.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail # entrypoint.sh — Runs as root: starts services, then drops to ieee shell AUDIT_DIR="/home/ieee/challenges/perms/audit" @@ -14,7 +15,6 @@ HIDDEN_FLAG_FILE="/opt/.hidden_flag" HIDDEN_PORT_FILE="/opt/.hidden_port" WEB_SERVICE_CONFIG="/opt/.web_service" HIDDEN_SERVICE_CONFIG="/opt/.hidden_service" -SSH_SECRET_ENV_CONFIG="/etc/ssh/sshd_config.d/workshop-secret.conf" generate_token() { printf '%s%s\n' "$1" "$(head -c 6 /dev/urandom | xxd -p)" @@ -25,7 +25,10 @@ generate_hidden_port() { } write_secure_token() { - generate_token "$1" | tee "$2" + local token + + token=$(generate_token "$1") + printf '%s\n' "$token" > "$2" chmod 600 "$2" } @@ -76,20 +79,17 @@ write_service_config() { chmod 600 "$1" } -write_secret_env_config() { - local secret - - secret=$(cat "$SECRET_FLAG_FILE") - mkdir -p /etc/ssh/sshd_config.d - printf 'SetEnv SECRET_FLAG=%s\n' "$secret" > "$SSH_SECRET_ENV_CONFIG" - chmod 600 "$SSH_SECRET_ENV_CONFIG" -} - -configure_ssh_access() { - : "${IEEE_PASSWORD:=ieee}" - echo "ieee:${IEEE_PASSWORD}" | chpasswd - mkdir -p /run/sshd /etc/ssh/sshd_config.d - ssh-keygen -A >/dev/null 2>&1 +start_flag_service() { + local config_file="$1" + local pid + + python3 /opt/flag-server.py "$config_file" & + pid=$! + sleep 0.2 + if ! kill -0 "$pid" 2>/dev/null; then + echo "Failed to start flag service for $config_file" >&2 + exit 1 + fi } start_network_services() { @@ -100,8 +100,8 @@ start_network_services() { write_service_config "$WEB_SERVICE_CONFIG" 8080 "$WEB_FLAG_FILE" write_service_config "$HIDDEN_SERVICE_CONFIG" "$hidden_port" "$HIDDEN_FLAG_FILE" - python3 /opt/flag-server.py "$WEB_SERVICE_CONFIG" & - python3 /opt/flag-server.py "$HIDDEN_SERVICE_CONFIG" & + start_flag_service "$WEB_SERVICE_CONFIG" + start_flag_service "$HIDDEN_SERVICE_CONFIG" } # ============================================================ @@ -201,9 +201,6 @@ if [ ! -f "$HIDDEN_FLAG_FILE" ] || [ ! -f "$HIDDEN_PORT_FILE" ]; then fi write_getflag_script -rm -f /etc/profile.d/workshop-secret.sh -write_secret_env_config -configure_ssh_access SPY_NAME=$(cat "$SPY_NAME_FILE") SECRET_FLAG=$(cat "$SECRET_FLAG_FILE") @@ -246,12 +243,11 @@ touch /opt/.progress /opt/.reset_markers chmod 600 /opt/.progress /opt/.reset_markers # ============================================================ -# SSH server + optional local shell +# Runtime mode # ============================================================ if [ -t 0 ] && [ -t 1 ]; then /usr/local/bin/welcome - /usr/sbin/sshd -D -e & export SECRET_FLAG export HOME="$HOME_DIR" export USER="ieee" @@ -259,5 +255,5 @@ if [ -t 0 ] && [ -t 1 ]; then export SHELL="/bin/bash" exec runuser --preserve-environment -u ieee -- bash -l else - exec /usr/sbin/sshd -D -e + exec tail -f /dev/null fi diff --git a/user-container/verifier.sh b/user-container/verifier.sh index fba9aa2..f56f9c3 100644 --- a/user-container/verifier.sh +++ b/user-container/verifier.sh @@ -17,7 +17,7 @@ HIDDEN_FLAG_FILE="/opt/.hidden_flag" HIDDEN_PORT_FILE="/opt/.hidden_port" WEB_SERVICE_CONFIG="/opt/.web_service" HIDDEN_SERVICE_CONFIG="/opt/.hidden_service" -SSH_SECRET_ENV_CONFIG="/etc/ssh/sshd_config.d/workshop-secret.conf" +BASHRC_BASELINE="/opt/.bashrc.baseline" RED='\033[0;31m' GREEN='\033[0;32m' @@ -102,8 +102,12 @@ generate_hidden_port() { } write_secure_token() { - generate_token "$1" | tee "$2" + local token + + token=$(generate_token "$1") + printf '%s\n' "$token" > "$2" chmod 600 "$2" + printf '%s\n' "$token" } verify_file_match() { @@ -203,13 +207,54 @@ write_service_config() { chmod 600 "$1" } -write_secret_env_config() { - local secret +normalize_path() { + if [ -z "${1:-}" ]; then + return 1 + fi + readlink -f -- "$1" 2>/dev/null +} + +sorted_normalized_file_paths() { + local file="$1" + local line + local normalized=() + + [ -f "$file" ] || return 1 + + while IFS= read -r line; do + [ -n "$line" ] || continue + line=${line%$'\r'} + if [[ "$line" != /* ]]; then + line="$HOME/$line" + fi + line=$(normalize_path "$line") || continue + normalized+=("$line") + done < "$file" + + [ "${#normalized[@]}" -gt 0 ] || return 1 + printf '%s\n' "${normalized[@]}" | sort -u +} + +worker_secret_process_running() { + local proc_dir + local cmdline + local environ + + for proc_dir in /proc/[0-9]*; do + [ -r "$proc_dir/cmdline" ] || continue + [ -r "$proc_dir/environ" ] || continue + + cmdline=$(tr '\0' ' ' < "$proc_dir/cmdline" 2>/dev/null || true) + case "$cmdline" in + worker_[1-4]\ --config=/etc/*.conf\ 99999\ *) ;; + *) continue ;; + esac - secret=$(cat "$SECRET_FLAG_FILE" 2>/dev/null) - mkdir -p /etc/ssh/sshd_config.d - printf 'SetEnv SECRET_FLAG=%s\n' "$secret" > "$SSH_SECRET_ENV_CONFIG" - chmod 600 "$SSH_SECRET_ENV_CONFIG" + environ=$(tr '\0' '\n' < "$proc_dir/environ" 2>/dev/null || true) + grep -Fx "PROC_SECRET=$1" <<< "$environ" > /dev/null 2>&1 && return 0 + done + + return 1 } start_flag_service() { @@ -263,33 +308,22 @@ reset_1_1() { # ============================================================ verify_1_2() { - local flag_file latest_file latest_atime atime f + local flag_file atime flag_file="$HOME/labyrinth/right/passage/chamber/flag.txt" - latest_file="" - latest_atime=0 - - while IFS= read -r f; do - atime=$(stat -c %X "$f" 2>/dev/null || echo 0) - if [ "$atime" -gt "$latest_atime" ]; then - latest_atime=$atime - latest_file="$f" - fi - done < <(find "$HOME/labyrinth" -type f 2>/dev/null) + atime=$(stat -c %X "$flag_file" 2>/dev/null || echo 0) - if [ "$latest_atime" -le 946684800 ]; then - fail "Navigate through ~/labyrinth/ and read the flag.txt file." - hint "Use 'ls' to explore directories and 'cd' to move into them." - elif [ "$latest_file" = "$flag_file" ]; then + if [ "$atime" -gt 946684800 ] || logged_since "1.2" "((^|[[:space:];|&(])((cat|less|more|head|tail|grep|sed|awk)([[:space:]]+[^;&|()]*)*[[:space:]]+([^[:space:]]*/)?flag\.txt([[:space:];|&)]|$))|((^|[[:space:];|&(])([^[:space:]]*/)?flag\.txt([[:space:]]|$)))"; then pass "1.2" "labyrinth_solved" else - fail "flag.txt wasn't the last labyrinth file you read." - hint "Once you find flag.txt, read it after exploring the dead ends." + fail "Navigate through ~/labyrinth/ and read the flag.txt file." + hint "Use 'ls' to explore directories and 'cd' to move into them." fi } reset_1_2() { find "$HOME/labyrinth" -type f -exec touch -a -t 200001010000.00 {} + + set_log_marker "1.2" reset_msg "1.2" } @@ -398,11 +432,20 @@ reset_2_3() { # ============================================================ verify_3_1() { - if runuser -u ieee -- test -x "$HOME/challenges/perms/show_flag.sh"; then - pass "3.1" "execute_permission" - else + local out + + if ! runuser -u ieee -- test -x "$HOME/challenges/perms/show_flag.sh"; then fail "The script still isn't executable." hint "chmod can add the execute permission to a file." + return + fi + + out=$(runuser -u ieee -- "$HOME/challenges/perms/show_flag.sh" 2>/dev/null || true) + if [ "$out" = "EXECUTE_SUCCESS" ]; then + pass "3.1" "execute_permission" + else + fail "The script is executable, but it does not run correctly yet." + hint "Run the script directly and make sure it prints the expected message." fi } @@ -434,27 +477,29 @@ reset_3_2() { # ============================================================ verify_3_3() { - AUDIT_DIR="$HOME/challenges/perms/audit" - TARGET="" - LATEST="" - LATEST_ATIME=0 - for f in "$AUDIT_DIR"/report*.txt; do - PERM=$(stat -c '%a' "$f") - [ "$PERM" = "600" ] && TARGET="$f" - ATIME=$(stat -c '%X' "$f") - if [ "$ATIME" -gt "$LATEST_ATIME" ]; then - LATEST_ATIME=$ATIME - LATEST="$f" - fi + local audit_dir target perm atime target_name f + + audit_dir="$HOME/challenges/perms/audit" + target="" + for f in "$audit_dir"/report*.txt; do + perm=$(stat -c '%a' "$f" 2>/dev/null || true) + [ "$perm" = "600" ] && target="$f" done - if [ "$LATEST_ATIME" -le 946684800 ]; then - fail "You haven't read any of the audit files yet." - hint "Use 'ls -l' to inspect permissions, then read the file that matches 600." - elif [ "$LATEST" = "$TARGET" ]; then + + if [ -z "$target" ]; then + fail "Challenge state is broken — no 600-permission file found." + hint "Run 'reset 3.3' to restore the challenge state." + return + fi + + atime=$(stat -c '%X' "$target" 2>/dev/null || echo 0) + target_name=$(basename "$target") + + if [ "$atime" -gt 946684800 ] || logged_since "3.3" "((^|[[:space:];|&(])((cat|less|more|head|tail|grep|sed|awk)([[:space:]]+[^;&|()]*)*[[:space:]]+([^[:space:]]*/)?${target_name}([[:space:];|&)]|$))|((^|[[:space:];|&(])([^[:space:]]*/)?${target_name}([[:space:]]|$)))"; then pass "3.3" "permission_reader" else - fail "The last file you read wasn't the one with 600 permissions." - hint "Look at the 'ls -l' output more carefully. Which permission string shows only owner read/write?" + fail "You haven't read the 600-permission file yet." + hint "Use 'ls -l' to inspect permissions, then read the file that matches 600." fi } @@ -476,6 +521,7 @@ reset_3_3() { done chown ieee:ieee "$HOME/challenges/perms/audit"/report*.txt touch -a -t 200001010000.00 "$HOME/challenges/perms/audit"/report*.txt + set_log_marker "3.3" reset_msg "3.3" } @@ -556,14 +602,14 @@ reset_4_3() { verify_5_1() { EXPECTED=$(cat "$BIGFILE_PATH_FILE" 2>/dev/null) - ACTUAL=$(cat "$HOME/bigfile.txt" 2>/dev/null | tr -d '[:space:]') + ACTUAL=$(sed -n '1{s/[[:space:]]*$//;p;}' "$HOME/bigfile.txt" 2>/dev/null) if [ -z "$EXPECTED" ]; then fail "Challenge state is missing the large file path." hint "Run 'reset 5.1' to restore the challenge state, then try again." elif [ -z "$ACTUAL" ]; then fail "File ~/bigfile.txt not found or is empty." hint "Use 'find' with a size filter, then save the result to ~/bigfile.txt." - elif [ "$ACTUAL" = "$EXPECTED" ]; then + elif [ "$(normalize_path "$ACTUAL")" = "$(normalize_path "$EXPECTED")" ]; then pass "5.1" "big_file_found" else fail "~/bigfile.txt doesn't contain the correct path." @@ -582,10 +628,23 @@ reset_5_1() { # ============================================================ verify_5_2() { - verify_file_match "5.2" "recursive_grep" "token.txt" \ - "$(grep -r "WORKSHOP_TOKEN" "$HOME/challenges/search/data/" 2>/dev/null)" \ - "Use grep to search recursively, then redirect the output to ~/token.txt." \ - "Search for the exact string 'WORKSHOP_TOKEN' across all files in the directory." + local expected_recursive + local expected_line + local actual + + expected_recursive=$(grep -r "WORKSHOP_TOKEN" "$HOME/challenges/search/data/" 2>/dev/null) + expected_line=$(printf '%s\n' "$expected_recursive" | sed 's|^[^:]*:||') + actual=$(cat "$HOME/token.txt" 2>/dev/null) + + if [ -z "$actual" ]; then + fail "File ~/token.txt not found or is empty." + hint "Use grep to search recursively, then redirect the matching result to ~/token.txt." + elif [ "$actual" = "$expected_recursive" ] || [ "$actual" = "$expected_line" ]; then + pass "5.2" "recursive_grep" + else + fail "~/token.txt doesn't contain the correct grep result." + hint "Search for the exact string 'WORKSHOP_TOKEN' across the directory and save either the recursive grep result or the matching line." + fi } reset_5_2() { @@ -612,7 +671,7 @@ reset_5_2() { verify_5_3() { EXPECTED=$(find "$HOME/challenges/search/configs/" -name "*.conf" -mtime -7 2>/dev/null | sort) - ACTUAL=$(sort "$HOME/recent.txt" 2>/dev/null) + ACTUAL=$(sorted_normalized_file_paths "$HOME/recent.txt" 2>/dev/null || true) if [ -z "$ACTUAL" ]; then fail "File ~/recent.txt not found or is empty." hint "Use 'find' with a time filter, then redirect the output to ~/recent.txt." @@ -721,7 +780,7 @@ verify_6_3() { fail "File ~/worker_secret.txt not found or is empty." hint "Find the right worker PID, read /proc//environ, and save just the secret value to ~/worker_secret.txt." elif [ "$ACTUAL" = "$EXPECTED" ]; then - if worker_secret_running "$EXPECTED"; then + if worker_secret_process_running "$EXPECTED"; then pass "6.3" "proc_explorer" else fail "The worker carrying the secret doesn't seem to be running right now." @@ -786,7 +845,6 @@ reset_7_1() { if [ ! -f "$SECRET_FLAG_FILE" ]; then write_secure_token ENV_ "$SECRET_FLAG_FILE" > /dev/null fi - write_secret_env_config rm -f "$HOME/secret_flag.txt" set_log_marker "7.1" reset_msg "7.1" @@ -806,9 +864,9 @@ verify_7_2() { elif [ -z "$ACTUAL" ]; then fail "File ~/path_flag.txt not found or is empty." hint "Add ~/challenges/path/bin to PATH, run getflag, and save its output to ~/path_flag.txt." - elif [ "$ACTUAL" = "$EXPECTED" ] && logged_since "7.2" "export.*PATH.*path/bin" && logged_since "7.2" "\bgetflag\b"; then + elif [ "$ACTUAL" = "$EXPECTED" ] && logged_since "7.2" "(export.*PATH|PATH=).*path/bin" && logged_since "7.2" "\bgetflag\b"; then pass "7.2" "path_updated" - elif [ "$ACTUAL" = "$EXPECTED" ] && logged_since "7.2" "export.*PATH.*path/bin"; then + elif [ "$ACTUAL" = "$EXPECTED" ] && logged_since "7.2" "(export.*PATH|PATH=).*path/bin"; then fail "You updated your PATH — now run 'getflag'." elif [ "$ACTUAL" = "$EXPECTED" ] && logged_since "7.2" "\bgetflag\b"; then fail "getflag won't work until you add its directory to PATH." @@ -816,7 +874,7 @@ verify_7_2() { elif [[ "$ACTUAL" == *[[:space:]]* ]]; then fail "~/path_flag.txt should contain only the output of getflag." hint "Save just the command output, not the command itself or extra text." - elif logged_since "7.2" "export.*PATH.*path/bin" && logged_since "7.2" "\bgetflag\b"; then + elif logged_since "7.2" "(export.*PATH|PATH=).*path/bin" && logged_since "7.2" "\bgetflag\b"; then fail "~/path_flag.txt doesn't contain the correct getflag output." hint "Run getflag after updating PATH, then save only its output to ~/path_flag.txt." else @@ -852,8 +910,12 @@ verify_7_3() { } reset_7_3() { - # Remove any alias hello line from .bashrc - sed -i "/^alias hello=/d" "$HOME/.bashrc" + if [ -f "$BASHRC_BASELINE" ]; then + cp "$BASHRC_BASELINE" "$HOME/.bashrc" + chown ieee:ieee "$HOME/.bashrc" + else + sed -i "/^alias hello=/d" "$HOME/.bashrc" + fi reset_msg "7.3" } diff --git a/web-server/install-host-broker.sh b/web-server/install-host-broker.sh index 7c4980d..96a167c 100755 --- a/web-server/install-host-broker.sh +++ b/web-server/install-host-broker.sh @@ -192,8 +192,39 @@ iptables -A WORKSHOP-EGRESS -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN iptables -A WORKSHOP-EGRESS -s "$WORKSHOP_SUBNET" -j REJECT --reject-with icmp-admin-prohibited iptables -C DOCKER-USER -j WORKSHOP-EGRESS >/dev/null 2>&1 || iptables -I DOCKER-USER 1 -j WORKSHOP-EGRESS +# Persist egress rules across reboots via a dedicated systemd service. +# Docker must be up before rules are applied so DOCKER-USER chain exists. +cat > /usr/local/lib/workshop/workshop-iptables.sh </dev/null || true +iptables -F WORKSHOP-EGRESS +iptables -A WORKSHOP-EGRESS -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN +iptables -A WORKSHOP-EGRESS -s "\$SUBNET" -j REJECT --reject-with icmp-admin-prohibited +iptables -C DOCKER-USER -j WORKSHOP-EGRESS >/dev/null 2>&1 || iptables -I DOCKER-USER 1 -j WORKSHOP-EGRESS +EOF +chmod 755 /usr/local/lib/workshop/workshop-iptables.sh + +cat > /etc/systemd/system/workshop-iptables.service <<'EOF' +[Unit] +Description=Workshop container egress firewall +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/lib/workshop/workshop-iptables.sh + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload manage_ssh_service +systemctl enable workshop-iptables.service >/dev/null systemctl enable workshop-registration.service >/dev/null systemctl restart workshop-registration.service >/dev/null diff --git a/web-server/uninstall-host-broker.sh b/web-server/uninstall-host-broker.sh index dbc2d59..c39155f 100755 --- a/web-server/uninstall-host-broker.sh +++ b/web-server/uninstall-host-broker.sh @@ -21,10 +21,18 @@ if [ -f /etc/systemd/system/workshop-registration.service ]; then systemctl reset-failed workshop-registration.service >/dev/null 2>&1 || true fi +if [ -f /etc/systemd/system/workshop-iptables.service ]; then + systemctl disable --now workshop-iptables.service >/dev/null 2>&1 || true + rm -f /etc/systemd/system/workshop-iptables.service + systemctl daemon-reload + systemctl reset-failed workshop-iptables.service >/dev/null 2>&1 || true +fi + rm -f /etc/ssh/sshd_config.d/workshop-broker.conf rm -f /etc/sudoers.d/workshop-broker rm -rf /usr/local/lib/workshop/web-server rm -f /usr/local/lib/workshop/workshop-login.sh +rm -f /usr/local/lib/workshop/workshop-iptables.sh rm -f /usr/local/bin/workshop-ops if [ -d /usr/local/lib/workshop ] && [ -z "$(ls -A /usr/local/lib/workshop 2>/dev/null)" ]; then