Impact
This is a potential moderate impact, low complexity privilege escalation vulnerability in Craft with certain user permissions setups.
Patches
This has been fixed in Craft 4.4.16 and Craft 3.9.6. Users should ensure they are running at least those versions.
References
https://github.com/craftcms/cms/pull/13932 https://github.com/craftcms/cms/pull/13931 https://github.com/craftcms/cms/blob/develop/CHANGELOG.md#4511---2023-11-16 https://github.com/craftcms/cms/blob/v3/CHANGELOG.md#396---2023-11-16
Summary XSS can be triggered by review volumes
PoC
1. Access setting tab 2. Create new assets 3. In assets name inject payload: "<script>alert(1337)</script> 4. Click Utilities tab 5. Choose all volumes, or volume trigger xss 6. Click Update asset indexes. 7. Wait to assets update success. 8. Progress complete. 9. Click on review button will trigger XSS
Root cause Function: index.php?p=admin/actions/asset-indexes/process-indexing-session&v=1680710595770 After loading completed, progess will load: "skippedEntries" and "missingEntries" These parameters is not yet filtered, I just tried "skippedEntries" but I think it will be work with "missingEntries"
My reponse: { "session": { "id": 10, "indexedVolumes": { "6": "\"<script>alert(1337)</script>" }, "totalEntries": 2235, "processedEntries": 2235, "cacheRemoteImages": true, "listEmptyFolders": false, "isCli": false, "actionRequired": true, "dateCreated": "Apr 5, 2023, 9:03:16 AM", "skippedEntries": [ "\"<script>alert(1337)</script>/assetpreviews/Image.php", "\"<script>alert(1337)</script>/assetpreviews/Pdf.php" ], "missingEntries": { "folders": [], "files": [] }, "processIfRootEmpty": false }, "skipDialog": false }
Resolved in https://github.com/craftcms/cms/commit/053d7119697e480ff81c5723bb9a33eaa49e0fc7
Summary Bypassing the validatePath function can lead to potential Remote Code Execution (Post-authentication, ALLOWADMINCHANGES=true)
Details
In bootstrap.php, the SystemPaths path is set as below. php // Set the vendor path. By default assume that it's 4 levels up from here $vendorPath = $findConfigPath('--vendorPath', 'CRAFTVENDORPATH') ?? dirname(DIR, 3);
// Set the "project root" path that contains config/, storage/, etc. By default assume that it's up a level from vendor/. $rootPath = $findConfigPath('--basePath', 'CRAFTBASEPATH') ?? dirname($vendorPath);
// By default the remaining directories will be in the base directory $dotenvPath = $findConfigPath('--dotenvPath', 'CRAFTDOTENVPATH') ?? "$rootPath/.env"; $configPath = $findConfigPath('--configPath', 'CRAFTCONFIGPATH') ?? "$rootPath/config"; $contentMigrationsPath = $findConfigPath('--contentMigrationsPath', 'CRAFTCONTENTMIGRATIONSPATH') ?? "$rootPath/migrations"; $storagePath = $findConfigPath('--storagePath', 'CRAFTSTORAGEPATH') ?? "$rootPath/storage"; $templatesPath = $findConfigPath('--templatesPath', 'CRAFTTEMPLATESPATH') ?? "$rootPath/templates"; $translationsPath = $findConfigPath('--translationsPath', 'CRAFTTRANSLATIONSPATH') ?? "$rootPath/translations"; $testsPath = $findConfigPath('--testsPath', 'CRAFTTESTSPATH') ?? "$rootPath/tests";
Because paths are validated based on the /path1/path2 format, this can be bypassed using a file URI scheme such as file:///path1/path2. File scheme is supported in mkdir() php / @param string $attribute @param array|null $params @param InlineValidator $validator @return void @since 4.4.6 / public function validatePath(string $attribute, ?array $params, InlineValidator $validator): void { // Make sure it’s not within any of the system directories $path = FileHelper::absolutePath($this->getRootPath(), '/');
$systemDirs = Craft::$app->getPath()->getSystemPaths();
foreach ($systemDirs as $dir) { $dir = FileHelper::absolutePath($dir, '/'); if (strstartswith("$path/", "$dir/")) { $validator->addError($this, $attribute, Craft::t('app', 'Local volumes cannot be located within system directories.')); break; } } }
ref. https://www.php.net/manual/en/wrappers.file.php
PoC 1) Create a new filesystem. Base Path: file:///var/www/html/templates
!1
2) Create a new asset volume. Asset Filesystem: localbypass
!2
3) Upload a ttml file with rce template code. Confirm poc.ttml file created in /var/www/html/templates twig {{'<pre>'}} {{13371337}} {{['cat /etc/passwd']|map('passthru')|join}} {{['id;pwd;ls -altr /']|map('passthru')|join}} !3 !4
4) Create a new route. URI: , Template: poc.ttml
!5
5) Confirm RCE on arbitrary path ( / )
!6
PoC Env
!0628 env
Impact Take control of vulnerable systems, Data exfiltrations, Malware execution, Pivoting, etc.
although the vulnerability is exploitable only in the authenticated users, configuration with ALLOWADMINCHANGES=true, there is still a potential security threat (Remote Code Execution)
Summary Unrestricted file extension lead to a potential Remote Code Execution (Authenticated, ALLOWADMINCHANGES=true)
Details Vulnerability Cause : If the name parameter value is not empty string('') in the View.php's doesTemplateExist() -> resolveTemplate() -> resolveTemplateInternal() -> resolveTemplate() function, it returns directly without extension verification, so that arbitrary extension files are rendered as twig templates (even if they are not extensions set in defaultTemplateExtensions = ['html', 'twig']) php / Searches for a template files, and returns the first match if there is one. @param string $basePath The base path to be looking in. @param string $name The name of the template to be looking for. @param bool $publicOnly Whether to only look for public templates (template paths that don’t start with the private template trigger). @return string|null The matching file path, or null. / private function resolveTemplate(string $basePath, string $name, bool $publicOnly): ?string { // Normalize the path and name $basePath = FileHelper::normalizePath($basePath); $name = trim(FileHelper::normalizePath($name), '/');
// $name could be an empty string (e.g. to load the homepage template) if ($name !== '') { if ($publicOnly && pregmatch(sprintf('/(^|\/)%s/', pregquote($this->privateTemplateTrigger, '/')), $name)) { return null; }
// Maybe $name is already the full file path $testPath = $basePath . DIRECTORYSEPARATOR . $name;
if (isfile($testPath)) { return $testPath; }
foreach ($this->defaultTemplateExtensions as $extension) { $testPath = $basePath . DIRECTORYSEPARATOR . $name . '.' . $extension;
if (isfile($testPath)) { return $testPath; } } }
foreach ($this->indexTemplateFilenames as $filename) { foreach ($this->defaultTemplateExtensions as $extension) { $testPath = $basePath . ($name !== '' ? DIRECTORYSEPARATOR . $name : '') . DIRECTORYSEPARATOR . $filename . '.' . $extension;
if (isfile($testPath)) { return $testPath; } } }
return null; }
When attacker with admin privileges on the DEV or Misconfigured STG, PROD, they can exploit this vulnerability to remote code execution (ALLOWADMINCHANGES=true)
PoC Step 1) Create a new filesystem. Base Path: /var/www/html/templates !1
Step 2) Create a new asset volume. Asset Filesystem: template !2
Step 3) Upload poc file( .txt , .js , .json , etc ) with twig template rce payload twig {{'<pre>'}} {{13371337}} {{['cat /etc/passwd']|map('passthru')|join}} {{['id;pwd;ls -altr /']|map('passthru')|join}} !7 !5
Step 4) Create a new global set with template layout. The template filename is poc.js !8
Step 5) When access global menu or /admin/global/test, poc.js is rendered as a template file and RCE confirmed !9
Step 6) RCE can be confirmed on other menus(Entries, Categories) where the template file is loaded. !10 !11
Poc Environment) ALLOWADMINCHANGES=true, defaultTemplateExtensions=['html','twig'] !0 !13 !14
Impact Take control of vulnerable systems, Data exfiltrations, Malware execution, Pivoting, etc.
Additionally, there are 371 domains using CraftCMS exposed on Shodan, and among them, 33 servers have "stage" or "dev" included in their hostnames.
although the vulnerability is exploitable only in the authenticated users, configuration with ALLOWADMINCHANGES=true, there is still a potential security threat (Remote Code Execution)
!2023-03-31 10 29 53
Remediation Recommend taking measures by referring to https://github.com/craftcms/cms-ghsa-9f84-5wpf-3vcf/pull/1 php // Maybe $name is already the full file path $testPath = $basePath . DIRECTORYSEPARATOR . $name;
if (isfile($testPath)) { // Remedation: Verify template file extension, before return $fileExt = pathinfo($testPath, PATHINFOEXTENSION); $isDisallowed = false;
if (isset($fileExt)) { $isDisallowed = !inarray($fileExt, $this->defaultTemplateExtensions);
if($isDisallowed) { return null; } else { return $testPath; } } }
!remediation
A malformed title in the feed widget of craftcms/cms can deliver an XSS payload. This has been resolved in this commit.