Home CraftCMS Zero-day – SSTI + XSS triggering RCE
Post
Cancel

CraftCMS Zero-day – SSTI + XSS triggering RCE

This article was updated and now includes the removed parts of the previous post. For POC and exploit, read the second part of this disclosure.

This write up is a coordinated disclosure of CVE-2021-27902 and CVE-2021-27903 that can be chained together to gain Remote Code Execution in CraftCMS. The articles is divided into sections. You can skip and read what you feel like or just read from top to bottom.

Overview

CraftCMS allows users to upload files via its Asset field. But the storage feature known as volume within Craft CMS can be configured to point to any directory. This ability can be exploited to upload a twig template to the templates directory. By pointing a route to the uploaded malicious twig template, we get a successful Server Side Template Injection. Using filters, we can get out of the twig sandbox and get an Arbitrary Code Execution.

The exploit chain is a result of 3 vulnerabilities ie. Stored Cross Site Scripting, Server Side Template Injection and finally, Arbitrary Code Execution, and 4 bugs ie. Broken Access Control, Unrestricted File Upload, Deserialization of Untrusted Data, and Misconfiguration. The bug resulted in monetary reward and 2 CVEs, namely, CVE-2021-27902 and CVE-2021-27903. The vulnerabilities has been fixed in 3.6+ and I have taken permission from the Craft CMS team to publicly disclose them. If you are using Craft CMS, you should upgrade to the latest version of Craft CMS and also follow the security standards of Craft CMS. The exploit depends on user interaction So if you are aware of cross site scripting and follow security measures, you will be fine.

Knowing the Application

Before starting the audit, the first thing I do is understand the application. It starts with installing the application and see how it works. In a blackbox test, we often do not have admin privileges and we cannot see what is behind that authentication wall. But in greybox testing, we can simply login and see the admin area. So that is the step two. Login and see behind the walls and understand the application.

Finding the first bug – Unrestricted File Upload

As I tinkered with Craft CMS, first thing I noticed there was an Asset section which allowed you to upload files. Testing it, I started uploading PHP files as many CMS’s often have unrestricted file uploads. But no luck. Then I thought maybe I can get XSS via SVG files. Interestingly, file was uploaded. But the CMS was using enshrined/svg-sanitize a filtration library that is quite good at its job. I tried every possible payload but SVG didn’t work. Finally I decided to look at the code. After auditing where file uploads are being blocked, I found that there is src/config/GeneralConfig.php file which contains a list of allowed file extensions. I saw that it had HTML files as valid file uploads! And here I was trynna pop an alert from SVG files. I uploaded an HTML file, see its link and it worked! The bug here was Unrestricted File Upload that resulted in a Cross Site Scripting vulnerability.

Turning Unrestricted File Upload into Vulnerability

But this can be categorized as self XSS because only privileged users are capable of uploading a file. Remember that Craft is a CMS and its job is to allow people to make a website. So there must be a way to allow file uploads from the frontend. I started searching for ways on the google to upload a file from unauthenticated pages. Sooner, I found that there is a functionality to create content, that is entries. With more research on how it works, I found that Craft CMS uses a creative concept of volumes that allows users to upload files to different directories. I configured the volume and started testing uploads. I created upload fields for the entry and tried using it. I was able to create entries and upload HTML files but it was still using the admin account. So I searched more and more and found this plugin called Guest Entries. I installed it and after reading its documentation, I created the following form to upload the files from the frontend.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{% macro errorList(errors) %}
    {% if errors %}
        <ul class="errors">
            {% for error in errors %}
                <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}
{% endmacro %}

{% from _self import errorList %}

<form method="post" accept-charset="UTF-8" enctype="multipart/form-data">
    {{ csrfInput() }}
    <input type="hidden" name="action" value="guest-entries/save">
    <input type="hidden" name="sectionId" value="2">
    <input type="hidden" name="enabled" value="1">
    {{ redirectInput('{uri}') }}

    <label for="title">Title</label>
    <input id="title" type="text" name="title"
        {%- if entry is defined %} value="{{ entry.title }}"{% endif -%}>

    {% if entry is defined %}
        {{ errorList(entry.getErrors('title')) }}
    {% endif %}

    <input type="file" name="fields[asset]">
    <input type="submit" value="Publish">
</form>

and configuring the volume and entries a little, I was able to upload the file from the frontend! But how would an attacker know where the file went?

To supplement the bug, I had to create an entries page that showed the link to uploaded files. Note that this is quite common for theme developers to create such forms and show the images/files uploaded from frontend:

1
2
3
4
5
6
7
<h1>{{entry.title}}</h1>

{% set rel = entry.asset.one() %}
{% if rel %}
    <p><a href="{{ rel.url }}">{{ rel.filename }}</a></p>
{% endif %}

By configuring both templates correctly, I was able to upload XSS and get its URL. Payday! But I didn’t bothered to report it.

Finding the second bug – Broken Access Control

Even after finding the stored XSS, I was still playing around with the CMS as it was quite interesting. While checking the settings, I came across a nice feature called aliases. I was interested in what these aliases can do, I kept on learning about them. From the source code audit, I came to know that this is actually a part of Yii2 framework. But what can I do with it? I tried finding the inputs where I could put them and sooner I found how dumb I was. I have been using these aliases this whole time in the volume configurations. I tried to go one step back with .. to point the volume outside the public directory and tried uploading a file, it worked! This broken access control bug can allow me to write a file anywhere I want!

Finding the third bug – Deserialization of Unstrusted Data

I was already hyped after finding this. I rushed to see the directory structure and tried uploading files everywhere I can. The end goal as always was RCE. So I uploaded files in vendor directories, tried overwriting the composer.json but I could not upload any PHP file. So I was stuck. Then I tried to upload twig templates within the twig directory but still, no luck. After getting tired of goofing around with uploading files, it came to my mind that I could also point a route to it. So I uploaded an SVG file and pointed a route to it. No surprise it worked.

Turning Broken Access Control and Deserialization of Untrusted Data into Server Side Template Injection

So I was able to upload files in templates directory and load it on the server as well. Obviously I ended up putting the famous {{7 * 7}} in the SVG and there it was! An SSTI using a chain of two bugs ie. the aliases shouldn’t have allowed uploading to templates directory and twig shouldn’t be rendering content from non twig templates. Note that both bugs can only be performed by an admin and no guest user would be able to do it.

Finding the fourth bug – Misconfiguration

After finding the SSTI, we are still within the sandbox of Twig so this is quite a locked up situation where you can only perform simple text manipulations. I tried every possible payload available for escaping the sandbox but no luck. Then I asked my team to help me and our best in the wild zeroday ninja gave the payload {{ ['id']|filter('system') }} which I was trying again and again with no success. But when I copy pasted it just worked. Maybe I was doing a typo. None the less, it worked and now was the time to write the PoC!

Connecting the dots and completing the PoC

With the misconfigured twig engine that didn’t implemented the sandbox configuration, I was able to remote code execution. Since I already had an XSS, I could simply let the JavaScript do all the manual work on behalf of the admin. So I wrote an exploit in JavaScript that automates all the stuff and uploads a shell when the XSS triggers. All these vulnerabilities have been fixed. Long live the Open Source!

PoC || GTFO

For POC and exploit, read the second part of this disclosure.

This post is licensed under CC BY 4.0 by the author.