
POC لثغرة الوصول غير المصرح به إلى الملفات في Apache Livy
لأغراض تعليمية وبحوث أمنية فقط. لا تستخدم ضد أنظمة لا تملكها أو ليس لديك إذن كتابي صريح لاختبارها. → إخلاء المسؤولية الكامل
| الحقل | التفاصيل |
|---|---|
| معرّف CVE | CVE-2025-60012 |
| الخطورة | متوسطة (CVSS 6.3) |
| المتأثر | Apache Livy 0.7.0-incubating, 0.8.0-incubating — عند الاتصال بـ Apache Spark 3.1 أو أحدث |
| تم الإصلاح في | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: التحقق غير السليم من المدخلات |
| تاريخ الإفصاح | 2026-03-13 |
| المُبلِّغ | Furue Hideyuki |
يمكن لمستخدم مُصادَق يمتلك حق الوصول إلى واجهة Livy REST أو JDBC إرسال جلسة Spark أو مهمة دفعية بقيم إعدادات مصمَّمة بعناية. يجتمع ضعفان ليتمكّن المهاجم من الرجوع إلى ملفات نظام الملفات المحلي خارج المسارات المسموح بها:
غياب التحقق من spark.archives — قدّم Spark 3.1 خاصية spark.archives كطريقة
موحّدة لتوزيع ملفات الأرشيف عبر جميع مديري العناقيد. قائمة المفاتيح المُرمّزة الثابتة في
Livy 0.8.0 التي تخضع مفاتيح إعداداتها للتحقق من المسار (HARDCODED_SPARK_FILE_LISTS) لا
تتضمن spark.archives. لذلك لا يتم أبدًا فحص المسار المُمرَّر عبر هذا المفتاح مقابل
القائمة البيضاء لنظام الملفات المحلي (livy.file.local-dir-whitelist)، مما يسمح للمهاجم
بالرجوع إلى أي ملف محلي.
تجاوز اجتياز المسار في فحص القائمة البيضاء — حتى بالنسبة لمفاتيح الإعدادات التي يتم
التحقق منها فعليًا، تستخدم مقارنة القائمة البيضاء في Livy 0.8.0 استدعاء startsWith
من Java String على المسار الخام. يمكن للمهاجم تجاوز ذلك باستخدام اجتياز المسار:
/whitelisted/dir/../../etc/passwd يجتاز فحص السلسلة النصية لكنه يُحل إلى خارج
الدليل المسموح به.
LivyConf.scalaالإصدار المعرض (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
الإصدار المُصلَح (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Session.scalaالإصدار المعرض (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
الإصدار المُصلَح (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
تم استنساخ كلا الإصدارين مباشرة من مستودع Apache Livy الرسمي على GitHub إلى مساحة العمل هذه باستخدام الأوامر الدقيقة التالية:
المستودع: https://github.com/apache/incubator-livy```bash
git clone --depth=1 --branch v0.8.0-incubating
https://github.com/apache/incubator-livy
livy-0.8.0
git clone --depth=1 --branch v0.9.0-incubating
https://github.com/apache/incubator-livy
livy-0.9.0
| الإصدار | الوسم | الالتزام المُحدد | المسار المحلي |
|---------|-----|-----------------|------------|
| 0.8.0-incubating | `v0.8.0-incubating` | `78b512658e4baf1183f2b352203ada1928d8111a` | `./livy-0.8.0/` |
| 0.9.0-incubating | `v0.9.0-incubating` | `7215f209b25b96488189567807eaded00953a492` | `./livy-0.9.0/` |
## الاختلافات الدقيقة للكود
تم إنشاء الاختلافات عن طريق استنساخ الوسمين محليًا (انظر أعلاه) وتشغيل:```bash
diff -u livy-0.8.0/server/src/main/scala/org/apache/livy/LivyConf.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/LivyConf.scala
diff -u livy-0.8.0/server/src/main/scala/org/apache/livy/sessions/Session.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/sessions/Session.scala
LivyConf.scala: تمت إضافة spark.archives إلى قائمة الملفات المرمّزة```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**تأثير الإدخال المفقود في v0.8.0:**
عندما يرسل مستخدم جلسة تحتوي على `conf: {"spark.archives": "file:///etc/passwd"}`، فإن Livy
0.8.0 لا يستدعي أبدًا `resolveURIs()` على تلك القيمة ولا يتحقق منها مقابل
`livy.file.local-dir-whitelist`. يتم تمرير المسار إلى Spark دون تحقق.
---
### الإصلاح 2 — `Session.scala`: تطبيع المسار قبل فحص القائمة البيضاء```diff
def resolveURI(uri: URI, livyConf: LivyConf): URI = {
...
if (resolved.getScheme() == "file") {
- require(livyConf.localFsWhitelist.find(resolved.getPath().startsWith).isDefined,
+ require(livyConf.localFsWhitelist.find(
+ Paths.get(resolved.getPath()).normalize.startsWith).isDefined,
s"Local path ${uri.getPath()} cannot be added to user sessions.")
}
}
التأثير في v0.8.0:
يمكن تجاوز فحص السلسلة النصية الخام startsWith باستخدام حمولة اجتياز المسار.
مثال: إذا كان `livy.file.local-dir-whitelist = /opt/safe-data```` /opt/safe-data/../../../etc/passwd
- v0.8.0: `"/opt/safe-data/../../../etc/passwd".startsWith("/opt/safe-data")` → **true** (تم تجاوزه)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (تم حظره)
## ملخص ناقل الهجوم```
Attacker (authenticated REST/JDBC user)
│
▼
POST /sessions (or /batches)
{
"conf": {
"spark.archives": "file:///etc/shadow" ← Attack 1: unvalidated Spark 3.1 key
"spark.jars": "file:///safe/../etc/shadow" ← Attack 2: path traversal bypass
}
}
│
▼
Livy 0.8.0 — validation skipped / bypassed
│
▼
Spark reads the file and distributes it to executors
│
▼
Attacker retrieves file contents via job output / logs
تم تنفيذ والتحقق من جميع خطوات إثبات المفهوم (PoC) هذه على النظام التالي:
| المكوّن | التفاصيل |
|---|---|
| نظام التشغيل المضيف | Ubuntu 24.04.4 LTS (Noble Numbat) |
| النواة | 6.17.0-14-generic x86_64 |
| البنية | x86_64 |
| إجمالي الذاكرة | 15.49 GiB |
| محرك Docker | 28.2.2 |
| JDK المضيف | OpenJDK 17.0.18 (يُستخدم على المضيف فقط — تستخدم الحاويات eclipse-temurin:11-jdk-focal) |
| الصورة الأساسية للحاوية | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| إصدار Spark (لكلتا الصورتين) | 3.1.3 with Hadoop 3.2 |
| إصدار Livy — الصورة القابلة للاستغلال | 0.8.0-incubating (Scala 2.12 build) |
| إصدار Livy — الصورة المصلحة | 0.9.0-incubating (Scala 2.12 build) |
---``` . ├── LICENSE ├── README.md ├── docker/ │ ├── fixed/ │ │ ├── Dockerfile │ │ └── livy.conf │ └── vulnerable/ │ ├── Dockerfile │ └── livy.conf ├── livy-0.8.0/ ← Apache Livy 0.8.0-incubating source ├── livy-0.9.0/ ← Apache Livy 0.9.0-incubating source └── test/ └── validate.sh
---
## إثبات المفهوم