From 65e64edc912d488f35b4075c3e803aa81bc596f8 Mon Sep 17 00:00:00 2001 From: Praneeth Sarode Date: Tue, 15 Apr 2025 23:29:49 +0530 Subject: [PATCH] feat: add support for ssh challenge type along with docs Signed-off-by: Praneeth Sarode --- _examples/simple-ssh/Dockerfile | 29 +++++++++++++++++++++ _examples/simple-ssh/beast.toml | 19 ++++++++++++++ core/config/challenge.go | 46 +++++++++++++++++++++++++++------ core/constants.go | 5 ++-- core/manager/pipeline.go | 2 +- core/manager/utils.go | 2 +- docs/ChallTypes.md | 20 +++++++++++--- docs/SampleChallenges.md | 34 +++++++++++++++++++++++- 8 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 _examples/simple-ssh/Dockerfile create mode 100644 _examples/simple-ssh/beast.toml diff --git a/_examples/simple-ssh/Dockerfile b/_examples/simple-ssh/Dockerfile new file mode 100644 index 00000000..3704aaf0 --- /dev/null +++ b/_examples/simple-ssh/Dockerfile @@ -0,0 +1,29 @@ +FROM ubuntu:16.04 + +# Install SSH server and other required packages +RUN apt-get update && apt-get install -y openssh-server sudo vim \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Configure SSH server +RUN mkdir /var/run/sshd +RUN echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config +RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config + +# Create a CTF user with a default password +RUN useradd -m -s /bin/bash ctf-user \ + && echo "ctf-user:ctf-password" | chpasswd \ + && echo "ctf-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ctf-user + +# Add the challenge flag in a hidden location +RUN echo "FLAG{SSH_CHALLENGE_FLAG}" > /home/ctf-user/.hidden_flag \ + && chmod 600 /home/ctf-user/.hidden_flag + +# Add a welcome message +RUN echo "Welcome to the SSH Challenge! Find the hidden flag." > /etc/motd + +# Expose SSH port +EXPOSE 22 + +# Start SSH service +CMD ["/usr/sbin/sshd", "-D"] diff --git a/_examples/simple-ssh/beast.toml b/_examples/simple-ssh/beast.toml new file mode 100644 index 00000000..cb62fade --- /dev/null +++ b/_examples/simple-ssh/beast.toml @@ -0,0 +1,19 @@ +[author] +name = "ph03nix" +email = "author@example.com" +ssh_key = "ssh-ed25519 AAAAC3NzaC1lZD" + +[challenge.metadata] +name = "simple-ssh" +flag = "FLAG{SSH_CHALLENGE_FLAG}" +type = "ssh" +points = 150 +description = "SSH into the server and find the flag. Here are the creds: ctf-user:ctf-password" + +[[challenge.metadata.hints]] +text = "Check for hidden files in the home directory" +points = 10 + +[challenge.env] +docker_context = "Dockerfile" +port_mappings = ["14442:22"] diff --git a/core/config/challenge.go b/core/config/challenge.go index 1fcfe113..d2684c8b 100644 --- a/core/config/challenge.go +++ b/core/config/challenge.go @@ -236,9 +236,9 @@ func (config *ChallengeMetadata) ValidateRequiredFields() (error, bool) { // # Exists for flexibility reasons try to use existing base iamges wherever possible. // base_image = "" // -// # Docker file name for specific type challenge - `docker`. +// # Docker file name for specific type challenge - `docker` or `ssh`. // # Helps to build flexible images for specific user-custom challenges -// docket_context = "" +// docker_context = "" // // # Environment variables that can be used in the application code. // [[var]] @@ -301,15 +301,25 @@ func checkIfPortExistInMapping(portMapping []cr.PortMapping, port uint32) bool { // GetPortMappings returns the entire port mapping for the challenge from the challenge // environment configuration. -func (config *ChallengeEnv) GetPortMappings() ([]cr.PortMapping, error) { +func (config *ChallengeEnv) GetPortMappings(challengeType string) ([]cr.PortMapping, error) { var mapping []cr.PortMapping var containerPorts []uint32 + + // If user has explicitly specified a host port to be mapped to the container's SSH port, + // then use that port mapping. Otherwise, instead of mapping the ports in config.Ports list + // as port:port, map the first port in the list as port:core.SSH_PORT, and the rest normally. + var sshPortMapped = false for _, portMap := range config.PortMappings { hp, cp, err := utils.ParsePortMapping(portMap) if err != nil { return mapping, err } + + if cp == core.SSH_PORT { + sshPortMapped = true + } + mapping = append(mapping, NewPortMapping(hp, cp)) containerPorts = append(containerPorts, cp) } @@ -317,7 +327,12 @@ func (config *ChallengeEnv) GetPortMappings() ([]cr.PortMapping, error) { for _, port := range config.Ports { if !utils.UInt32InList(port, containerPorts) { containerPorts = append(containerPorts, port) - mapping = append(mapping, NewPortMapping(port, port)) + if !sshPortMapped && challengeType == core.SSH_CHALLENGE_TYPE_NAME { + mapping = append(mapping, NewPortMapping(port, core.SSH_PORT)) + sshPortMapped = true + } else { + mapping = append(mapping, NewPortMapping(port, port)) + } } } @@ -373,8 +388,8 @@ func (config *ChallengeEnv) GetAllContainerPorts() ([]uint32, error) { // GetDefaultPort returns the default port used by the challenge from the challenge environment // configuration. -func (config *ChallengeEnv) GetDefaultPort() uint32 { - mappings, err := config.GetPortMappings() +func (config *ChallengeEnv) GetDefaultPort(challengeType string) uint32 { + mappings, err := config.GetPortMappings(challengeType) if err != nil || len(mappings) == 0 { return 0 } @@ -395,7 +410,7 @@ func (config *ChallengeEnv) ValidateRequiredFields(challType string, challdir st return fmt.Errorf("max ports allowed for challenge : %d given : %d", core.MAX_PORT_PER_CHALL, len(config.Ports)) } - portMappings, err := config.GetPortMappings() + portMappings, err := config.GetPortMappings(challType) if err != nil { return fmt.Errorf("error while parsing port mapping: %s", err) } @@ -430,7 +445,7 @@ func (config *ChallengeEnv) ValidateRequiredFields(challType string, challdir st } // Run command is only a required value in case of bare challenge types. - if config.RunCmd == "" && config.Entrypoint == "" && config.DockerCtx=="" && challType == core.BARE_CHALLENGE_TYPE_NAME { + if config.RunCmd == "" && config.Entrypoint == "" && config.DockerCtx == "" && challType == core.BARE_CHALLENGE_TYPE_NAME { return fmt.Errorf("a valid run_cmd should be provided for the challenge environment") } @@ -464,6 +479,21 @@ func (config *ChallengeEnv) ValidateRequiredFields(challType string, challdir st return fmt.Errorf("web Root directory does not exist") } } + } else if challType == core.SSH_CHALLENGE_TYPE_NAME { + // Challenge type is SSH which requires docker_context + if config.DockerCtx == "" { + return errors.New("docker_context can not be empty for SSH challenges") + } else if config.DockerCtx != "" { + if filepath.IsAbs(config.DockerCtx) { + return fmt.Errorf("docker_context path should be relative to challenge directory root") + } else if err := utils.ValidateFileExists(filepath.Join(challdir, config.DockerCtx)); err != nil { + return fmt.Errorf("docker_context file does not exist") + } + } + + if !checkIfPortExistInMapping(portMappings, core.SSH_PORT) { + log.Warnf("No SSH port (22) found in port mappings for SSH challenge, it might not be accessible via SSH") + } } for _, script := range config.SetupScripts { diff --git a/core/constants.go b/core/constants.go index 310dbf4e..db045a51 100644 --- a/core/constants.go +++ b/core/constants.go @@ -56,6 +56,7 @@ const ( //chall types SERVICE_CHALLENGE_TYPE_NAME string = "service" WEB_CHALLENGE_TYPE_NAME string = "web" BARE_CHALLENGE_TYPE_NAME string = "bare" + SSH_CHALLENGE_TYPE_NAME string = "ssh" ) const ( // chall actions @@ -89,7 +90,7 @@ const ( // default config ITERATIONS int = 65536 HASH_LENGTH int = 32 TIMEPERIOD int64 = 6 * 60 * 60 - SSH_PORT int = 22 + SSH_PORT uint32 = 22 ) const ( // roles @@ -138,7 +139,7 @@ var SIDECAR_ENV_PREFIX = map[string]string{ } // Available challenge types -var AVAILABLE_CHALLENGE_TYPES = []string{STATIC_CHALLENGE_TYPE_NAME, SERVICE_CHALLENGE_TYPE_NAME, BARE_CHALLENGE_TYPE_NAME, WEB_CHALLENGE_TYPE_NAME} +var AVAILABLE_CHALLENGE_TYPES = []string{STATIC_CHALLENGE_TYPE_NAME, SERVICE_CHALLENGE_TYPE_NAME, BARE_CHALLENGE_TYPE_NAME, WEB_CHALLENGE_TYPE_NAME, SSH_CHALLENGE_TYPE_NAME} var DockerBaseImageForWebChall = map[string]map[string]map[string]string{ "php": { diff --git a/core/manager/pipeline.go b/core/manager/pipeline.go index 94ffc661..c6bd5de2 100644 --- a/core/manager/pipeline.go +++ b/core/manager/pipeline.go @@ -256,7 +256,7 @@ func deployChallenge(challenge *database.Challenge, config cfg.BeastChallengeCon // Since till this point we have already valiadated the challenge config this is highly // unlikely to fail. - portMapping, err := config.Challenge.Env.GetPortMappings() + portMapping, err := config.Challenge.Env.GetPortMappings(config.Challenge.Metadata.Type) if err != nil { return fmt.Errorf("error while parsing port mapping for the challenge %s: %s", config.Challenge.Metadata.Name, err) } diff --git a/core/manager/utils.go b/core/manager/utils.go index a31e878b..21c28036 100644 --- a/core/manager/utils.go +++ b/core/manager/utils.go @@ -366,7 +366,7 @@ func appendAdditionalFileContexts(additionalCtx map[string]string, config *cfg.B return fmt.Errorf("error while parsing Xinetd config template :: %s", err) } - port := config.Challenge.Env.GetDefaultPort() + port := config.Challenge.Env.GetDefaultPort(config.Challenge.Metadata.Type) data := BeastXinetdConf{ Port: fmt.Sprintf("%d", port), diff --git a/docs/ChallTypes.md b/docs/ChallTypes.md index 6816ee7a..68885e2e 100644 --- a/docs/ChallTypes.md +++ b/docs/ChallTypes.md @@ -4,7 +4,7 @@ Any service whether it is a binary file, or a shell script, which needs to be instantiated on every connection can be easily hosted using `service` type challenge. **Xinetd** is for hosting these type of challenges inside a docker container. -###Primary Requirements +### Primary Requirements ```toml # Relative path to binary or script which needs to be executed when the specified @@ -21,7 +21,7 @@ Web challenges are hosted using the corresponding images from Dockerhub. Current * Python : Django and Flask * Php -###Primary Requirements +### Primary Requirements ```toml # Relative directory corresponding to root of the challenge where the root @@ -57,10 +57,24 @@ entrypoint = "" Authors might have tested the challenges in a isolated docker environment and might not want to port the challenge to one of these types. So they can use `docker` type challenge in which you can provide your own docker context file and ports. -###Primary Requirements +### Primary Requirements ```toml # Docker file name for specific type challenge - `docker`. # Helps to build flexible images for specific user-custom challenges docket_context = "" ``` + +## SSH Challenge + +SSH challenges allow participants to connect directly to a container via SSH. This challenge type requires a dockerfile to be provided, which will be run with the necessary port mappings to allow SSH access. If the 22 port has been explicitly mapped to a host port using `port_mappings` then it will be mapped that host port. Otherwise, if the `ports` list has been used, then the first port of that list, will be mapped to the container's 22 port. + +### Primary Requirements + +```toml +# Docker file name for the SSH challenge +docker_context = "" + +# Port to be exposed for SSH access (typically 22) +port_mappings = ["14442:22"] +``` diff --git a/docs/SampleChallenges.md b/docs/SampleChallenges.md index 9c469e4b..bc74b07f 100644 --- a/docs/SampleChallenges.md +++ b/docs/SampleChallenges.md @@ -184,7 +184,7 @@ The type of challenge consist of the following format - `web:php::< For deploying a challenge with database requirement beast sidecars needs to be used. -``` +```toml [author] name = "fristonio" email = "deepeshpathak09@gmail.com" @@ -241,3 +241,35 @@ For other databases, more information can be found on Sidecars documentation. Make sure that you are installing all the mysql related dependencies in the `apt_deps` configuration parameter. As in the above case `php*-mysql` + +## SSH Challenges + +SSH challenges allow participants to connect directly to a container using SSH and interact with a shell environment to solve the challenge. These challenges are ideal for scenarios where participants need to explore a system, find hidden files, or exploit vulnerabilities in a controlled environment. + +```toml +[author] +name = "ph03n1x" +email = "author@example.com" +ssh_key = "ssh-rsa AAAAB3NzaC1y..." + +[challenge.metadata] +name = "simple-ssh" +flag = "FLAG{SSH_CHALLENGE_FLAG}" +type = "ssh" +points = 150 +description = "SSH into the server and find the flag. The creds are ctf-user:ctf-password" + +[[challenge.metadata.hints]] +text = "Check for hidden files in the home directory" +points = 10 + +[challenge.env] +docker_context = "Dockerfile" +port_mappings = ["14442:22"] # Map external port 14442 to container's SSH port 22 +``` + +Participants will be able to connect to this challenge using SSH: +``` +ssh ctf-user@challenge-host -p 2222 +Password: ctf-password +```