Category: web
Difficulty: medium
Author: ghostccamm
Files:
Ports Open: 80
Jeff hates frontend web development, but made a sick REST API using Django for his new secure blog.
Can you hack into the admin portal and grab the flag?
There are two major parts to solve this challenge.
- Compromise a Django administrator account.
- Using the compromised account, figure out a way to bypass the IP address whitelist to access the Django administrator portal
Below I will explain how to solve each section.
Viewing the available articles on /api//articles
, we can see there is a Django administrator account named jeff
that we should try to compromise.
In the provided source code, students will see that the Article
model has a relational mapping to a Django user account for the created_by
field.
from django.db import models
from django.contrib.auth.models import User
class Article(models.Model):
"""
Test Article model
"""
title = models.CharField(max_length=255)
body = models.TextField()
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self) -> str:
return f"{self.title}-{self.created_by.username}"
class Meta:
ordering = ["title"]
Below is the code for the view that query article objects.
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from app.models import Article
from app.serializers import ArticleSerializer
class ArticleView(APIView):
"""
View for Articles
"""
def get(self, request: Request, format=None):
"""
Just return all articles
"""
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
def post(self, request: Request, format=None):
"""
Query articles
"""
articles = Article.objects.filter(**request.data)
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
Of particular interest is Article.objects.filter(**request.data)
, that directly inserts the user's input into the filter statement. Django's Object Relational Mapper (ORM) allows filtering objects by attributes on relational fields. In addition, a field lookup can be injected to manipulate querying data that can be exploited to infer values stored. Combining these two facts together, the hashed password can be leaked character by character by querying created_by__password__startswith
. For an example sending a the following JSON POST requests to /api//articles
will result in:
a list of articles since account password hashes on Django start with pbkdf2
{
"created_by__password__startswith": "pbkdf2"
}
no articles since none of the account password hashes start with pbkdf2T
{
"created_by__password__startswith": "pbkdf2T"
}
This is enough to perform an ORM IDOR injection attack. The solve script pass_leak.py will exploit this vulnerability to dump the password hash for the account named jeff
, as shown in the following GIF.
This dumps the following password hash
pbkdf2_sha256$1000$057C2I2qdGH98Hm2CSkiKZ$6Eq+K931+YFv4OV578LDDDyFoWEp2OClbcnRF1qxHjE=
Running the following hashcat to crack the password hash saved in a file named jeff.hash
using the rockyou.txt
wordlist.
hashcat -m 10000 jeff.hash /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
This will crack the password hash and retrieve the password for the account jeff
is shrekndonkey
.
So now we know the password for the admin account named jeff
. However, if we directly visit /admin
the Nginx web server blocks the request.
Below is the Nginx configuration file that is provided with the source code.
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
index index.html;
root /var/www/html;
}
# I think I did this correctly... had some issues with Django but fixed it now
location ~ ^/(api|admin) {
# Make sure no one else can access /admin besides myself
location ~ ^/admin {
allow 127.0.0.1;
deny all;
}
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host 127.0.0.1:8000;
proxy_set_header Origin http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Explaining in english, the location
block will reverse proxy all requests to the Django web application that start with either /api
or /admin
. If the URI path starts with /admin
, then it will deny all requests except from 127.0.0.1
.
However, if we check the URL route configuration for the django web application in djangoapp/secureblog/urls.py
we can see that re_path
is used instead of path
for matching URL paths on Django.
from django.contrib import admin
from django.urls import re_path
from app.views import ArticleView
# Had some weird issues when combined with NGINX reverse proxy earlier so using re_path instead of path
urlpatterns = [
re_path('admin/', admin.site.urls),
re_path('api/articles/', ArticleView.as_view()),
]
re_path
performs a regex pattern match when routing URL paths. Since the admin/
URL pattern is listed at the top, it takes precedence if the Regex patten admin/
matches the URL path of a request.
Combining the two regex pattern matches to bypass, we start our URL path with /api
so Nginx will proxy the request Django app and bypass the ^/admin
localhost only block. Then we append admin/
to the URL path so Django will route the traffic to the admin views. Some example URL paths are /apiadmin/
or /api/admin/
as shown below.
Exploiting this, you can log in as jeff:shrekndonkey
and view the flag that is stored in one of the models. Do note you will need to keep on adding /api
to the start of the URL path since Django will keep on trying to redirect to /admin
.