
Apache Spark 4.2.0 - عالي - عن بُعد - كتابة ملف - تطبيق History appStorePath معرّف التطبيق غير المعقّم
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-store-path
المقال: https://abraxaslabs.tech/research/spark-history-store-path
الفئة: كتابة ملف (مشروطة بالإعدادات) الوصول: عن بُعد
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
تقوم HistoryServerDiskManager.appStorePath بتنفيذ new File(appStoreDir, appId + extension) دون أي تنقية. إن Utils.sanitizeDirName مخصصة فقط لأسماء ملفات سجل الأحداث. حقل JSON App ID يُقرأ عبر asText(). أما السرد فيخزّن event.appId.orNull. وتستخدم Lease.commit تلك السلسلة. معرّف التطبيق ../ يكتب ملف .rdb خارج spark.history.store.path. القيمة الافتراضية غير المضبوطة لـ store.path تكون في الذاكرة (دون كتابة). المحتويات هي ملف مخزن، وليست تنفيذًا عن بُعد (RCE).
| المعرّف | لا يوجد CVE بعد |
| الفئة | كتابة ملف (مخزن History على القرص مشروط بالإعدادات) |
| الوصول | عن بُعد (كاتب سجل الأحداث بالإضافة إلى History مع ضبط spark.history.store.path؛ قوائم التحكم بالوصول في الواجهة معطّلة افتراضيًا) |
| CWE | CWE-22 |
| CVSS | عالٍ: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N عند ضبط spark.history.store.path |
| المنتج | Apache Spark History Server |
| المتأثر | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| المصادقة | كاتب spark.history.fs.logDirectory (الافتراضي file:/tmp/spark-events)؛ يجب أن يكون spark.history.store.path مضبوطًا في History |
| الترخيص | GNU Affero GPL v3.0 |
| المختبر | 127.0.0.1:18990 فقط. أداة استغلال الربط المذكور. إثبات وجود ملف .rdb خارج store.path. |
نموذج الضحية نفسه كما في CVE-2025-54920: خادم History موجّه إلى دليل سجلات يستطيع المهاجم الكتابة فيه. لم يعد Jackson ينشئ أدوات من فئة HiveConnection. لا يزال مسار المخزن على القرص يثق بمعرّف التطبيق (App ID) القادم من JSON الأحداث.
| الطريقة | المصبّ | الصلاحية | المختبر مقابل المستنتج |
|---|---|---|---|
زرع سجل أحداث يكون فيه App ID بصيغة JSON هو ../<name> (أو ../../oracle/...). انتظر حتى تتم عملية الفحص. GET الذي يحمّل واجهة التطبيق / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath ثم Lease.commit renameTo | السلامة: إنشاء مسار .rdb (RocksDB الافتراضي) أو .ldb (LevelDB) خارج {store.path}/apps بصفته History UID. مُثبت مختبريًا على الـ gadget: escaped=yes، والملف /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb يحتوي على الشاهد. المحتويات هي ملف مخزن، وليست بايت كود مضيف يختاره المهاجم، وليست صدفة. | مُثبت مختبريًا (كتابة الـ gadget لبايتات الشاهد عند المسار المُهرَّب). إن renameTo الكامل لـ History على مجلد RocksDB هو نفس عملية الضم. |
| نفس App ID عندما يكون الوجهة المُهرَّبة مجلدًا موجودًا بالفعل | Lease.commit if (dst.isDirectory()) deleteStore(dst) ثم Utils.deleteRecursively | التوافر: حذف مجلد موجود ينتهي اسمه بالفعل بلاحقة الـ backend، ثم استبداله بالمخزن المؤجَّر. | مستنتج (تثبيت HistoryServerDiskManager.scala commit). كتب الـ gadget ملفًا، ولم يستدعِ deleteRecursively. |
spark.history.store.path الافتراضي غير المضبوط | LOCAL_STORE_DIR createOptional؛ diskManager = storePath.map | مغلق. سرد في الذاكرة. لا يوجد HistoryServerDiskManager. مُثبت مختبريًا gated=yes written=no. | مُثبت مختبريًا |
تطبيق Utils.sanitizeDirName على App ID أولًا | مساعد اسم ملف سجل الأحداث | يبقى داخل apps/. يتحول / و : إلى -. مُثبت مختبريًا سلبيًا. | مُثبت مختبريًا |
التحكم في App ID local-control مع ضبط store.path | نفس عملية الضم | يكتب {store.path}/apps/local-control.rdb. يبقى داخل apps/. | مُثبت مختبريًا |
مغلق مجاور: المخزن الافتراضي غير المضبوط (في الذاكرة). لا يزال Jackson sparkEventFromJson يتطلب SparkListenerEvent (CVE-2025-54920). احتياطي URL getAppUI eventlog_v2_ + nameForAppAndAttempt مُنقّى. مفتاح KV للسرد ليس مسار نظام ملفات. HybridStore / kvstore mapper لا يحتوي على Id.CLASS. لا يقبل History HTTP مسار رفع. الضغط الافتراضي معطّل. المنظّف الافتراضي معطّل. إرسال المهام على عنقود مفتوح (SECURITY.md FAQ). تحميل نموذج ML (docs/ml-security.md). هذه البقايا ليست RCE على المضيف.
ثبّتُ Spark 4.2.0، وليس master، وليس 4.3 RC. ثمانية وعشرون عملية بحث. إن RCE العام لـ History Jackson مغلق على هذه الطبقة. كتّاب سجل الأحداث يستدعون بالفعل Utils.sanitizeDirName لذا لا يمكن أن يحتوي ملف السجل على /. اسم ملف ذاكرة التخزين المؤقت على القرص يستخدم App ID الخام.
private[history] def appStorePath(appId: String, attemptId: Option[String]): File = {
val fileName = appId + attemptId.map("_" + _).getOrElse("") + extension
new File(appStoreDir, fileName)
}
appStoreDir هو {spark.history.store.path}/apps. في Unix، new File(parent, "../../oracle/name.rdb") يُحل خارج المجلد الأب. تأخذ JsonProtocol.applicationStartFromJson القيمة json.get("App ID").asText(). تُخزّن AppListingListener.onApplicationStart القيمة event.appId.orNull. تُغذّي lease.commit(appId, attempt.info.attemptId) تلك السلسلة النصية إلى appStorePath.
المنعطفات الخاطئة المسجّلة مسبقًا: التعامل مع store.path الافتراضي غير المضبوط على أنه كتابة (فهو في الذاكرة)؛ توقّع أن يتجاهل new File(dir, "/abs") في Unix المجلد الأب (وهو لا يفعل ذلك؛ فالباقي هو ..)؛ الادعاء بوجود RCE لأن اللاحقة هي .rdb؛ إقلاع History Server كامل بينما الربط هو الـ oracle؛ التعامل مع sanitizeDirName على أنها تغطي هذا المسار.
python3 spark-history-store-path-Abraxas-Labs.py
أو
cd lab
./run.sh
Gadget الخاص بـ HistoryServerDiskManager.appStorePath على 127.0.0.1:18990. ثبّت المقتطفات تحت lab/pin/. لا يتم إقلاع History Server الكامل. اربطه بواجهة loopback.
SUCCESS SPARK-HISTORY-STORE-PATH how=gadget escaped=yes gated=yes outside=/work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb SPARK-HISTORY-STORE-PATH-WITNESS
شغّل Utils.sanitizeDirName (أو ارفض .. / / / NUL) على appId وattemptId داخل appStorePath قبل new File. بعد الدمج، اشترط أن يبقى dst.getCanonicalFile() تحت appStoreDir.getCanonicalFile(). يبقى spark.history.store.path غير المضبوط هو الافتراضي الآمن.