Writing a Custom Burp Suite Extension to Detect Mass Assignment Vulnerabilities in APIs

Crafting a Burp Suite Extension for Mass Assignment Detection

Detecting mass assignment vulnerabilities in APIs manually is often a tedious process, requiring constant parameter manipulation and observation. This post details the development of a custom Burp Suite extension designed to automate the initial discovery of these flaws, specifically targeting JSON-based API endpoints. The goal is to intercept relevant HTTP requests, inject common sensitive parameters into the request body, and analyze the server's response for potential indication of successful assignment.

Understanding Mass Assignment in APIs

Mass assignment, also known as "object injection" or "auto-binding," occurs when an application automatically converts client-supplied data (e.g., JSON, form data) into internal object properties without proper filtering or whitelisting. This can allow an attacker to modify properties that were not intended to be exposed or changed, such as `isAdmin`, `user_role`, `account_balance`, or `verified`. An API endpoint designed to update a user's `name` and `email` might inadvertently accept and process an `isAdmin: true` parameter if not explicitly handled. Identifying these during reconnaissance with tools like Zondex for exposed services can streamline the testing process significantly.

Setting Up the Burp Extension Environment

To develop a Burp Suite extension in Python, you'll need a Jython standalone JAR.

Jython Interpreter Configuration

1. Download the latest Jython standalone JAR. 2. In Burp Suite, navigate to `Extender -> Options -> Python Environment`. 3. Set the "Location of Jython standalone JAR file" to the path of your downloaded JAR. 4. Add the directory where you'll store your Python extension script to the "Folders for loading modules."

The Extension Blueprint: IBurpExtender and IHttpListener

Every Burp Suite extension in Python must implement the `IBurpExtender` interface. For our mass assignment detector, we also need to implement `IHttpListener` to intercept and modify HTTP traffic.

IBurpExtender Implementation

This interface defines the entry point for your extension. The `registerExtenderCallbacks` method is crucial, providing access to Burp's API. We'll use this to register our HTTP listener, set the extension name, and get helper functions.

IHttpListener for Interception

The `processHttpMessage` method is the heart of our detection logic. Burp calls this method for every HTTP request and response. We'll examine requests here, modify them, and analyze subsequent responses.

Core Logic: Parameter Tampering and Response Analysis

The extension's effectiveness hinges on intelligently injecting parameters and accurately interpreting the server's reaction.

Identifying Target Requests

We're primarily interested in requests that modify data, typically `POST`, `PUT`, or `PATCH` requests to API endpoints, especially those with JSON request bodies. We filter out static file requests and other noise to focus on relevant targets. The use of a proxy like GProxy can help manage and route specific traffic through Burp for more focused analysis.

Crafting Malicious Payloads

For each identified request, we parse its JSON body. We then construct a new JSON body by adding a set of predefined, common "sensitive" parameters (e.g., `isAdmin`, `role`, `user_id`, `price`, `accountStatus`) with various values (e.g., `true`, `admin`, `1`, `active`). A baseline request without our injected parameters but with identical original parameters is also sent to establish a control.

Analyzing API Responses

After sending the modified request, we compare its response to the baseline response. Key indicators of potential mass assignment include:

  • A 200 OK status code when the original request received a different status, or the modified request received a 200 OK instead of an error.
  • Significant changes in response body length compared to the baseline, suggesting new data was processed.
  • Specific sensitive data appearing in the response (e.g., `isAdmin: true` in a profile update response after injecting `isAdmin: true`).
  • Error messages that indicate an attempt to set an unauthorized property, which can still be a finding.

Extension Code Walkthrough

This Python script for Burp Suite demonstrates a practical approach to detecting mass assignment.

import json
from burp import IBurpExtender
from burp import IHttpListener
from burp import IHttpRequestResponse
from burp import IExtensionHelpers

