diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..5dbf5ed --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,24 @@ +name: Build and push database client Docker image +on: + push: + branches: + - main +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v4 + + - name: build and tag container image + run: make build + + - name: login to GitHub container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: push container image + run: make push \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9bdbc07 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang + +COPY go.mod main.go ./ + +RUN go mod download && \ + go build -o main main.go && \ + apt-get update && \ + apt-get install -y iputils-ping + +ENTRYPOINT [ "./main" ] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6391ce --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: install + +build: + docker build . -t asteurer/test + +push: + docker push ghcr.io/kube-hack/command-injection + +install: + helm upgrade --install command-injection ./chart + +uninstall: + helm uninstall command-injection \ No newline at end of file diff --git a/README.md b/README.md index e69de29..81aaade 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,65 @@ +# Overview + +This is an educational resource demonstrating a web server running in a Kubernetes cluster that has a command injection vulnerability. Here are a few suggestions for how you might use this repository: + +1. Practice your hacking skills by getting the contents of the `flag_to_capture` file, which is stored somewhere on the web server. + +2. Read the source code found in `main.go` and the solution guide found in `solution/README.md` to better-understand what command injection vulnerabilities look like, how to exploit them, and how to prevent them. + +3. Use this as a guide/inspiration for building your own applications with vulnerabilities. + +The instructions and solutions were written assuming you are using some kind of Linux distribution (sorry Windows :grimacing:), whether Ubuntu, MacOS, or another Debian-based OS. + +## \*\*\*\*\**DISCLAIMER*\*\*\*\*\* + +This is an application with a built-in security vulnerability. Please don't deploy the Helm chart into a production environment. There are also instructions showing how to exploit command injection vulnerabilities, so please don't use this to break any laws :grin:. + +# Usage + +## Requirements + +- Latest version of [Helm](https://helm.sh/docs/intro/install/) +- Latest version of [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) +- A fully-compliant Kubernetes distribution (i.e. microk8s, k3s, k3d) that is running on Linux/amd64, and is using containerd or Docker as the runtime. + +## Deploying to Kubernetes + +Add the Helm chart repository: + +```sh +helm repo add kube-hack https://kube-hack.github.io/charts +``` + +Update the charts in your Helm repository: + +```sh +helm repo update +``` + +Deploy the chart to your Kubernetes cluster: + +```sh +helm install command-injection kube-hack/command-injection +``` + +## Interacting with the application + +### Port-forward the application + +```sh +kubectl port-forward svc/web-server-command-injection 3000:3000 +``` + +After the application is port-forwarded (accessible via localhost), you can open your browser and enter `localhost:3000` to access the web application. + +### Using the application + +This is a web application that sends a `ping` request to a url on the internet, and prints the response on the page. Enter a website (i.e. google.com) into the field next to the `Enter a URL or Domain:` label and click the `Ping` button to see the response. + +### Validating the value of the flag + +If you have found the `flag_to_capture` file, send a `curl` request to `localhost:3000/validate` with the contents of the file as a string in the request payload: + +```sh +curl --data-binary "your-string-here" localhost:3000/validate +``` diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..bfd1cbd --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: command-injection +description: A chart that deploys a web server that has a command injection vulnerability. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 diff --git a/chart/files/templates/index.html b/chart/files/templates/index.html new file mode 100644 index 0000000..e8cb889 --- /dev/null +++ b/chart/files/templates/index.html @@ -0,0 +1,47 @@ + + + + + + + Ping Result + + + +

Ping a Host

