Hisham Ladha

NorthSec 2025 - Writeup: News of the Seas Challenge

Northsec is home to some of the most interesting CTF challenges I have ever come across. To be honest, a lot of them require pretty in-depth knowledge of certain topics which can sometimes be roadblocks. However, these serve as great learning opportunities and help you realize just how vast the world of cybersecurity is.

One of the challenges I really enjoyed and managed to solve was "News of the seas".

TLDR;

It was a web challenge that leveraged a vulnerability in Django's ORM method objects.filter() and when combined with pythons dictionary unpacking, leads to sensitive information disclosure. Don't worry if you don't understand what this means, I'll try to dumb it down below.

Investigating the website

The challenge description included the website and source code to the backend with a brief hint letting us know that there was a user called Admin. Our objective was to try to find the password of the Admin user before the heist gets compromised! (The heist was part of this years Northsec theme)

Anyways, opening up the home page, we see that it is a blog "News of the seas" showing a couple of articles written by a user called "Joe Creator".

Clicking on the link below "Filter by Shocking News", essentially filters the blog posts and returns only news with the keyword "shock". Interestingly, we see a new directory /search with a Key-value pair content__contains=shock. This is a bit strange to see as it looks like a method (__contains) is being called in the url 🤔 Let's come back to this later...

Investigating the source code

We have 2 interesting files:

  1. views.py This file contains the function handlers for different routes in the application. I noticed a few things at first:

  1. model.py Here we see the different tables in the db.

Taking a look at how the search functionality works

As I mentioned above, the /search endpoint seems to be taking in some URL parameters which are directly evaluated by the filter function on line 14 of views.py.

Let's see if we can play around with the /search endpoint to understand whats happening.

Investigating Article.objects.filter()

In order to understand how Django is filtering for records in the database using the url parameters, let's briefly understand how it works with the help of w3 schools: https://www.w3schools.com/django/django_queryset_filter.php

Above, we have seen that it is possible to filter for articles based on the db attributes and field lookup references.

The question is, what can we do with this?

Trying to find JoeCreators password

At this point, I realized that since we are able to use field lookups and query for db attributes directly in the url, perhaps this can expose some more information.

It is important to mention that since the Article.objects.filter() function takes in the argument of **request.GET.dict(). This essentially means that it is possible to chain multiple key-value parameter pairs and feed them into the filter() function.

Now, lets see if we can retrieve articles based on some guesswork about Joe Creators password. Is he dumb enough to have his password simply as "password"?

This is done by chaining two different key-value pairs that get fed into the filter function. In the end, the function call ends up looking like this:

articles = Article.objects.filter(author__name="Joe Creator", author__password__startswith="p")

Of course not, maybe he's dumber than we thought? Does his password start with "j", meaning that his password is simply his name "joecreator"? lets find out!

YES! Joe truly isn't the brightest bulb 🤣 Evidently, his password turned out to be "joecreator".

Now, this doesn't really reveal anything about the password of the Admin user, which is our objective. However, this tells us that we can directly query the database based on the plaintext password of a user!

Trying to find the Admins password

As seen in the User class in the source code, there is an attribute called created_by which is pretty self explanatory.

My theory is, if it is possible to filter for articles from Joe Creator, where the user Joe Creator was created by Admin, then we can essentially try to enumerate the admins password.

WHAT! The user Joe Creator was not created by admin? How does this make any sense.

At this point, I thought that perhaps the query was not working or something was wrong. Surely, a user had to have been created by the Admin, right?

Another user created Joe Creator!!

After thinking for a really long time, I decided to try to check whether another user created Joe Creator and if so, then surely that intermediate user had to be created by Admin, correct?

Something along these lines: Admin created User Z who created Joe Creator

The query looked something like this: /search?author__name=Joe%20Creator&author__created_by__created_by__name=Admin

Alas! It worked! This means that we are now able to query for the user attributes of Admin.

Bruteforcing the Admins password

Looking at the field lookups for the filter() method in Django's ORM, I noticed that there was the startswith lookup value.

Perhaps, using the startswith field lookup to try to guess the Admin password might yield something interesting?

The query looks like this: /search?author__name=Joe%20Creator&author__created_by__created_by__password__startswith=a

and roughly translates to: "Filter for all articles written by Joe Creator AND check whether the password of the Admin (who is the one who created User Z who in turn, created Joe Creator) starts with the letter a". Wow, thats a mouthful.

This didn't work but, maybe another character will work.

Checking whether the password starts with "c" actually worked!

BOOM! At this point, I was sure that trying to guess the password manually by typing it in the url would be impossible and therefore decided to create a python script with the help of Gemini 2.5 pro.

The python script essentially did the following:

  1. Make a request to the url with the same parameters above.
  2. Keep appending alphanumeric characters to the end of "c" and for each possible character, check if it returns articles.
  3. If it does, append it to the current known beginning of the Admins password.
  4. Keep doing so until appending all possible characters yields no articles, meaning that we now have the full password of the Admin!

Would you look at that! It worked!!

Verifying this by supplying the value as the password for the user Admin to the login function, returns the flag!

#CyberSecurity