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:
- views.py This file contains the function handlers for different routes in the application. I noticed a few things at first:
- The search method seems to be returning articles that have been filtered using
Articles.objects.filter
with the function argument being**request.GET.dict()
on line 14. This seems interesting... - The login method is simple and just returns the flag if the correct password for the username "Admin" is supplied.
- model.py Here we see the different tables in the db.
- Interestingly, the author attribute in the Article table is a foreign key to the User table.
- The User table also has a password attribute and a created_by attribute which is a foreign key to itself. This is important to us because if we can theoretically find a way of querying the db for the user Admin, we can easily retrieve the password since it is a simple text field.
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.
AHA! Just passing in the parameters
author__name=Joe%Creator
seems to work and returns all articles written by this user!I wonder if the same works for Admin?
- Sadly, it returned nothing.
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
- In short, it filters for records based on attributes provided like
author__name=Joe%Creator
- However, as we saw with the filtering of "shocking" articles, it can also be used with different field lookups like:
contains
,startswith
,exact
and more.
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.
- To see what else I would be able to reveal, I tried to see whether it was possible to filter for articles from Joe Creator based on his password.
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 thefilter()
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")
- The filter function is defined like: filter(*args, **kwargs)¶
- This essentially runs an SQL
AND
- This essentially runs an SQL
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 thestartswith
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:
- Make a request to the url with the same parameters above.
- Keep appending alphanumeric characters to the end of "c" and for each possible character, check if it returns articles.
- If it does, append it to the current known beginning of the Admins password.
- 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!