+
+ + + +
+ +
+ + diff --git a/chart/templates/server.yaml b/chart/templates/server.yaml new file mode 100644 index 0000000..e86d1e8 --- /dev/null +++ b/chart/templates/server.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-server-command-injection +spec: + replicas: 1 + selector: + matchLabels: + app: web-server-command-injection + template: + metadata: + labels: + app: web-server-command-injection + spec: + containers: + - name: web-server-command-injection + image: ghcr.io/kube-hack/command-injection:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + volumeMounts: + - mountPath: /go/templates + name: templates + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + volumes: + - name: templates + configMap: + name: html-templates-command-injection + +--- + +apiVersion: v1 +kind: ConfigMap +metadata: + name: html-templates-command-injection +data: +{{ (.Files.Glob "files/templates/*").AsConfig | indent 4 }} + +--- + +apiVersion: v1 +kind: Service +metadata: + name: web-server-command-injection + labels: + app: web-server-command-injection +spec: + type: ClusterIP + ports: + - port: 3000 + targetPort: 8080 + selector: + app: web-server-command-injection \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c8a93cd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module command-injection + +go 1.22.4 diff --git a/main.go b/main.go new file mode 100644 index 0000000..c364ce1 --- /dev/null +++ b/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "html/template" + "io" + "math/rand" + "net/http" + "os" + "os/exec" + "path" + "time" +) + +var secretFilePath string + +func main() { + // The flag is created in a random location with a new value every time the container is restarted + path, err := createFlag() + if err != nil { + panic(err) + } + + secretFilePath = path + + http.HandleFunc("/", ping) + http.HandleFunc("/validate", validate) + http.ListenAndServe(":8080", nil) +} + +// ping sends a ping request to the requested web address +func ping(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tmpl, err := template.ParseFiles("templates/index.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl.Execute(w, nil) + + } else if r.Method == http.MethodPost { + requestBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer r.Body.Close() + + cmdString := fmt.Sprintf("ping -c 2 %s", string(requestBytes)) + + cmd := exec.Command("sh", "-c", cmdString) + + outputBytes, err := cmd.CombinedOutput() + if err != nil { + errorMessage := fmt.Sprintf("unable to ping address:\n%v\n%s\n", err, outputBytes) + http.Error(w, errorMessage, http.StatusBadRequest) + return + } + + w.Write(outputBytes) + } +} + +// validate checks the flag file's content with the request body to see if they match +func validate(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer r.Body.Close() + + fileBytes, err := os.ReadFile(secretFilePath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if string(fileBytes) == string(bodyBytes) { + w.Write([]byte("Success! You found the flag.")) + } else { + w.Write([]byte("The string submitted doesn't match the content of the flag file.")) + } + } +} + +// createFlag will select a directory in root at random and place a flag file with a randomly-generated string +func createFlag() (string, error) { + rootDir, err := os.ReadDir("/") + if err != nil { + return "", err + } + + randBytes := getRandomByteArray(64) + + // Integer of the random bytes + randNum := binary.BigEndian.Uint64(randBytes) + + // Random number corresponding to the number of directories in root + folderPosition := int(randNum) % (len(rootDir) - 1) + + // Placing the flag in a random directory in root + path := path.Join("/", rootDir[folderPosition].Name(), "flag_to_capture") + + outfile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return "", err + } + + if _, err := io.Copy(outfile, bytes.NewReader(randBytes)); err != nil { + return "", err + } + outfile.Close() + + return path, nil +} + +// Adapted from https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go +func getRandomByteArray(length int) []byte { + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, length) + + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + + return b +} diff --git a/solution/README.md b/solution/README.md new file mode 100644 index 0000000..1ca9ed4 --- /dev/null +++ b/solution/README.md @@ -0,0 +1,40 @@ +# What is command injection? + +Command injection is a vulnerability that allows the execution of unauthorized commands on the host of a web-facing application. This means that someone who exploits a command injection vulnerability has a lot of options: +- Retrieving Kubernetes service account token, which they could use to run `kubectl` commands. +- Viewing environment variables that could contain API keys, passwords, etc. +- Altering the web server's static and executable files to redirect users to a malicious website. +- Accessing other services running on the same network as the web server host. + +# How to identify a command injection vulnerability + +If application spawns a shell and passes user input into the shell command, this makes it vulnerable to command injection: + +```go +cmdString := fmt.Sprintf("ping -c 2 %s", userInput) +cmd := exec.Command("sh", "-c", cmdString) +``` + +In the above example, there is nothing stopping a user from passing in any command they would like to run, including `google.com; rm --no-preserve-root -rf /`, which ends the `ping` command and deletes every file on the host. + +# How to guard against command injection + +You should always use a built-in Golang library to run commands on the host. Rather than running `ping` via `exec.Command`, we might consider using this library instead: [pro-bing](https://github.com/prometheus-community/pro-bing). + +If there is a software running on the host that doesn't have a built-in Golang library, you should never spawn a shell to run commmands. Instead, run commands directly with the user input as arguments: + +```go +cmd := exec.Command("ping", "-c", "2", userInput) +``` + +This will prevent the user from executing any other commands other than `ping`; however, keep in mimd that this does not prevent them from adding unauthorized arguments or flags to a command. + +# How to hack into the web server + +Our objective is to retrieve the file contents of the `flag_to_capture` file, so we need to run a `find` command at the root. In the URL field of the web application, we'll send this string to the web server to get the contents of the file: + +```sh +google.com; cat $(find / -type f -name 'flag_to_capture') +``` + +*****Note***: Each time the Kubernetes pod hosting the web server is restarted, the flag file's contents will be different, and will be randomly placed in a different directory. \ No newline at end of file