同事问我研究过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。