Skip to content

Latest commit

 

History

History
261 lines (231 loc) · 20 KB

File metadata and controls

261 lines (231 loc) · 20 KB

Here is the combined list of vulnerabilities, formatted as markdown:

Combined Vulnerability Report: Django-Q Project

Vulnerability 1: Insecure Deserialization via Pickle in Task Processing

  • Vulnerability Name: Insecure Deserialization via Pickle in Task Processing

  • Description:

    1. Django-q utilizes pickle serialization through django_q.signing.PickleSerializer to convert tasks into a byte stream before signing and storing them in the broker.
    2. The SignedPackage.dumps method employs pickle.dumps to serialize the task data.
    3. Subsequently, the SignedPackage.loads method uses pickle.loads to deserialize the task data when retrieving it from the broker or cache for processing by pusher process.
    4. pickle.loads is inherently vulnerable to arbitrary code execution when handling untrusted data, as deserialization can be manipulated to execute malicious code.
    5. Should an attacker manage to compromise the SECRET_KEY or circumvent signature verification, or if the broker is misconfigured or exposed, they could inject a maliciously crafted pickled payload into the task queue.
    6. When a worker processes this tampered task, the call to SignedPackage.loads in pusher will deserialize the malicious payload, leading to arbitrary code execution within the worker's environment during task unpacking in pusher before task is placed into task_queue.
  • Impact:

    • Critical: Remote Code Execution (RCE). Exploitation of this vulnerability can lead to remote code execution (RCE) on the host running worker processes. An attacker may execute arbitrary system commands with the same privileges as the worker process, compromising confidentiality, integrity, and availability of the system. Successful exploitation allows an attacker to gain complete control over the worker server, potentially leading to data breaches, service disruption, and further attacks on the infrastructure and the potential for lateral movement to other parts of the infrastructure.
  • Vulnerability Rank: critical

  • Currently Implemented Mitigations:

    • Task packages are signed using Django's signing mechanism (django.core.signing) via SignedPackage class in django_q/signing.py. This is intended to prevent tampering with the task data in transit, so that any alteration of the payload should, in theory, invalidate the signature.
    • Deserialization is performed only after a successful signature check.
    • The SignedPackage.dumps and SignedPackage.loads methods use a SECRET_KEY and PREFIX salt from Django settings to enhance the signature's security.
  • Missing Mitigations:

    • Input Validation and Sanitization: The application lacks proper validation and sanitization of task data before deserialization using pickle.loads. While signing prevents tampering in transit, it does not prevent deserialization vulnerabilities if the original serialized data is malicious. Implement rigorous input validation and sanitization for task functions and arguments before serialization. This should include checks to ensure that the function and arguments are expected and safe, reducing the attack surface.
    • Alternative Serialization Methods: Relying on pickle for deserialization of potentially untrusted data is inherently risky. Moving away from pickle to a safer serialization format like JSON (if feasible for the data structures) would significantly reduce this risk. If pickle is necessary, consider using pickle.safe_load if available and applicable, or explore other secure deserialization practices and replace pickle with safer serialization formats like json or consider using libraries like marshmallow for controlled serialization and deserialization. These alternatives are less prone to arbitrary code execution vulnerabilities.
    • Encryption of Task Packages: In addition to signing, implement encryption of task packages to protect confidentiality and integrity. Encryption ensures that even if an attacker intercepts a task package, they cannot read or modify its contents without the decryption key.
    • Robust SECRET_KEY Management: Enhance SECRET_KEY management practices, including secure storage, regular rotation, and protection against unauthorized access. Employ methods like environment variables, vault systems, or hardware security modules to manage the key securely.
    • Runtime Restrictions, Sandboxing, or Whitelisting: The project still relies entirely on Python’s built‑in pickle module for (de)serialization without additional runtime restrictions, sandboxing, or whitelisting of allowed object types.
    • Fallback to Secure Serialization: There is no fallback to a more secure serialization mechanism (for example, JSON) in production scenarios where untrusted data might be fed into the broker.
  • Preconditions:

    • An attacker needs to be able to submit tasks to the django-q queue or inject or modify messages in the broker. This is often possible if task queuing is exposed through a web interface or API, even indirectly, or if a Redis, Disque, or similar message broker is misconfigured or publicly accessible.
    • The django-q worker needs to be configured to process tasks from a queue that an attacker can influence.
    • The attacker must know, guess, or force the use of the proper signature (by compromising or brute‑forcing the SECRET_KEY), thereby making the malicious payload pass the signature check, or compromise the SECRET_KEY or circumvent signature verification.
    • An insider threat scenario where an authorized user with task creation privileges intentionally injects malicious tasks.
  • Source Code Analysis:

    1. django_q/signing.py:

      import pickle
      
      from django_q import core_signing as signing
      from django_q.conf import Conf
      
      BadSignature = signing.BadSignature
      
      
      class SignedPackage:
          """Wraps Django's signing module with custom Pickle serializer."""
      
          @staticmethod
          def dumps(obj, compressed: bool = Conf.COMPRESSED) -> str:
              return signing.dumps(
                  obj,
                  key=Conf.SECRET_KEY,
                  salt=Conf.PREFIX,
                  compress=compressed,
                  serializer=PickleSerializer,
              )
      
          @staticmethod
          def loads(obj) -> any:
              return signing.loads(
                  obj, key=Conf.SECRET_KEY, salt=Conf.PREFIX, serializer=PickleSerializer
              )
      
      
      class PickleSerializer:
          """Simple wrapper around Pickle for signing.dumps and signing.loads."""
      
          @staticmethod
          def dumps(obj) -> bytes:
              return pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
      
          @staticmethod
          def loads(data) -> any:
              return pickle.loads(data)
      • The SignedPackage class is responsible for signing and loading task packages.
      • SignedPackage.loads uses signing.loads from django_q.core_signing with PickleSerializer.
      • PickleSerializer.loads directly calls pickle.loads(data) without any input validation, which is the root cause of the vulnerability.
    2. django_q/core_signing.py:

      import datetime
      import time
      import zlib
      
      from django.core.signing import BadSignature, JSONSerializer, SignatureExpired
      from django.core.signing import Signer as Sgnr
      from django.core.signing import TimestampSigner as TsS
      from django.core.signing import b64_decode, dumps
      from django.utils import baseconv
      from django.utils.crypto import constant_time_compare
      from django.utils.encoding import force_bytes, force_str
      
      dumps = dumps
      
      """
      The loads function is the same as the `django.core.signing.loads` function
      The difference is that `this` loads function calls `TimestampSigner` and `Signer`
      """
      
      
      def loads(
          s,
          key=None,
          salt: str = "django.core.signing",
          serializer=JSONSerializer,
          max_age=None,
      ):
          """
          Reverse of dumps(), raise BadSignature if signature fails.
      
          The serializer is expected to accept a bytestring.
          """
          # TimestampSigner.unsign() returns str but base64 and zlib compression
          # operate on bytes.
          base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
          decompress = False
          if base64d[:1] == b".":
              # It's compressed; uncompress it first
              base64d = base64d[1:]
              decompress = True
          data = b64_decode(base64d)
          if decompress:
              data = zlib.decompress(data)
          return serializer().loads(data)
      
      # ... (Signer and TimestampSigner classes)
      • This file provides the loads function used by SignedPackage.loads.
      • It includes signature verification using TimestampSigner.unsign, which confirms the integrity of the data during transit.
      • However, the vulnerability persists because after signature verification, the data is still deserialized using the potentially unsafe PickleSerializer().loads(data). The signature only ensures that the data hasn't been tampered with after signing, but not that the original serialized data is safe.
    3. django_q/cluster.py:

      def worker(
          task_queue: Queue, result_queue: Queue, timer: Value, timeout: int = Conf.TIMEOUT
      ):
          # ...
          for task in iter(task_queue.get, "STOP"):
              # ...
              try:
                  f = task["func"]
                  # if it's not an instance try to get it from the string
                  if not callable(task["func"]):
                      f = pydoc.locate(f) # potential code injection if task["func"] is user controlled and not validated
                  # ...
                  res = f(*task["args"], **task["kwargs"]) # execution of deserialized function
                  result = (res, True)
              except Exception as e:
                  result = (f"{e} : {traceback.format_exc()}", False)
                  # ...
              # ...
      
      def pusher(task_queue: Queue, event: Event, broker: Broker = None):
          # ...
          while True:
              # ...
              if task_set:
                  for task in task_set:
                      ack_id = task[0]
                      # unpack the task
                      try:
                          task = SignedPackage.loads(task[1]) # Deserialization happens here
                      except (TypeError, BadSignature) as e:
                          logger.error(e, traceback.format_exc())
                          broker.fail(ack_id)
                          continue
                      task["ack_id"] = ack_id
                      task_queue.put(task)
              # ...
      • The pusher function in cluster.py is where tasks are dequeued from the broker and deserialized using SignedPackage.loads(task[1]). This is the entry point for the malicious payload.
      • The worker function then retrieves tasks from the task_queue and executes the deserialized function f with its arguments.
  • Security Test Case:

    1. Setup: Deploy a publicly accessible instance of a django-q application. Ensure you have access to configure and monitor this instance. For demonstration purposes, you may need to simulate or gain access to the SECRET_KEY. In a real-world scenario, assume the attacker has found a way to compromise or guess the key, or is attempting to exploit a bypass in signature verification, or that the broker (e.g., Redis) is accessible without strong authentication. Configure the Django settings with a known or weak SECRET_KEY.
    2. Craft Malicious Payload: Prepare a Python dictionary that represents a legitimate task structure, but replace the function (func) to be executed with a malicious command. For example, use os.system('touch /tmp/django_q_pwned') as the malicious function. Alternatively, you could inject malicious code within the arguments (args or kwargs) that gets executed upon deserialization. Create a custom Python script that uses Python’s pickle module to construct a payload that, when deserialized, executes an arbitrary command (for example, running os.system("touch /tmp/exploited")).
    import os
    import pickle
    from django_q.signing import SignedPackage
    
    malicious_task = {
        'name': 'malicious_task',
        'func': 'os.system',
        'args': ('touch /tmp/django_q_pwned',),
        'kwargs': {},
        'started': None,
        'stopped': None,
        'success': False,
        'result': None,
    }
    1. Serialize the Malicious Payload: Use pickle.dumps from the standard Python library to serialize the malicious task dictionary into a byte stream.
    pickled_payload = pickle.dumps(malicious_task, protocol=pickle.HIGHEST_PROTOCOL)
    1. Sign the Malicious Payload: Employ the django_q.signing.SignedPackage.dumps method, or directly use django.core.signing.dumps with PickleSerializer, along with the compromised SECRET_KEY to sign the pickled payload. This step creates a signed malicious task package that django-q workers will recognize as valid if the key is indeed compromised or signature verification is bypassed. Sign this payload using the project’s signing function (e.g. via SignedPackage.dumps) so that it passes the signature check.
    from django.conf import settings
    settings.configure(SECRET_KEY='your_django_secret_key', DJANGO_Q_PREFIX='django_q') # Replace 'your_django_secret_key' with the actual SECRET_KEY
    signed_payload = SignedPackage.dumps(malicious_task) # If SECRET_KEY is configured in Django settings
    
    # OR, if you want to manually sign (e.g., for testing with a known key):
    # from django.core import signing
    # signed_payload = signing.dumps(malicious_task, key=settings.SECRET_KEY, salt=settings.DJANGO_Q_PREFIX, serializer=PickleSerializer)
    1. Inject Malicious Task into Queue: Manually inject this crafted, signed malicious package into the django-q task queue. Depending on the broker being used:
      • For Redis broker: Use redis-cli to connect to the Redis instance and execute LPUSH <task_queue_key> <malicious_signed_package>. The task queue key is usually django_q:q. Inject the crafted payload directly into the broker (for instance, using the Redis CLI).
      redis-cli LPUSH django_q:q "<signed_payload_string>"
      • For ORM broker: Directly insert a new record into the django_q_ormq table with the malicious payload. You need to serialize the signed payload to string before inserting.
      from django_q.models import OrmQ
      import base64
      OrmQ.objects.create(key='django_q', payload=base64.b64encode(signed_payload.encode()).decode('utf-8'))
    2. Trigger Task Processing: Allow the django-q cluster (sentinel, pusher, worker, monitor) to be running. The pusher will pick up the task from the queue. Allow the scheduler to run so that it processes this schedule record. When a worker process retrieves the malicious task, verify (by checking for expected side effects such as file creation or log entries) that the payload executes and grants remote code execution.
    3. Verify Code Execution: After the pusher has processed the task, verify if the malicious code was executed on the worker machine. For the example payload os.system('touch /tmp/django_q_pwned'), check if a file named django_q_pwned has been created in the /tmp/ directory of the worker's filesystem. If the file exists, it confirms successful Remote Code Execution (RCE). Demonstrate that replacing the unsafe pickle.loads with a safer alternative (such as using JSON or pickle.safe_load if applicable and available) prevents the malicious code from executing.

