Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions _examples/simple-ssh/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
19 changes: 19 additions & 0 deletions _examples/simple-ssh/beast.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[author]
name = "ph03nix"
email = "[email protected]"
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"]
46 changes: 38 additions & 8 deletions core/config/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -301,23 +301,38 @@ 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)
}

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))
}
}
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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")
}

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion core/manager/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion core/manager/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
20 changes: 17 additions & 3 deletions docs/ChallTypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"]
```
34 changes: 33 additions & 1 deletion docs/SampleChallenges.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ The type of challenge consist of the following format - `web:php:<PHP Version>:<

For deploying a challenge with database requirement beast sidecars needs to be used.

```
```toml
[author]
name = "fristonio"
email = "[email protected]"
Expand Down Expand Up @@ -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 = "[email protected]"
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
```