同事问我研究过Java下的xxe漏洞嘛,为啥修复建议不起作用,emmmm然后这个问题我就回答不上来了,这个问题有两个关注点:
1.java下xxe产生的原理是啥。
2.修复建议的修复代码是啥。
测试代码:
package com.l1nk3r.xxe.javaxxe; import org.w3c.dom.Document; import javax.xml.parsers.*; import java.io.ByteArrayInputStream; import java.io.InputStream; public class DocumentXXE { public static void main(String[] args) throws Exception { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); Document doc = db.parse(is); } }
在 db.parse
处下个断点,代码来到这个 Java.xml.parsers.DocumentBuilder#parse 类中,跟进 return parse
。
在 DocumentBuilderImpl#parse 中,调用了 DOMParser#parse
跟进 DOMParser#parse 这个方法,调用 parse 方法来解析 xmlInputSource 。
跟进 XMLParser#parse ,调用 fConfiguration.parse 方法。
而这个 fConfiguration.parse 实际上是个继承接口,这里根据 debug 会进入 XML11Configuration#parse
中。
通过 setInputSource(source)
方法将 Xml 数据赋值给 fInputSource ,然后调用 parse(boolean complete)
构造方法进行处理。
跟进 parse(boolean complete)
方法,然后就来到了 XMLDocumentFragmentScannerImpl#scanDocument
方法中。
跟进 XMLDocumentFragmentScannerImpl#scanDocument
方法,这个方法首先会针对xml数据流中的 document 部分进行扫描。
然后就会扫描xml数据流中的 DTD 部分。
最后开始扫描 ELEMENT 部分。
然后调用 next 方法。
跟进 XMLDocumentFragmentScannerImpl#next ,这个方法会针对一些特殊字符进行标记。
而我们刚刚payload: <doc>&xxe;</doc>
里面存在 &
特殊字符,所以这里会调用 setScannerState 构造方法将 fScannerState
设置为 SCANNER_STATE_REFERENCE 。
代码继续往下走,会来到 XMLDocumentFragmentScannerImpl#next 方法中的下图代码位置,我们已经知道这时候的 fScannerState
是 SCANNER_STATE_REFERENCE ,所以这里的 case 应该要来到 SCANNER_STATE_REFERENCE
中。
在 SCANNER_STATE_REFERENCE
这个case中,会调用 scanEntityReference 来处理 fContentBuffer 中的数据。跟进 scanEntityReference 方法,首先会获取 scanName ,我们payload里面的name自然是xxe,所以这里debug的结果也是xxe。
代码继续下行,来到下图位置,调用 XMLEntityManager#startEntity 进行处理。
跟进 XMLEntityManager#startEntity 最后会调用 startEntity(String name,XMLInputSource xmlInputSource,boolean literal, boolean isExternal)
这个构造方法,并且可以看到已经解析了payload中的外部地址。
在 startEntity 方法中调用了 setupCurrentEntity 方法。
跟进 XMLEntityManager#setupCurrentEntity ,这里可以看到解析了我们payload中的地址,并且发起了http的请求。
实际调用栈
setupCurrentEntity:646, XMLEntityManager (com.sun.org.apache.xerces.internal.impl) startEntity:1300, XMLEntityManager (com.sun.org.apache.xerces.internal.impl) startEntity:1237, XMLEntityManager (com.sun.org.apache.xerces.internal.impl) scanEntityReference:1908, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl) next:3067, XMLDocumentFragmentScannerImpl$FragmentContentDriver (com.sun.org.apache.xerces.internal.impl) next:606, XMLDocumentScannerImpl (com.sun.org.apache.xerces.internal.impl) scanDocument:510, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl) parse:848, XML11Configuration (com.sun.org.apache.xerces.internal.parsers) parse:777, XML11Configuration (com.sun.org.apache.xerces.internal.parsers) parse:141, XMLParser (com.sun.org.apache.xerces.internal.parsers) parse:243, DOMParser (com.sun.org.apache.xerces.internal.parsers) parse:348, DocumentBuilderImpl (com.sun.org.apache.xerces.internal.jaxp) parse:121, DocumentBuilder (javax.xml.parsers) main:19, DocumentXXE (com.l1nk3r.xxe.javaxxe)
目前百度查询xxe的修复方式实际上有这么一段代码,但是这么一段代码实际上是不会生效。
看看为啥不起作用,可以选择在 dbf.setExpandEntityReferences(false);
下一个断点。 DocumentBuilderFactoryImpl 这个类中的 expandEntityRef 变量的值默认是true,通过 setExpandEntityReferences(false)
之后将 expandEntityRef 变量的值设置为false。
代码继续下行来到 dbf.newDocumentBuilder 中。
该方法会返回一个实例化的 DocumentBuilderFactoryImpl 对象。
而在 DocumentBuilderFactoryImpl 这个对象中,会根据刚刚的 expandEntityRef 的值取反之后赋值给 CREATE_ENTITY_REF_NODES_FEATURE ,也就是 http://apache.org/xml/features/dom/create-entity-ref-nodes
,对应的结果为 true 。
最后这里处理完之后自然返回 DocumentBuilderFactoryImpl 对象,代码继续下行来到 parse 处。
前面步骤省略,和之前一致,来到 XMLParser#parse 这里之后,这里有个 reset 方法。
跟进这个 reset 方法,实际上来到了 AbstractDOMParser#reset 中,并且将 fCreateEntityRefNodes 的值设置为我们刚刚修改过,也就是 true 这个值。
这是到这部分的调用栈
之后代码继续下行,在 XMLDocumentFragmentScannerImpl#scanDocument
方法中,会先扫描 document 部分,再扫描 DTD 部分,最后扫描 ELEMENT 部分。 XMLDocumentFragmentScannerImpl#next 会针对 &
进行标记,调用 scanEntityReference 方法将 fScannerState
设置为 SCANNER_STATE_REFERENCE ,最后依然会调用 setupCurrentEntity 创建连接并发起请求,以获取外部实体的内容。
然后继续往下走来到 XMLEntityManager#endEntity ,经过一系列调用会来到 AbstractDOMParser#endGeneralEntity
中,会判断前面设置的 fCreateEntityRefNodes 的值,如果为 true则展开实体引用到生成的文档中替换掉 &xxx
的实体引用声明,设置为false则保留实体引用声明的DOM树在生成的文档中。
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); /*以下为修复代码*/ //https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#Java //禁用DTDs (doctypes),几乎可以防御所有xml实体攻击 dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); //首选 //如果不能禁用DTDs,可以使用下两项,必须两项同时存在 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); //防止外部实体POC dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); //防止参数实体POC /*以上为修复代码*/ DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(request.getInputStream());
当然如果还不放心的话,下面是owasp推荐的,其实也就是多了三个属性的设置,具体可以看看下面
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); String FEATURE = null; try { FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; dbf.setFeature(FEATURE, true); // If you can't completely disable DTDs, then at least do the following: // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities // JDK7+ - http://xml.org/sax/features/external-general-entities FEATURE = "http://xml.org/sax/features/external-general-entities"; dbf.setFeature(FEATURE, false); // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities // JDK7+ - http://xml.org/sax/features/external-parameter-entities FEATURE = "http://xml.org/sax/features/external-parameter-entities"; dbf.setFeature(FEATURE, false); // Disable external DTDs as well FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; dbf.setFeature(FEATURE, false); // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); >.. // Load XML file or stream using a XXE agnostic configured parser... DocumentBuilder safebuilder = dbf.newDocumentBuilder();
当然还是需要看一下原理,选择在下面这个位置代码下个断点。
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
前面我们在错误修复方法里面,在 dbf.setExpandEntityReferences(false);
的时候,我们知道在 DocumentBuilderImpl 类中,调用 domParser.setFeature
将 expandEntityRef 取反之后赋值给 CREATE_ENTITY_REF_NODES_FEATURE ,之后调用 reset 方法的时候将 fCreateEntityRefNodes 的设置为 CREATE_ENTITY_REF_NODES_FEATURE 的结果。
回到现在这个设置,有点不太一样,在 DocumentBuilderImpl 类中找不到我们设置的这个 disallow-doctype-decl
的配置,因此会继续向下,下图是在 DocumentBuilderImpl 中第234行,调用 setFeatures 方法。
这个方法会继续向下行,来到了 XMLDocumentScannerImpl#setFeature 中,并且将 fDisallowDoctype 设置为 true
,这是整个 setFeature 操作过程中的调用链。
紧接着进入到 XMLDocumentFragmentScannerImpl#scanDocument ,我们知道首先会扫描 document
部分,然后调用 XMLDocumentScannerImpl$PrologDriver#next 方法。
在 XMLDocumentScannerImpl$PrologDriver#next 方法中,调用 setScannerState 将 fScannerState 设置为24,也就是 SCANNER_STATE_DOCTYPE 属性。
紧接着代码继续下行,根据 fScannerState 进行选择,这里我们前面的 fScannerState 的状态设置为了 SCANNER_STATE_DOCTYPE ,所以自然进入这个case中,然后针对我们之前的 fDisallowDoctype 属性进行判断,如果是true的话,就抛出异常。
这是到这个部分的调用栈。
异常会被抛到 XML11Configuration.parse()
中处理。处理的结果是 fParseInProgress
变量被设置为了 false
,接着会调用 cleanup()
方法在完全解析XML文档之前终止解析,这个是到最后终止部分的调用栈。
测试代码:
package com.l1nk3r.xxe.javaxxe; import java.io.ByteArrayInputStream; import java.io.InputStream; import org.jdom2.Document;d import org.jdom2.input.SAXBuilder; public class SAXBuilderXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); SAXBuilder sb = new SAXBuilder(); Document doc = sb.build(is); } }
下图是这个方法到触发xxe的调用栈,可以看到画圈的部分和我们前面分析的 DocumentBuilder 中造成xxe的调用栈基本相似,最后都是到 XMLEntityManager#setupCurrentEntity 中触发xxe。
修复建议可以参考下列代码,注意实例化 SAXBuilder 类和 build 方法解析之间的这些属性。
SAXBuilder sb = new SAXBuilder(); sb.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); sb.setFeature("http://xml.org/sax/features/external-general-entities", false); sb.setFeature("http://xml.org/sax/features/external-parameter-entities", false); sb.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); Document doc = sb.build(is);
可以看到实际上将 disallow-doctype-decl 设置为true之后,就会抛出以下报错,具体原因和 DocumentBuilder
一致。
测试代码:
package com.l1nk3r.xxe.javaxxe; import org.xml.sax.HandlerBase; import java.io.ByteArrayInputStream; import java.io.InputStream; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; public class SAXParseFactoryXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser parser = spf.newSAXParser(); parser.parse(is, (HandlerBase) null); } }
默认情况下也存在xxe,具体看下图调用栈就懂了。
SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); spf.setFeature("http://xml.org/sax/features/external-general-entities", false); spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); SAXParser parser = spf.newSAXParser();
测试代码:
package com.l1nk3r.xxe.javaxxe; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.stream.StreamSource; import java.io.ByteArrayInputStream; import java.io.InputStream; public class SAXTransformerFactoryXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance(); StreamSource source = new StreamSource(is); sf.newTransformerHandler(source); } }
调用栈:
SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance(); sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); StreamSource source = new StreamSource(is); sf.newTransformerHandler(source);
测试代码:
package com.l1nk3r.xxe.javaxxe; import java.io.ByteArrayInputStream; import java.io.InputStream; import org.dom4j.io.SAXReader; public class SAXReaderXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); SAXReader saxReader = new SAXReader(); saxReader.read(is); } }
调用栈
SAXReader saxReader = new SAXReader(); saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false); saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); saxReader.read(is);
测试代码:
package com.l1nk3r.xxe.javaxxe; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import java.io.ByteArrayInputStream; import java.io.InputStream; public class XMLReaderXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); XMLReader reader = XMLReaderFactory.createXMLReader(); reader.parse(new InputSource(is)); } }
调用栈:
XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); reader.setFeature("http://xml.org/sax/features/external-general-entities", false); reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); reader.parse(new InputSource(is));
测试代码:
package com.l1nk3r.xxe.javaxxe; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import java.io.ByteArrayInputStream; import java.io.InputStream; public class SchemaFactoryXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); StreamSource source = new StreamSource(is); Schema schema = factory.newSchema(source); } }
调用栈:
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); StreamSource source = new StreamSource(is); Schema schema = factory.newSchema(source);
测试代码:
package com.l1nk3r.xxe.javaxxe; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamReader; public class XMLInputFactoryXxe { public static void main(String[] args) throws Exception { XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(ResourceUtils.getPoc1()); try { while (reader.hasNext()) { int type = reader.next(); if (type == XMLStreamConstants.START_ELEMENT) {//开始节点 System.out.print(reader.getName()); } else if (type == XMLStreamConstants.CHARACTERS) {//表示事件字符 System.out.println("type" + type); } else if (type == XMLStreamConstants.END_ELEMENT) {//结束节点 System.out.println(reader.getName()); } } reader.close(); } catch (Exception e) { e.printStackTrace(); } } }
package com.l1nk3r.xxe.javaxxe; import java.io.InputStream; public class ResourceUtils { public static InputStream getPoc1() { return ResourceUtils.class.getClassLoader().getResourceAsStream("poc1.xml"); } }
调用栈:
XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(ResourceUtils.getPoc1());
测试代码:
package com.l1nk3r.xxe.javaxxe; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stream.StreamSource; import java.io.ByteArrayInputStream; import java.io.InputStream; public class TransformerFactoryXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); TransformerFactory tf = TransformerFactory.newInstance(); StreamSource source = new StreamSource(is); tf.newTransformer().transform(source, new DOMResult()); } }
调用栈:
TransformerFactory tf = TransformerFactory.newInstance(); tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); StreamSource source = new StreamSource(is); tf.newTransformer().transform(source, new DOMResult());
测试代码:
package com.l1nk3r.xxe.javaxxe; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import java.io.ByteArrayInputStream; import java.io.InputStream; public class ValidatorSampleXxe { public static void main(String[] args) throws Exception { String str = "<!DOCTYPE doc [ /n" + "<!ENTITY xxe SYSTEM /"http://127.0.0.1:8888/">/n" + "]><doc>&xxe;</doc>"; InputStream is = new ByteArrayInputStream(str.getBytes()); SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); Schema schema = factory.newSchema(); Validator validator = schema.newValidator(); StreamSource source = new StreamSource(is); validator.validate(source); } }
调用栈:
Schema schema = factory.newSchema(); Validator validator = schema.newValidator(); validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); StreamSource source = new StreamSource(is); validator.validate(source);
测试代码:
package com.l1nk3r.xxe.javaxxe; import javax.xml.bind.JAXBContext; import javax.xml.bind.Unmarshaller; public class UnmarshallerXxe { public static void main(String[] args) throws Exception { Class tClass = UnmarshallerXxe.class; JAXBContext context = JAXBContext.newInstance(tClass); Unmarshaller um = context.createUnmarshaller(); Object o = um.unmarshal(ResourceUtils.getPoc1()); tClass.cast(o); } }
使用默认的解析方法不会存在 XXE
问题,这也是唯一一个使用默认的解析方法不会存在 XXE
的一个库。
通过上面的总结,默认情况下用 Unmarshaller 来处理xml不会发生xxe的问题。我们可以看到调用栈的过程中,存在xxe问题的库或者类实际上最后底层调用都是jdk自身处理xml的类,最后的核心触发流程都会来到 XMLEntityManager#setupCurrentEntity 当中。
针对修复方式实际上也是两种:
其一是通过setfeature的方式来防御 XXE
,主要几个配置项如下所示:
"http://apache.org/xml/features/disallow-doctype-decl", true "http://apache.org/xml/features/nonvalidating/load-external-dtd", false "http://xml.org/sax/features/external-general-entities", false "http://xml.org/sax/features/external-parameter-entities", false
还有一种是通过修改 XMLConstants 这个类的一些配置来进行修复:
XMLConstants.ACCESS_EXTERNAL_DTD, "" XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""
实际上通常禁用DTD,就ok了。但是由于一些实际业务情况,无法禁用DTD,这时候建议禁用通用实体和参数实体一起配置,因为禁用了通用实体就不会被回显,禁用了参数实体就不会被外带查询。
当然xml配置中还有很多冗余部分,具体可以看看前文 DocumentBuilder 中正确修复方式中的一些官方给的配置。
最后当然再提一个小建议,修复XXE建议采取最小化原则。如果一股脑将这些参数全部禁用或者限制,可能会出现一些奇怪的bug。