class BurpExtender(IBurpExtender, IHttpListener):

    EXTENSION_NAME = "Mass Assignment Detector"
    SENSITIVE_PARAMS = [
        ("isAdmin", True), ("isAdmin", 1), ("isAdmin", "true"),
        ("is_admin", True), ("is_admin", 1), ("is_admin", "true"),
        ("role", "admin"), ("role", "administrator"), ("role", "super_admin"),
        ("user_id", 1), ("user_id", "1"),
        ("accountStatus", "active"), ("accountStatus", "verified"),
        ("price", 0), ("price", 1000000),
        ("isPremium", True), ("isPremium", 1),
        ("active", True), ("active", 1),
        ("userType", "privileged"), ("userType", "staff")
    ]
    SCOPE_ONLY = True # Set to True to only process in-scope items

    def registerExtenderCallbacks(self, callbacks):
        self.callbacks = callbacks
        self.helpers = callbacks.getHelpers()
        callbacks.setExtensionName(self.EXTENSION_NAME)
        callbacks.registerHttpListener(self)
        callbacks.issueAlert("Mass Assignment Detector loaded successfully.")
        print(f"{self.EXTENSION_NAME} loaded.")

    def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
        # We only care about requests
        if not messageIsRequest:
            return

        # Only process requests if in scope and not from other Burp tools (e.g., Scanner, Intruder)
        # 16 is BSS_TOOL_PROXY, 4 is BSS_TOOL_REPEATER
        if toolFlag not in (self.callbacks.TOOL_PROXY, self.callbacks.TOOL_REPEATER):
            return

        if self.SCOPE_ONLY and not self.callbacks.isInScope(messageInfo.getUrl()):
            return

        requestInfo = self.helpers.analyzeRequest(messageInfo)
        content_type = requestInfo.getContentType()
        method = requestInfo.getMethod()

        # Target JSON POST/PUT/PATCH requests
        if method in ("POST", "PUT", "PATCH") and content_type == IExtensionHelpers.CONTENT_TYPE_JSON:
            httpService = messageInfo.getHttpService()
            requestBytes = messageInfo.getRequest()
            
            # Extract request body
            requestBodyOffset = requestInfo.getBodyOffset()
            requestBody = self.helpers.bytesToString(requestBytes[requestBodyOffset:])

            try:
                original_json_data = json.loads(requestBody)
            except ValueError:
                # Not valid JSON, skip
                return

            print(f"[*] Processing request to: {messageInfo.getUrl()}")
            self.callbacks.issueAlert(f"[*] Processing potential mass assignment target: {messageInfo.getUrl()}")

            # --- Step 1: Send a baseline request (original body + dummy param) ---
            # This helps differentiate actual mass assignment from general error handling
            baseline_json_data = dict(original_json_data)
            baseline_json_data["_dummy_param_"] = "test" # Add a harmless dummy param
            baseline_body = self.helpers.stringToBytes(json.dumps(baseline_json_data))
            
            baseline_request = self.helpers.buildHttpMessage(
                self.helpers.buildRequestHeaders(requestInfo),
                baseline_body
            )
            baseline_response_info = self.callbacks.makeHttpRequest(httpService, baseline_request)
            baseline_response = self.helpers.bytesToString(baseline_response_info.getResponse())
            baseline_response_analyzed = self.helpers.analyzeResponse(baseline_response_info.getResponse())
            baseline_status_code = baseline_response_analyzed.getStatusCode()
            baseline_body_len = len(self.helpers.bytesToString(baseline_response_info.getResponse()[baseline_response_analyzed.getBodyOffset():]))

            # --- Step 2: Iterate and inject sensitive parameters ---
            for param_name, param_value in self.SENSITIVE_PARAMS:
                modified_json_data = dict(original_json_data)
                modified_json_data[param_name] = param_value
                
                modified_body = self.helpers.stringToBytes(json.dumps(modified_json_data))
                
                # Build the new request
                modified_request = self.helpers.buildHttpMessage(
                    self.helpers.buildRequestHeaders(requestInfo),
                    modified_body
                )

                # Send the modified request
                response_info = self.callbacks.makeHttpRequest(httpService, modified_request)
                response = self.helpers.bytesToString(response_info.getResponse())
                
                # Analyze response
                response_analyzed = self.helpers.analyzeResponse(response_info.getResponse())
                status_code = response_analyzed.getStatusCode()
                response_body_len = len(self.helpers.bytesToString(response_info.getResponse()[response_analyzed.getBodyOffset():]))

                # --- Step 3: Compare and Report ---
                if status_code == 200 and param_name in response:
                    alert_message = f"Mass Assignment Vulnerability Likely: '{param_name}' set to '{param_value}' and reflected in response at {messageInfo.getUrl()}."
                    self.callbacks.issueAlert(alert_message)
                    print(f"[!] {alert_message}")
                    # Optionally, log the request/response pair for manual review
                    self.callbacks.addScanIssue(
                        self.create_mass_assignment_issue(
                            messageInfo.getHttpService(),
                            messageInfo.getUrl(),
                            [messageInfo.getRequest(), modified_request, response_info.getResponse()],
                            alert_message,
                            "High"
                        )
                    )
                elif status_code != baseline_status_code:
                    alert_message = (f"Mass Assignment Potential: Status code changed from {baseline_status_code} "
                                     f"to {status_code} for '{param_name}':'{param_value}' at {messageInfo.getUrl()}.")
                    self.callbacks.issueAlert(alert_message)
                    print(f"[?] {alert_message}")
                    self.callbacks.addScanIssue(
                        self.create_mass_assignment_issue(
                            messageInfo.getHttpService(),
                            messageInfo.getUrl(),
                            [messageInfo.getRequest(), modified_request, response_info.getResponse()],
                            alert_message,
                            "Medium"
                        )
                    )
                elif abs(response_body_len - baseline_body_len) > 20: # Arbitrary threshold for body length change
                    alert_message = (f"Mass Assignment Potential: Response body length changed significantly "
                                     f"for '{param_name}':'{param_value}' at {messageInfo.getUrl()}. "
                                     f"(Baseline: {baseline_body_len}, Modified: {response_body_len})")
                    self.callbacks.issueAlert(alert_message)
                    print(f"[?] {alert_message}")
                    self.callbacks.addScanIssue(
                        self.create_mass_assignment_issue(
                            messageInfo.getHttpService(),
                            messageInfo.getUrl(),
                            [messageInfo.getRequest(), modified_request, response_info.getResponse()],
                            alert_message,
                            "Low"
                        )
                    )
                # You could add more sophisticated checks here, like diffing JSON responses

    def create_mass_assignment_issue(self, http_service, url, http_messages, issue_detail, severity):
        return CustomScanIssue(
            http_service,
            url,
            http_messages,
            self.EXTENSION_NAME,
            "Mass Assignment Vulnerability",
            issue_detail,
            severity,
            "Certain API endpoints may allow unauthorized modification of sensitive object properties by accepting unexpected parameters. The application should explicitly whitelist allowed parameters.",
            None
        )

