CVE-2019-1306: Are you my Index?October 24, 2019 | Guest Blogger
In September, Microsoft released patches to address a remote code execution (RCE) vulnerability in Azure DevOps (ADO) and Team Foundation Server (TFS). In this Critical-rated vulnerability, an attacker would need to upload a specially crafted file to a vulnerable ADO or TFS server repo and wait for the system to index the file. Doing so would result in code execution on the target system. This bug was reported to the ZDI program by Mikhail Shcherbakov. He has graciously provided the following write-up on the details of CVE-2019-1306.
The BinaryFormatter is known as a popular binary serializer in the .NET platform. It’s also known as the deserializer with an insecure configuration by default. Back in 2012, James Forshaw described the first gadgets for BinaryFormatter in the landmark presentation “Are you my Type? Breaking .NET Through Serialization (PDF)” at Black Hat. After that, .NET developers began to chant the mantra “Don’t use BinaryFormatter for deserializing untrusted data,” at least by the default configuration. However, it is challenging to distinguish untrusted and trusted data in complex modern systems. A good example of this is the recently patched CVE-2019-1306, which affects the Microsoft Azure DevOps Server.
The Microsoft Azure DevOps Server is the good old re-branded Team Foundation Server (TFS), re-branded, i.e., CI/CD/Source Control/Issue Tracker/Wiki system. A list of features shows we are dealing with a complex architecture that includes many internal data formats. That makes it a great target to find insecure deserialization vulnerabilities. Because the Azure DevOps Server has a self-hosted edition, we can use static and dynamic approaches to analyze the delivered binaries. Most of the application is written in .NET, so I developed tooling for Data Flow and Control Flow analysis of Common Intermediate Language (CIL). You can check the prototype of this tool called DeReviewer. It supports DSL-like syntax to describe vulnerable patterns and payloads, it tests payloads automatically, and builds call graphs to reach potentially vulnerable methods. I ran DeReviewer against the Microsoft Azure DevOps Server and got an interesting call graph showing the usage of
We can zoom in on this graph and see that the calls from the assembly
Microsoft.VisualStudio.Services.Search.Server.Jobs.dll contain the method
DeserializeToObject. If we decompile this method, we see that this uses BinaryFormatter in an insecure way.
That looks like possible remote code execution if the attacker could pass an arbitrary binary array to the parameter
arrayBytes. However, the code of
Microsoft.VisualStudio.Services.Search.Server.Jobs.dll is used by the background service TFSJobAgent, which builds and handles internal indexes. The TFSJobAgent uses an insecure implementation of
DeserializeToObject for deserializing its own indexes only. The index data seems really trusted. Let’s take a more detailed look at the design of TFSJobAgent.
DeserializeToObject gets called when the service TFSJobAgent loads an index of a Wiki page of the Azure DevOps organization account. There are several methods used by the TFSJobAgent service to create and update the Wiki index. One way involves a crawler that monitors a Git repository and updates indexes when new changes have been pushed to the server. The Azure DevOps account needs to be set up for storing Wiki pages in Git for the usage of this scenario. A user with minimal privileges on the system can set it up just by clicking “Publish code as wiki” in the Web interface. In this case, the TFSJobAgent service runs the crawler, which parses new and updated Wiki content. It will then serialize the results to index files. Everything looks secure for now.
Microsoft.VisualStudio.Services.Search.Parser.WikiParserExecutor converts the binary content from Git to a string and parses it to Markdown format using Markdig library. The code that handles this is:
When I saw this piece of code, I could not believe my eyes. This method instantiates the class
ParsedData with binary unparsed Content if the Markdig throws an exception during the parsing content of the Wiki page. After that, Content field is stored in the index. The following code expects that a data of the internal index is validated and uses the method
DeserializeToObject to reconstruct the
ParsedMarkDownData object. So, we need to find some invalid Markdown text to get some exception, combinate it with a payload, store to file, and push it to Git as a Wiki page. Sounds like a good plan for RCE exploitation. The Azure DevOps Server does not use the latest version of Markdig library, and this fact simplifies the search for some invalid Markdown text. I downloaded the code of the Markdig library from GitHub and took a look at Unit Tests firstly. The test
Markdig.Tests.MiscTests::TestInvalidCodeEscape is reproduced for the required version of Markdig, and the parser throws the exception. The attacker can use the Markdown string
```**Header**\t from this test to bypass parsing the content. I got this successful result in 10 minutes. I love Open Source and GitHub!
The next step is a generation of RCE payload for BinaryFormatter. James Forshaw described the TypeConfuseDelegate gadget for BinaryFormatter in 2017, see his post on the Project Zero Team blog for more details. The code of the payload generator looks like:
Now we combine the payload with the incorrect Markdown string:
Store the result to a file, commit the file to Git repository, push it to the Azure DevOps Server and… nothing happens! The TFSJobAgent doesn’t run any cmd command as we expect. I debugged this case and explored the code in more detail and found that the crawler validates all files from Git and creates indexes only for text files. It is reasonable, but it seems this is the end for us. However, the code surprised me again. The crawler uses the method
FileTypeUtil::DetermineEncoding to “guess” the Unicode encoding of the content by the first bytes. It replaces the content to dummy bytes for binary content. This happened when I pushed my payload before.
Let’s study a header of BinaryFormatter serialized data. The specification from Microsoft describes the format. The first byte in BinaryFormatter serialized stream must be 0, the next 4 bytes are RootId and equal 1 of integer type by default. The
FileTypeUtil::DetermineEncoding method has a single allowable header, which starts at 0. It is
0x0000FEFF. Thus, we change RootId in the header and the body of serialized data to
0x00FEFF00, and it should work. The following code makes it before adding the incorrect Markdown string:
Store the result in a file and push it to the Azure DevOps Server again. I recorded what was happening in the demo. We got RCE by having access to the Git repository only.
Microsoft has fixed CVE-2019-1306, and you need to apply a security patch for your Azure DevOps Server. Note that the Windows Automatic Updates don’t include this patch, and you need to install it manually. The developers added a custom SerializationBinder in BinaryFormatter configuration that allows deserializing only known types. It is one of the best practices for the deserialization of untrusted data. You should use the same approach for insecure serializers, even if you think that you are working with trusted data. As you can see, it is complicated to determine in modern complex systems. The internal index looks like trusted, parsed and validated data, but the attacker may compromise it.
You can find me on Twitter at @yu5k3. Follow me if you are interested in .NET security and usage of modern static analysis methods in the security area. I am going to periodically publish the results of my research at the KTH Royal Institute of Technology.
Thanks again to Mikhail for providing this great write-up. This was his first bug submission to the ZDI program, and we certainly hope to see more submissions from him in the future. Until then, follow the team for the latest in exploit techniques and security patches.