The file-share service implements a webservice where users can upload files and share them with other users and also “convert” files.
Both back- and frontend are written in C# using the ASP.NET framework, with Blazor for the frontend, with the exception of the “convert” functionality, which is implemented as a shell-script using imagemagick in a separate docker container.
The API
The relevant API consists of the following endpoints:
/api/fileshare/convert/<filename> (POST): Upload and convert file <filename>
/api/fileshare/shared (GET), with parameter sharedUser: List files shared with me by user sharedUser
/api/fileshare/shared/<filename> (GET), with parameter sharedUser: Download file <filename> shared with me by user sharedUser
/api/users (GET): List recent (max 10k) users
/api/share (GET): List users I share with
/api/share (POST): Share with users specified in json body
/api/share/withme (GET): List users that share with me
User accounts are managed by ASP.NET’s IdentityUser and identified using UIDs and a user’s files are stored under /app/files/<uid>.
The Flagstore
Flags were stored by the gameserver as svg-files named supersecret.svg, which included the flag in multiple places.
Bug 1: Path Traversal
The download functionality is implemented like this:
As shown, the filepath is built from three components, path (globally set to /app), the user-id, and the requested fileName taken from the URL.
However, the {*fileName} allows us to provide an absolute path as the “file name”, and Path.Combine will happily overwrite the previous parts of the path:
This method is intended to concatenate individual strings into a single string that represents a file path. However, if an argument other than the first contains a rooted path, any previous path components are ignored, and the returned string begins with that rooted path component
This allows us to download any file we wish by specifying its full path as the filename.
Exploit
Since we can obtain other user’s UIDs (from the /api/users endpoint) and since the gameserver placed flags always in a file named supersecret.svg, we can request the file /app/files/{uid}/supersecret.svg and get the flag that way.
Getting this exploit to work during the CTF took us longer than expected, mostly because the service uses OpenID connect and after messing about with a number of python libraries we settled on doing everything manually using only requests (full exploit given at the end).
And since we opted for an exploit-first strategy, we even lost some flags to this rather simple bug.
The same bug also exists when downloading shared files.
Hoping that some teams might have fixed the issue only in the first place, we also built an exploit using the shared file download.
Exploit
This time, we need to register two users and have one share files with the other.
Otherwise, nothingh changes:
Bug 2: Caching is hard
Full disclosure: We found this bug only in preparation for the write-up, but it explains why we still lost some flags after deploying the fix for the first bug.
When downloading shared files, a custom authorization attribute (CustomFileShareAuthorizationAttribute) is used to check the request.
The core of this check is the following:
In essence, this checks if the user from the sharedUser parameter exists and whether the current user is in its SharedWithUser list.
The result of this check is cached, however only the current user is used as the cache key.
As long as there is a cache entry for the custom user, we will thus always get the same authorization result, irrespective of the sharedUser parameter.
Exploit
To exploit this, we simply need to ensure that we can get a cache-entry with true for our user.
We can achieve this by simply creating two users, allowing sharing between them, and then access a shared file.
Afterwards, we can access everyones files, since the cache will always return true for our user.
Note that we do not even need to upload an actual file to share, since the authorization code is run before everything else.
Another nice side-effect of this exploit is that it allows us the list other user’s files using the /api/fileshare/shared endpoint, which allows us to download files without guessing filenames.
Fix
Using a (currentUser, sharedUserId)-tuple as the cache-key might have worked during the CTF, but really the cache should then be invalidated when a user changes their sharing-settings.
The best “fix” here is therefore probably to just remove the cache altogether.
Bug 3: Ghostscript Command Execution
Disclosure again: also this bug was not found during the CTF, but we discovered the exploit later in our traffic dumps.
Users can “convert” files by uploading them to the special /api/fileshare/convert endpoint.
Behind the scenes, this places the file in a special folder /app/files/convert/<uid>/ on which another Docker container is listening with inotify-wait.
Upon notification, the bash script running in this second container then basically runs
While this seems rather innocuous, for some filetype (postscript!), imagemagick’s convert utility will call out to ghostscrit.
It also turns out that the container uses an older version of ghostscript (9.26), which is vulnerable to CVE-2019-6116, which can be used to achieve code execution.
In particular, this vulnerability allows postscript files to access an internal .forceput function, which can be used to overwrite parts of ghostscript’s sandbox settings.
This allows it to access privileged files and devices, such as the %pipe%command pseudo device, which is a ghostscript extension to the postscript standard and opens a pipe to a new process running the command command.
With the ability to run arbitrary commands inside a container that has full access to all files, it is then rather trivial to obtain all flags.
Exploit
The bulk of the exploit is written in postscript.
Rather than copy the exploit we saw on the wire 1:1, we tried our best to paraphrase and comment it (Fortunately, the original exploit was nice enough to have comments like <- put your backconnect IP here…).
Super-quick postscript 101:
postscript is executed in a stack-based VM, 42 places the number 42 on the stack, (Hello) places the string Hello on the stack, exch calls the exchange function, which swaps the two topmost stack elements.
/foobar places the name foobar on the stack, { cmd1 cmd 2 } pushes a codeblock with two commands on the stack, def calls the define function, which takes two arguments from the stack, a name and a codeblock and defines a new command with that name which will then execute the given code.
The python-part of the exploit is then only needed to create a file-share account and upload the newly created file.
Fix
The obvious fix here is to update ghostscript :-)
But why was the old version running there in the first place?
A closer look at the Dockerfile for the relevant container reveals the answer:
The sneaky line here is the first one, since imagick/imagemagick is not an “official” imagemagick Dockerimage (there appears to be none), but was rather uploaded by community user imagick, who joined dockerhub only beginning of May…
Final thoughts, and full exploit code
One of the nice things about CTFs is that you always learn new things.
In this case:
OpenID connect can be implemented in pure python requests, but there might be better ways.
If code looks suspicious (like the cache in the CustomFileShareAuthorizationAttribute), it almost definitely is.
An excrutiating amount of detail about postscript.