Skip to content

RCE via Arbitrary File Write

High
SebastianStehle published GHSA-phqq-8g7v-3pg5 Nov 7, 2023

Package

No package listed

Affected versions

7.8.2

Patched versions

7.9.0

Description

Summary

An arbitrary file write vulnerability in the backup restore feature allows an authenticated attacker to gain remote code execution (RCE).

Details

Squidex allows users with the squidex.admin.restore permission to create and restore backups. Part of these backups are the assets uploaded to an App. For each asset, the backup zip archive contains a .asset file with the actual content of the asset as well as a related AssetCreatedEventV2 event, which is stored in a JSON file:

$ zipinfo backup.zip
...
-rw-r--r--  3.0 unx       43 tx stor 23-Oct-09 12:04 attachments/176/46c05041-9588-4179-b5eb-ddfcd9463e1e_0.asset
-rw-r--r--  3.0 unx     1184 tx defN 23-Oct-09 12:04 events/0/4.json
...


Amongst other things, the JSON file contains the event type (AssetCreatedEventV2), the ID of the asset (46c05041-9588-4179-b5eb-ddfcd9463e1e), its filename (test.txt), and its file version (0):

$ cat events/0/4.json
{"n":{"t":"AssetCreatedEventV2","s":"asset-33e55647-7db1-4a20-b27b-0f690753a5d5--46c05041-9588-4179-b5eb-ddfcd9463e1e","p":"{\u0022parentId\u0022:\u002200000000-0000-0000-0000-000000000000\u0022,\u0022fileName\u0022:\u0022test.txt\u0022,\u0022fileHash\u0022:\u0022tguDdazklWWU13e5N/3kflTT9vxjvjy4gyxByFts5V8=\u0022,\u0022mimeType\u0022:\u0022text/plain\u0022,\u0022slug\u0022:\u0022test.txt\u0022,\u0022fileVersion\u0022:0,\u0022fileSize\u0022:43,\u0022type\u0022:\u0022Unknown\u0022,\u0022metadata\u0022:{},...}

When a backup with this event is restored, the BackupAssets.ReadAssetAsync method is responsible for re-creating the asset. For this purpose, it determines the name of the .asset file in the zip archive, reads its content, and stores the content in the filestore (by default FolderAssetStore):

    private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader,
        CancellationToken ct)
    {
        try
        {
     // (1) get name of .asset file in zip archive
            var fileName = GetName(assetId, fileVersion);

     // (2) read content of .asset file
            await using (var stream = await(fileName, ct))
            {
       // (3) store content in filestore
                await assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream, true, ct);
            }
        }
        catch (FileNotFoundException)
        {
            return;
        }
    }

The GetName method constructs the name of .asset file by using the assetId and fileVersion:

    private static string GetName(DomainId assetId, long fileVersion)
    {
        return $"{assetId}_{fileVersion}.asset";
    }

Please notice that this filename is only used to retrieve the .asset file from the zip archive. When the asset is stored in the filestore via the UploadAsync method, the assetId and fileVersion are passed as arguments. These are further passed to the method GetFileName, which determines the filename where the asset should be stored:

    public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true,
        CancellationToken ct = default)
    {
        var fileName = GetFileName(appId, id, fileVersion, suffix);
        return assetStore.UploadAsync(fileName, stream, overwrite, ct);
    }

The GetFileName is slightly different from the GetName method. Again, the assetId (parameter id) is used, but the fileVersion is only appended, if it is equal or greater than zero. Also, there is no suffix by default (no .asset extension):

    private string GetFileName(DomainId appId, DomainId id, long fileVersion = -1, string? suffix = null)
    {
        var sb = new StringBuilder(20);
        // ...
        sb.Append(id);
        if (fileVersion >= 0)
        {
            sb.Append('_');
            sb.Append(fileVersion);
        }
       // ...
        return sb.ToString();
    }

In both cases, for the retrieval (GetName) and the storage (GetFileName) of an asset file, the assetId is inserted into the filename without any sanitization.

Impact

The vulnerability allows an attacker with squidex.admin.restore privileges to run arbitrary operating system commands on the underlying server (RCE).

An unauthenticated attacker can combine this vulnerability with an XSS vulnerability to trigger the vulnerability in the context of a user with the required privileges and restore a malicious backup to take over the Squidex instance.

Severity

High

CVE ID

CVE-2023-46253

Weaknesses