# Custom Scan Issue Class for reporting
class CustomScanIssue(self.callbacks.getHelpers().createCustomScanIssue):
    def __init__(self, httpService, url, httpMessages, name, issueName, issueDetail, severity, confidence, background):
        self.httpService = httpService
        self.url = url
        self.httpMessages = httpMessages
        self.name = name
        self.issueName = issueName
        self.issueDetail = issueDetail
        self.severity = severity
        self.confidence = confidence
        self.background = background

    def getUrl(self):
        return self.url

    def getIssueName(self):
        return self.issueName

    def getIssueType(self):
        return 0 # A generic issue type

    def getSeverity(self):
        return self.severity

    def getConfidence(self):
        return "Certain" # Or other appropriate level

    def getIssueBackground(self):
        return self.background

    def getRemediationBackground(self):
        return "Implement strict input validation and whitelist acceptable parameters on the server side."

    def getIssueDetail(self):
        return self.issueDetail

    def getRemediationDetail(self):
        return None

    def getHttpMessages(self):
        return self.httpMessages

    def getHttpService(self):
        return self.httpService

Imports and Global Setup

We import necessary Burp API interfaces and `json` for parsing. `SENSITIVE_PARAMS` holds tuples of parameter names and values to inject. The `SCOPE_ONLY` flag helps limit the extension's activity to in-scope items.

BurpExtender Class Structure

The `registerExtenderCallbacks` method initializes `callbacks` and `helpers` objects, sets the extension name, and registers the HTTP listener. It also prints an alert to the Burp UI and console.

processHttpMessage in Detail

1. **Filtering**: The method first checks if the message is a request, if it's from the Proxy or Repeater tools, and if it's in scope. 2. **Request Analysis**: It analyzes the request to get the method and content type. We specifically target `POST`, `PUT`, or `PATCH` requests with `application/json` content. 3. **JSON Parsing**: The request body is extracted and parsed as JSON. If parsing fails, the request is skipped. 4. **Baseline Request**: A modified request is created by adding a harmless `_dummy_param_` to the original JSON. This baseline is sent, and its response status code and body length are recorded. This helps establish a control for comparison. 5. **Parameter Injection Loop**: For each sensitive parameter in `SENSITIVE_PARAMS`, a new JSON payload is constructed by adding the sensitive parameter and its value to the original request's body. 6. **Sending Modified Request**: The new request is built using `self.helpers.buildHttpMessage` and sent via `self.callbacks.makeHttpRequest`. 7. **Response Comparison**: The response to the injected request is analyzed. * If a `200 OK` is returned and the injected parameter is reflected in the response body, it's a high-confidence finding. * If the status code changes significantly from the baseline, it's flagged as a medium-confidence finding. * If the response body length changes significantly (here, an arbitrary threshold of 20 bytes), it suggests a low-confidence finding for further investigation. * Each finding generates a Burp alert and a new entry in the "Scanner Issues" tab, providing context and severity. Tools like Secably can then import such issues from Burp reports for comprehensive vulnerability management.

Deployment and Testing

1. Save the code as a Python file (e.g., `mass_assignment_detector.py`). 2. In Burp Suite, go to `Extender -> Extensions`. 3. Click "Add," select "Python" as the extension type, and choose your `.py` file. 4. Ensure the output in the "Output" tab indicates successful loading. 5. Browse an application, especially one with JSON-based API interactions (e.g., user profile updates, settings changes). Use Burp's Proxy to intercept traffic. The extension will automatically process relevant requests and report findings in the "Alerts" tab and "Scanner Issues."