RCE in Citrix ShareFile Storage Zones Controller (CVE-2021-22941) – A Walk-Through
Citrix ShareFile Storage Zones Controller uses a fork of the third-party library NeatUpload. Versions before 5.11.20 are affected by a relative path traversal vulnerability (CTX328123/CVE-2021-22941) when processing upload requests. This can be exploited by unauthenticated users to gain Remote Code Execution.
Come and join us on a walk-through of finding and exploiting this vulnerability.
BACKGROUND
In April, Citrix published an advisory that addresses three vulnerabilities in ShareFile Storage Zones Controller (from here on just "ShareFile"). In contrast to a previous patch in the same product, there were no lightweight patches available, which could have been analyzed quickly. Instead, only full installation packages were available. So, we downloaded StorageCenter_5.11.18.msi to have a look at it.THE TRAVELOGUE
A first glance at the files contained in the .msi file revealed the third-party library NeatUpload.dll. We knew that the latest version contains a Padding Oracle vulnerability, and since the NeatUpload.dll file had the same .NET file version number as ShareFile (i. e., 5.11.18), chances were that somebody had reported that very vulnerability to Citrix.After installation of version 5.11.18 of ShareFile, attaching to the w3wp.exe process with dnSpy, and opening the NeatUpload.dll, we noticed that the handler class Brettle.Web.NeatUpload.UploadStateStoreHandler was missing. So, it must have either been removed by Citrix or they used an older version. Judging by the other classes in the library, the version used by ShareFile appeared to share similarities with NeatUpload 1.2 available on GitHub.
So, not a quick win, after all? As we did not find a previous version of ShareFile such as 5.11.17, that we could use to diff against 5.11.18, we decided to give it a try to look for something in 5.11.18.
Finding A Path From Sink To Source
Since NeatUpload is a file upload handling library, our first attempts were focused on analyzing its file handling. Here FileStream was a good candidate to start with. By analyzing where that class got instantiated, the first result already pointed directly to a method in NeatUpload, the Brettle.Web.NeatUpload.UploadContext.WritePersistFile() method. Here a file gets written with something that appears to be some kind of metric of an upload request:




Content-Disposition: form-data; name="text4"; filename="text5"
As for text6, the FieldNameTranslator.FileFieldNameToPostBackID(string) method call either returns the value of the FieldNameTranslator.PostBackID field if present:

So, let's summarize our knowledge of the HTTP request requirements so far:
POST /default.aspx?foo HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary="boundary"
Content-Length: 94
--boundary
Content-Disposition: form-data; name="text4"; filename="text5"
--boundary--
The request path and query string are not yet known, so we'll simply use dummies. This works because HTTP modules are not bound to paths like HTTP handlers are.Important Checkpoints Along The Route
Let's set some breakpoints at some critical points and ensure they get reached and behave as assumed:- UploadHttpModule.Application_BeginRequest() – to ensure the HTTP module is actually active (the BeginRequest event handler is the first in the chain of raised events)
- FieldNameTranslator..ctor() – to ensure the FieldNameTranslator.PostBackID field gets set with our value.
- FilteringWorkerRequest.ParseOrThrow() – to ensure the multipart parsing works as expected.
- UploadContext.set_PostBackID(string) – to ensure the UploadContext.postBackID field is set with our value.
- UploadContext.WritePersistFile() – to ensure the file path and content contain our value

Let's change the default.aspx to upload.aspx and send the request again. This time the breakpoint at the constructor of FieldNameTranslator should be hit. Here we can see that the PostBackID field value is taken from a query string parameter named id or upload id (which is actually configured in the web.config file).
After sending a new request with the query string id=foo, our next breakpoint at FilteringWorkerRequest.ParseOrThrow() should be hit. After stepping through that method, you'll notice that some additional parameters bp and account id is expected:

Let's add them with bogus values and try it again. This time the breakpoint at UploadContext.WritePersistFile() should get hit where the FileStream gets created:

So, now we have reached the FileStream constructor but the UploadContext.PostBackID field value is null as it hasn't been set yet.
Are We Still On Track?
You may have noticed that the breakpoint at UploadContext.set_PostBackID(string) also hasn't been hit yet. This is because of the while loop in FilteringWorkerRequest.ParseOrThrow() uses the result of FilteringWorkerRequest.CopyUntilBoundary(string, string, string) as a condition but it returns false on its first call so the while block never gets executed.When looking at the code of CopyUntilBoundary(string, string, string) (not depicted here), it appears that it fills some buffer with the posted data and returns false if _doneReading is true. The byte array tmpBuffer has a size of 4096 bytes, which are minimalistic example request certainly does not exceed.
After sending a multipart part that is larger than 4096 bytes the breakpoint at the FileStream should get hit twice, once with a null value originating from within the while condition's FilteringWorkerRequest.CopyUntilBoundary(string, string, string) call and once with foo originating from within the while block:

Stepping into the FileStream the constructor also shows the resulting path, which is C:\inetpub\wwwroot\Citrix\StorageCenter\context\foo. Although context does not exist, we're already within the document root directory that the w3wp.exe process user has full control of:

Let's prove this by writing a file to it using id=../foo:

We have reached our destination, we can write into the webroot directory!
WHAT'S IN THE BACKPACK?
Now that we're able to write files, how can we exploit this? We have to keep in mind that the id/uploadid parameter is used for both the file path and the content.That means the restriction is that we can only use characters that are valid in Windows file system paths. According to the naming conventions of files and paths, the following characters are not allowed:
- Characters in the range of 0–31 (0x00–0x1F)
- < (less than)
- > (greater than)
- : (colon)
- " (double quote)
- | (vertical bar or pipe)
- ? (question mark)
- * (asterisk)
So, is that the end of this journey? At best a denial of service when overwriting existing files? Have we already tried hard enough?
Running With Razor
@("Hello, World!")And ShareFile does not just use Web Forms but also MVC:

Note that we can't just add new views as their rendering is driven by the corresponding controller. But we can overwrite an existing view file like the ConfigService\Views\Shared\Error.cshtml, which is accessible via /ConfigService/Home/Error:
What is still missing now is the writing of the actual payload using Razor syntax. We won't show this here, but here is a hint: unlike Unix-based systems, Windows doesn't require each segment in a file path to exist as it gets resolved symbolically. That means, we could use additional "directories" to contain the payload as long as we "step out" of them so that the resolved path still points to the right file.

TIMELINE AND FIX
Code White reported the vulnerability to Citrix on May 14th. On August 25th, Citrix released the ShareFile Storage Zones Controller 5.11.20 to address this vulnerability by validating the passed value before assigning FieldNameTranslator.PostBackID
Post a Comment