This security test case effectively demonstrates how an attacker, by exploiting insecure deserialization via Pickle and potentially compromising the SECRET_KEY or bypassing signature checks, or by exploiting a misconfigured broker, can achieve remote code execution on django-q worker instances. The vulnerability exists in the pusher process during task unpacking before the task is even assigned to a worker for execution.

Vulnerability 2: Unsafe Use of Eval() in Schedule Parameter Parsing

  • Vulnerability Name: Unsafe Use of Eval() in Schedule Parameter Parsing
  • Description: The scheduler code expects schedule records to provide keyword arguments (in the kwargs field) as a string that can be converted into a dictionary. To perform this conversion, the project uses Python’s eval() function wrapped as:
    if s.kwargs:
        try:
            kwargs = eval(f"dict({s.kwargs})")
        except SyntaxError:
            kwargs = {}
    Because eval() executes its input as arbitrary Python code, if an attacker can create or modify a schedule record—via a poorly secured Django admin interface or an exposed scheduling API—they can insert malicious Python code within the string. When the scheduler processes this record, the dangerous code within s.kwargs is executed.
  • Impact:
    • High: Remote Code Execution (RCE). An attacker who successfully injects arbitrary Python code into the schedule’s kwargs can achieve remote code execution in the context of the scheduler. Depending on the privileges of the scheduler, this may allow full system compromise.
  • Vulnerability Rank: High
  • Currently Implemented Mitigations:
    • The code wraps the evaluation in a try/except block, such that if a SyntaxError is raised the schedule is processed with an empty dictionary.
    • It is assumed that schedule records are created exclusively by trusted administrators.
  • Missing Mitigations:
    • Use of ast.literal_eval(): Relying on eval() to convert unsanitized input remains inherently unsafe. A secure alternative such as Python’s ast.literal_eval() should be used to safely parse literals.
    • Input Validation and Sanitization: There is no active validation or sanitization of the string contained in s.kwargs before it is passed to eval(). Input validation should be implemented to sanitize the s.kwargs string before using eval() or preferably ast.literal_eval().
  • Preconditions:
    • The attacker must be able to create or modify schedule records (for example, through an exposed admin interface or an unsecured scheduling API endpoint).
    • The scheduler must run and process the record containing the malicious kwargs string.
  • Source Code Analysis:
    • In the scheduler routine (as evidenced by tests in test_scheduler.py and documented in the code comments), the schedule record’s kwargs is converted using eval(f"dict({s.kwargs})") without any restrictions.
    • This effectively means that any malicious payload inserted into the kwargs field will be executed in the Python runtime.
  • Security Test Case:
    1. Access Schedule Creation Interface: In a controlled environment, obtain access to the schedule creation interface (for example, via the Django admin or a scheduling API).
    2. Create Malicious Schedule Record: Create a schedule record whose kwargs field is set to a malicious string. For instance, use a payload such as:
      "__import__('os').system('touch /tmp/exploited') or {}"
      
    3. Trigger Scheduler: Allow the scheduler to run so that it processes this schedule record.
    4. Verify Code Execution: Verify that the injected command is executed by checking for its side‐effect (e.g., confirm that the file /tmp/exploited has been created).
    5. Test Mitigation: Demonstrate that replacing the unsafe eval() with a safer alternative (such as ast.literal_eval()) prevents the malicious code from executing.