Skip to content

Commit

Permalink
updating README and adding files
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Steurer <[email protected]>
  • Loading branch information
Andrew Steurer committed Aug 18, 2024
1 parent 18b992b commit e3715a9
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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" ]
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```
9 changes: 9 additions & 0 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions chart/files/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--Courtesy of ChatGPT-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ping Result</title>
<script>
async function fetchData(event) {
event.preventDefault();

const inputField = document.getElementById("urlInput");
const userInput = inputField.value.trim();

try {
const response = await fetch("/", {
method: "POST",
headers: {
"Content-Type": "text/plain",
},
body: userInput,
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.text();
document.getElementById("result").innerText = data;
} catch (error) {
console.error("Error fetching data:", error);
document.getElementById("result").innerText = "Error fetching data.";
}
}
</script>
</head>
<body>
<h1>Ping a Host</h1>
<form onsubmit="fetchData(event)">
<label for="urlInput">Enter a URL or Domain:</label>
<input type="text" id="urlInput" name="url" required>
<button type="submit">Ping</button>
</form>
<!-- This is where the server response will be displayed -->
<br><div id="result"></div>
</body>
</html>
59 changes: 59 additions & 0 deletions chart/templates/server.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module command-injection

go 1.22.4
132 changes: 132 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit e3715a9

Please sign in to comment.