-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Andrew Steurer <[email protected]>
- Loading branch information
Andrew Steurer
committed
Aug 18, 2024
1 parent
18b992b
commit e3715a9
Showing
10 changed files
with
402 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module command-injection | ||
|
||
go 1.22.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.