|
| 1 | +--- |
| 2 | +title: Multiple vulnerabilities in Kanboard (Exploiting web applications Part II) |
| 3 | +header: Multiple vulnerabilities in Kanboard (Exploiting web applications Part II) |
| 4 | +tags: ['advisories', 'writeup'] |
| 5 | +cwes: ['Improper Limitation of a Pathname to a Restricted Directory (Path Traversal) (CWE-22)'] |
| 6 | +affected_product: 'Kanboard' |
| 7 | +vulnerability_release_date: '2024-11-11' |
| 8 | +--- |
| 9 | + |
| 10 | +This article is a continuation of a write-up series, where we discuss web application vulnerabilities found during red team operations. This time, the target was the Kanboard software. <!--more--> |
| 11 | + |
| 12 | +### Project Management in Kanboard style |
| 13 | + |
| 14 | +With over 8000 stars on GitHub, [Kanboard](https://github.com/kanboard/kanboard) is one of the most popular applications for organizing projects following the Kanban approach. |
| 15 | + |
| 16 | +During one of our red team assessments, we discovered that our client self-hosts an instance of Kanboard. |
| 17 | +Since it is open-source, we decided to hunt for vulnerabilities by reading the source code and penetration testing in parallel. |
| 18 | + |
| 19 | +So, it all started with a `git clone`. We used the `ack` tool as an in place grep replacement, which helped us find interesting code sections. Browsing through the code leaves the first impression that it is well structured and cleanly written. |
| 20 | + |
| 21 | + |
| 22 | +#### Initial access and juicy features |
| 23 | + |
| 24 | +But first, let's switch to the customer instance again - not having said yet that the good old `admin:admin` credential set helped us out once again :) |
| 25 | +We could successfully authenticate as the administrator and thus open us various possibilities to potentially exploit application functionalities. |
| 26 | + |
| 27 | +Having administrative access to the Kanboard instance also gives us lots of interesting information about the target's project details including network and system configurations. But can we abuse the server? |
| 28 | +We browse through different projects, boards, comments, item details and uploaded attachments. |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +One interesting feature of Kanboard is allowing administrators to download the complete SQLite database as a gzip file or upload it to update the database. |
| 33 | +So we did this: a surprise backup of our target instance. |
| 34 | +We decompress the downloaded file by executing `gzip -d db.sqlite.gz` and open it with an SQLite browser. |
| 35 | + |
| 36 | +We can see the raw data of projects, comments, etc. |
| 37 | +Especially the `project_has_files` table holds our attention, as it stores relative file paths of uploaded files. |
| 38 | +So we switched to the source code repository and looked through the source code, to determine how the the filepaths are read and used within the application. |
| 39 | + |
| 40 | +##### Write and delete files - but what about reading them? |
| 41 | + |
| 42 | +In the web UI, we see that uploaded files can be downloaded through URLs like this: `https://example.com/project/1/file/1/download/<hash>`. |
| 43 | +So searching through the code base for `/download` points us to the underlying PHP class that is responsible for serving files: the [FileViewerController](https://github.com/kanboard/kanboard/blob/v1.2.41/app/Controller/FileViewerController.php). |
| 44 | + |
| 45 | +```bash |
| 46 | +$ ack "/download" |
| 47 | +ServiceProvider/RouteProvider.php |
| 48 | +77: $container['route']->addRoute('project/:project_id/file/:file_id/download/:etag', 'FileViewerController', 'download'); |
| 49 | +148: $container['route']->addRoute('task/:task_id/file/:file_id/download/:etag', 'FileViewerController', 'download'); |
| 50 | +``` |
| 51 | + |
| 52 | +The below `download()` function of the [FileViewerController.php](https://github.com/kanboard/kanboard/blob/v1.2.41/app/Controller/FileViewerController.php#L152) looks very simple. But what exactly do `$this->getFile();` and `$this->objectStorage->output($file['path']);` do? |
| 53 | + |
| 54 | +```php |
| 55 | +public function download() |
| 56 | +{ |
| 57 | + try { |
| 58 | + $file = $this->getFile(); |
| 59 | + $this->response->withFileDownload($file['name']); |
| 60 | + $this->response->send(); |
| 61 | + $this->objectStorage->output($file['path']); |
| 62 | + } catch (ObjectStorageException $e) { |
| 63 | + $this->logger->error($e->getMessage()); |
| 64 | + } |
| 65 | +} |
| 66 | +``` |
| 67 | +`$this->getFile()` is a call to the super class of [BaseController](https://github.com/kanboard/kanboard/blob/v1.2.41/app/Controller/BaseController.php#L93). |
| 68 | + |
| 69 | +```php |
| 70 | +protected function getFile() |
| 71 | +{ |
| 72 | + $project_id = $this->request->getIntegerParam('project_id'); |
| 73 | + $task_id = $this->request->getIntegerParam('task_id'); |
| 74 | + $file_id = $this->request->getIntegerParam('file_id'); |
| 75 | + |
| 76 | + [...] |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +We see that this function parses the `project_id` and `file_id` parameter values that we already saw in the route definition of download URLs. The function essentially parses the two values from the URL, performs a SQL select on the attachments and returns an array with the data, collected from the SQL entry. No path sanitization that we can see so far! |
| 81 | + |
| 82 | +So lets check the `output()` function of the objectStorage, which is defined in [FileStorage.php](https://github.com/kanboard/kanboard/blob/v1.2.41/app/Core/ObjectStorage/FileStorage.php#L75). |
| 83 | + |
| 84 | +```php |
| 85 | +public function output($key) |
| 86 | +{ |
| 87 | + $filename = $this->path.DIRECTORY_SEPARATOR.$key; |
| 88 | + |
| 89 | + if (! file_exists($filename)) { |
| 90 | + throw new ObjectStorageException('File not found: '.$filename); |
| 91 | + } |
| 92 | + |
| 93 | + readfile($filename); |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +The function retrieves the `$key` parameter, which in this case is a relative path from the SQLite database, checks the file's existence and returns its content. No checks - this smells like a arbitrary file read if we are able to modify the path successfully. |
| 98 | + |
| 99 | +Since we are certain that the application is vulnerable to an arbitrary file read, we immediately test it out: |
| 100 | + |
| 101 | +1. We download the database |
| 102 | +2. Decompress it via `gzip -d db.sqlite.gz` |
| 103 | +3. Open it in a SQL browser and go to table `project_has_files`. |
| 104 | +4. For one of the already uploaded files, we modify the `path` to something we want to read `../../../../../../../etc/passwd` |
| 105 | +5. Commit our SQL changes and save the file |
| 106 | +6. Compress it again with `gzip db.sqlite` |
| 107 | +7. Upload it to the server |
| 108 | +8. Download the modified file via the web ui |
| 109 | + |
| 110 | +And what we get is: |
| 111 | + |
| 112 | + |
| 113 | + |
| 114 | +Hurray! We are able to read arbitrary files from the server - further a "referenced" file can be deleted (if Kanboard has sufficient permissions) via the web ui. |
| 115 | +This vulnerability has been assigned CVE-2024-51747 - reported via [GHSA-78pf-vg56-5p8v](https://github.com/kanboard/kanboard/security/advisories/GHSA-78pf-vg56-5p8v). |
| 116 | + |
| 117 | +##### code `exec` |
| 118 | + |
| 119 | +File reads are nice - but we prefer to have code execution on the target. So we still decided to look deeper. |
| 120 | +When reviewing PHP code it's always a good start to check for the known dangerous php functions assembled [in this great collection](https://gist.github.com/mccabe615/b0907514d34b2de088c4996933ea1720). |
| 121 | + |
| 122 | +```bash |
| 123 | +$ ack -i "system\s*\(" --php |
| 124 | +ServiceProvider/LoggingProvider.php |
| 125 | +42: $driver = new System(); |
| 126 | +$ ack -i "shell_exec\s*\(" --php |
| 127 | +``` |
| 128 | + |
| 129 | +Unfortunately, we have no results except false positives in our source code, when we grepped for command execution functions. |
| 130 | + |
| 131 | +However, one result with a `require` statement looks interesting. |
| 132 | + |
| 133 | +```bash |
| 134 | +$ ack -i "require\s*\(" --php |
| 135 | +app/Core/Translator.php |
| 136 | +176: self::$locales = array_merge(self::$locales, require($filename)); |
| 137 | +``` |
| 138 | + |
| 139 | +`require` is a PHP language statement to include other files, which leads directly to RCE, if the file is controllable. |
| 140 | +Having a `$filename` variable - that could be controllable by us - looks interesting. |
| 141 | +Let's see where `$filename` comes from and which value it has. |
| 142 | +It is defined in [Translator.php](https://github.com/kanboard/kanboard/blob/v1.2.41/app/Core/Translator.php#L173) in the load() function. |
| 143 | + |
| 144 | +```php |
| 145 | +public static function load($language, $path = '') |
| 146 | +{ |
| 147 | + if ($path === '') { |
| 148 | + $path = self::getDefaultFolder(); |
| 149 | + } |
| 150 | + |
| 151 | + $filename = implode(DIRECTORY_SEPARATOR, array($path, $language, 'translations.php')); |
| 152 | + |
| 153 | + if (file_exists($filename)) { |
| 154 | + self::$locales = array_merge(self::$locales, require($filename)); |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +The `load()` function seems to be called from [LanguageModel.php](https://github.com/kanboard/kanboard/blob/v1.2.41/app/Model/LanguageModel.php#L214). |
| 160 | + |
| 161 | +```bash |
| 162 | +$ ack -i "load\s*\(" --php |
| 163 | +Model/LanguageModel.php |
| 164 | +214: Translator::load($this->getCurrentLanguage()); |
| 165 | +``` |
| 166 | + |
| 167 | +And it depends on `getCurrentLanguage()` from the same class. |
| 168 | + |
| 169 | +```php |
| 170 | +public function getCurrentLanguage() |
| 171 | +{ |
| 172 | + return $this->userSession->getLanguage() ?: $this->configModel->get('application_language', 'en_US'); |
| 173 | +} |
| 174 | + |
| 175 | +/** |
| 176 | + * Load translations for the current language |
| 177 | + * |
| 178 | + * @access public |
| 179 | + */ |
| 180 | +public function loadCurrentLanguage() |
| 181 | +{ |
| 182 | + Translator::load($this->getCurrentLanguage()); |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +Do you spot something fishy? Maybe not, since we haven't explained it yet - but `$this->configModel->get('application_language', 'en_US');` reads a configuration value from the SQLite `settings` table. In particular, it reads the `application_language` entry or defaults to `en_US`. |
| 187 | +Since we already inspected all further handling of the the `application_language` value we can conclude that this fields leads to a constrained RCE on the server! |
| 188 | + |
| 189 | +If we set `application_language` to an arbitrary path via path traversal, the value will be used to construct the `$filename` path, which we saw before. |
| 190 | +However, at the end of the path, the code adds a `translations.php`. Meaning, if we are able to write a `translations.php` file anywhere on the server, where Kanboard can read it and modify the SQLite database accordingly, then we achieve code execution because Kanboard includes this file. |
| 191 | + |
| 192 | +We tried to abuse this, by uploading a `translations.php` file via Kanboard's file attachments function, but the files are not saved with their original filename, but with a hash instead. This leaves us unlucky to abuse it all-at-once :S |
| 193 | + |
| 194 | + |
| 195 | +This vulnerability has been assigned CVE-2024-51748 - reported via [GHSA-jvff-x577-j95p](https://github.com/kanboard/kanboard/security/advisories/GHSA-jvff-x577-j95p). |
| 196 | + |
| 197 | + |
| 198 | +Last but not least, after reporting all vulnerabilities we noticed on retesting that we are still logged in in our testing instance after multiple days. How can this be? |
| 199 | + |
| 200 | +It turns out that the session invalidation was not working properly, thus keeping sessions alive for an indefinite time. |
| 201 | +This vulnerability has been assigned CVE-2024-55603 - reported via [GHSA-gv5c-8pxr-p484](https://github.com/kanboard/kanboard/security/advisories/GHSA-gv5c-8pxr-p484) with additional details. |
| 202 | + |
| 203 | +These findings once again show the danger of default credentials, giving initial access, which than can be used to exploit a system. |
| 204 | +Also it shows, that even configuration data cannot be trusted - user controllable input must be properly sanitized in all cases. |
| 205 | + |
| 206 | + |
| 207 | +----- |
| 208 | + |
| 209 | +Timeline: |
| 210 | + |
| 211 | +* **2024-10-31:** Vulnerability [CVE-2024-51747](https://github.com/kanboard/kanboard/security/advisories/GHSA-78pf-vg56-5p8v) and [CVE-2024-51748](https://github.com/kanboard/kanboard/security/advisories/GHSA-jvff-x577-j95p) has been reported to the vendor. |
| 212 | +* **2024-11-03:** Vendor has reported that the vulnerabilities will be fixed in next release. |
| 213 | +* **2024-11-10:** Kanboard 1.2.42 has been released with both fixes. |
| 214 | +* **2024-11-18:** Vulnerability [CVE-2024-55603](https://github.com/kanboard/kanboard/security/advisories/GHSA-gv5c-8pxr-p484) has been reported to the vendor. |
| 215 | +* **2024-12-08:** Vendor has reported that the vulnerability will be fixed in next release. |
| 216 | +* **2024-12-18:** Kanboard 1.2.43 has been released with the fix. |
| 217 | +* **2025-05-08:** This blog post was published. |
| 218 | + |
| 219 | +<style> |
| 220 | +img { |
| 221 | + border: 1px solid #555; |
| 222 | +} |
| 223 | +</style> |
0 commit comments