Continuing my previous post where I mentioned that the XStream RCE issue issue also affected SpringMVC RESTful WebServices using the XStream SpringOXM wrapper, I wanted to share a POC server. The code is quite simple and can be found in the XStreamServer GitHub Repo. It contains a WebService defined by the ContactController:

@Controller
@RequestMapping("/contacts")
public class ContactController {

    @Autowired
    private ContactRepository contactRepository;

    @RequestMapping( value = "/{id}", method = RequestMethod.GET )
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public final Contact get( @PathVariable( "id" ) final Long contactId ){
        System.out.println("get");
        return contactRepository.findOne(contactId);
    }

    @RequestMapping( method = RequestMethod.POST )
    @ResponseStatus( HttpStatus.CREATED )
    @ResponseBody
    public final String create( @RequestBody final Contact contact ){
        System.out.println("Contact name: " + contact.getFirstName());
        contactRepository.save((ContactImpl) contact);
        return "OK";
    }
}

The create method binds an incoming XML message with a Contact instance. This application is configured to use XStream as its binding library as shown here:

<!-- Marshaller configuration -->  
<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">  
    <property name="marshaller" ref="xstreamMarshaller"/>
    <property name="unmarshaller" ref="xstreamMarshaller"/>
</bean>

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">  
    <property name="aliases">
        <props>
            <prop key="contact">org.pwntester.springserver.ContactImpl</prop>
        </props>
    </property>
</bean>  

So SpringMVC will handle the XML document to the SpringOXM wrapper for unmarshalling. SpringOXM uses the XStreamMarshaller so it will simply call XStream in order to unmarshall the Contact object. At this point and with the details provided in the XStream RCE post its game over.

Use maven and jetty to start the server:

mvn -Djetty.port=8080 -DDebug clean jetty:run  

Expected use:

curl --header "content-type: application/xml" --data @contact.xml "http://localhost:8080/contacts"  

Exploit knowing the interface:

curl --header "content-type: application/xml" --data @exploit.xml "http://localhost:8080/contacts"  

Generic Exploit:

curl --header "content-type: application/xml" --data @exploit2.xml "http://localhost:8080/contacts"  

What to do about it

When I reported the issue to the Spring Security Team they updated their documentation and they added a CatchAllConverter for the users to use if they wish:

Documentation changes:

Jira ticket to create a new CatchAllConverter:

The main purpose of the catch-all converter class is to register itself as a catchall last converter with normal (or higher) priority, after converters that support specific domain classes. That way default XStream converters with lower priorities and possible security vulnerabilities do not get invoked.

They added the catch-all converter which is great but they did not register it by default so unless your XStreamMarshaller config looks the following, you will be in trouble:

<!-- Marshaller configuration -->  
<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">  
    <property name="marshaller" ref="xstreamMarshaller"/>
    <property name="unmarshaller" ref="xstreamMarshaller"/>
</bean>

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">  
    <property name="aliases">
        <props>
            <prop key="contact">org.pwntester.springserver.ContactImpl</prop>
        </props>
    </property>
    <property name="converters">
        <list>
            <bean class="org.springframework.oxm.xstream.CatchAllConverter"/>
            <bean class="org.pwntester.springserver.ContactConverter"/>
        </list>
    </property>
</bean>  

Please note that Spring documentation is wrong and the "CatchAllConverter" needs to be registered in the first place so it gets lower priority as showed in the XStreamMarshaller.setConverters code and not in the last place as suggested by the documentation:

    public void  [More ...] setConverters(ConverterMatcher[] converters) {
        for (int i = 0; i < converters.length; i++) {
            if (converters[i] instanceof Converter) {
                getXStream().registerConverter((Converter) converters[i], i);
            }
            else if (converters[i] instanceof SingleValueConverter) {
                getXStream().registerConverter((SingleValueConverter) converters[i], i);
            }
            else {
                throw new IllegalArgumentException("Invalid ConverterMatcher [" + converters[i] + "]");
            }
        }
    }

So summing up, if you are using XStream marshaller in your SpringMVC web service and havent set any Catch-All Converter, you are screwed. But it has an easy (undocumented) solution:

  • Write a custom converter for each of the classes you are expecting
  • Register a CatchAllConverter followed by your custom converters in the XStreamMarshaller configuration.

Thanks for reading!